diff --git a/backend/job/candidate/views.py b/backend/job/candidate/views.py
index e000ede..dba250c 100644
--- a/backend/job/candidate/views.py
+++ b/backend/job/candidate/views.py
@@ -1263,8 +1263,8 @@ class CandidateView:
payload["candidate_id"]=score.get("candidate_id")
if score.get("user_id") and not payload.get("user_id"):
payload["user_id"]=score["user_id"]
- if score.get("job_post_id"):
- payload["scored_job_post_id"]=score["job_post_id"]
+ if score.get("job_post_id"):
+ payload["scored_job_post_id"]=score["job_post_id"]
return await self.attach_application_history(payload)
# List mode: inbox applications + manual/form applications (dedupe by user).
inbox_payloads=await self.attach_job_posts(rows)
diff --git a/frontend/candidate-browse.test.mjs b/frontend/candidate-browse.test.mjs
new file mode 100644
index 0000000..0bb0e8c
--- /dev/null
+++ b/frontend/candidate-browse.test.mjs
@@ -0,0 +1,42 @@
+/**
+ * Candidate browse queue — unique ids, neighbors, profile paths.
+ *
+ * node candidate-browse.test.mjs
+ */
+import assert from 'node:assert/strict'
+import { uniqueBrowseEntries, neighborsOf, candidatePath } from './src/lib/candidateBrowse.js'
+
+const rows = [
+ { userId: 'a', name: 'Ada', stage: 'Interview', jobTitle: 'Backend' },
+ { user_id: 'a', name: 'Ada duplicate application' },
+ { userId: '', name: 'Skipped' },
+ { userId: 'b', email: 'b@example.com' },
+ { userId: 'c', name: 'Chris', job_title: 'Design' },
+]
+
+const entries = uniqueBrowseEntries(rows)
+assert.deepEqual(entries.map((row) => row.userId), ['a', 'b', 'c'])
+assert.equal(entries[0].name, 'Ada')
+assert.equal(entries[0].stage, 'Interview')
+assert.equal(entries[1].name, 'b@example.com')
+assert.equal(entries[2].jobTitle, 'Design')
+
+const mid = neighborsOf(entries, 'b')
+assert.equal(mid.index, 1)
+assert.equal(mid.total, 3)
+assert.equal(mid.prev.userId, 'a')
+assert.equal(mid.next.userId, 'c')
+
+const first = neighborsOf(entries, 'a')
+assert.equal(first.prev, null)
+assert.equal(first.next.userId, 'b')
+
+const missing = neighborsOf(entries, 'z')
+assert.equal(missing.index, -1)
+assert.equal(missing.prev, null)
+assert.equal(missing.next, null)
+
+assert.equal(candidatePath('user/1'), '/candidate/user%2F1')
+assert.equal(candidatePath('abc', 'Resume'), '/candidate/abc?tab=Resume')
+
+console.log('All candidate browse checks passed')
diff --git a/frontend/candidate-profile.test.mjs b/frontend/candidate-profile.test.mjs
index 099384c..e411896 100644
--- a/frontend/candidate-profile.test.mjs
+++ b/frontend/candidate-profile.test.mjs
@@ -57,7 +57,13 @@ page.on('request', async (request) => {
let data = []
if (request.method() === 'OPTIONS') return request.respond({ status: 204, headers: { 'access-control-allow-origin': '*', 'access-control-allow-headers': '*', 'access-control-allow-methods': '*' } })
if (path === '/users/me') data = activeUser
- if (path === '/candidate/fetch') data = candidate
+ if (path === '/candidate/fetch') {
+ const userId = new URL(request.url()).searchParams.get('user_id')
+ if (!userId) data = []
+ else if (userId === 'test-candidate-b') data = { ...candidate, user_id: 'test-candidate-b', name: 'Omar Ali' }
+ else if (userId === 'test-candidate-c') data = { ...candidate, user_id: 'test-candidate-c', name: 'Hina Raza' }
+ else data = candidate
+ }
if (path === '/forms/definitions') data = {}
if (path === '/s3/open') data = { url: `${base}/fixture-resume.pdf` }
if (path === '/fixture-resume.pdf') return request.respond({ status: 200, contentType: 'application/pdf', body: '%PDF-1.4\n%%EOF' })
@@ -72,6 +78,13 @@ page.on('request', async (request) => {
})
await page.evaluateOnNewDocument((account) => {
localStorage.setItem('tf-auth', JSON.stringify({ access_token: 'fixture-token', refresh_token: 'fixture-refresh', expires_at: Date.now() + 3600000, data: account }))
+ sessionStorage.setItem('tf-candidate-browse', JSON.stringify({
+ entries: [
+ { userId: 'test-candidate', name: 'Sarah Khan', stage: 'Shortlist', jobTitle: 'Marketing Manager' },
+ { userId: 'test-candidate-b', name: 'Omar Ali', stage: 'Interview', jobTitle: 'Marketing Manager' },
+ { userId: 'test-candidate-c', name: 'Hina Raza', stage: 'Screening', jobTitle: 'Content Lead' },
+ ],
+ }))
}, user)
async function clickText(selector, text) {
const clicked = await page.evaluate((selector, text) => {
@@ -89,7 +102,8 @@ try {
await load()
assert.equal(await page.$eval('.cw-hero h1', (element) => element.textContent), 'Sarah Khan')
assert.equal(await page.$$eval('.cw-applications tbody tr', (rows) => rows.length), 3)
- assert.equal(await page.$('.sidebar'), null, 'Detail page must use full width')
+ assert.ok(await page.$('.sidebar'), 'Candidate page keeps the app sidebar')
+ assert.equal(await page.$eval('.cw-browse-counter', (element) => element.textContent.trim()), '1 of 3')
assert.equal(writes.length, 0, 'Opening a candidate is read-only')
for (const width of [1536, 1280, 1024, 768, 390, 320]) {
await page.setViewport({ width, height: 1100 })
@@ -102,6 +116,13 @@ try {
}
console.log('ok Profile data, 3-column desktop and mobile layouts (320–1536px)')
await page.setViewport({ width: 1536, height: 1100 })
+ await page.click('[aria-label="Next candidate"]')
+ await page.waitForFunction(() => document.querySelector('.cw-hero h1')?.textContent === 'Omar Ali')
+ assert.equal(await page.$eval('.cw-browse-counter', (element) => element.textContent.trim()), '2 of 3')
+ assert.match(page.url(), /\/candidate\/test-candidate-b/)
+ await page.click('[aria-label="Previous candidate"]')
+ await page.waitForFunction(() => document.querySelector('.cw-hero h1')?.textContent === 'Sarah Khan')
+ console.log('ok Previous / next walks the candidate list')
await clickText('.cand-page-actions button', 'Favorite')
await page.waitForFunction(() => document.querySelector('.cand-page-actions button').getAttribute('aria-pressed') === 'true')
await page.click('.cw-rating [role=radio]:nth-child(5)')
diff --git a/frontend/package.json b/frontend/package.json
index 158c403..2bd2e46 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,8 +16,9 @@
"test:candidates": "node candidates-table.test.mjs",
"test:profile": "node candidate-profile.test.mjs",
"test:cvbank": "node cvbank.test.mjs",
+ "test:browse": "node candidate-browse.test.mjs",
"test:mobile": "node mobile.test.mjs",
- "verify": "vite build && node smoke.test.mjs && node token.test.mjs && node theme.test.mjs && node format.test.mjs && node inbox-loading.test.mjs && node candidates-table.test.mjs && node cvbank.test.mjs"
+ "verify": "vite build && node smoke.test.mjs && node token.test.mjs && node theme.test.mjs && node format.test.mjs && node inbox-loading.test.mjs && node candidates-table.test.mjs && node cvbank.test.mjs && node candidate-browse.test.mjs"
},
"dependencies": {
"@tanstack/react-query": "^5.101.4",
diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx
index b4a364b..5aab174 100644
--- a/frontend/src/app/AppLayout.jsx
+++ b/frontend/src/app/AppLayout.jsx
@@ -39,17 +39,17 @@ export default function AppLayout() {
}, [location.pathname, setNavOpen])
return (
-
+
Skip to content
- {(!candidateView || navOpen) &&
}
+ />
- setNavOpen((o) => !o)} searchRef={searchRef} candidateView={candidateView} />
+ setNavOpen((o) => !o)} searchRef={searchRef} />
{/* Keyed by pathname: navigating away from a crashed screen resets it. */}
@@ -60,14 +60,14 @@ export default function AppLayout() {
- {!candidateView &&
setDockOpen(true)}
title="AI Recruiter Assistant"
aria-label="Open AI Assistant"
>
- }
+
setDockOpen(false)} />
diff --git a/frontend/src/app/GlobalSearch.jsx b/frontend/src/app/GlobalSearch.jsx
index 3555140..4190c46 100644
--- a/frontend/src/app/GlobalSearch.jsx
+++ b/frontend/src/app/GlobalSearch.jsx
@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query'
import { Avatar, Icon } from '../ui/primitives'
import { qk } from '../lib/queryKeys'
+import { openCandidateProfile } from '../lib/candidateBrowse'
import * as searchApi from '../api/search'
export default function GlobalSearch({ inputRef }) {
@@ -79,7 +80,7 @@ export default function GlobalSearch({ inputRef }) {
{candidates.length > 0 && Candidates
}
{candidates.map((c) => (
- go('/candidates', { openCandidate: c.id })}>
+
openCandidateProfile(navigate, c.id, candidates.map((row) => ({ userId: row.id, name: row.name })))}>
{c.name}
diff --git a/frontend/src/app/Topbar.jsx b/frontend/src/app/Topbar.jsx
index 947666f..27bc55b 100644
--- a/frontend/src/app/Topbar.jsx
+++ b/frontend/src/app/Topbar.jsx
@@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Dropdown, { DropdownGroup } from '../ui/Dropdown'
import GlobalSearch from './GlobalSearch'
-import { BrandGlyph } from '../components/BrandMark'
import { Icon } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useTheme } from '../theme/ThemeProvider'
@@ -29,7 +28,7 @@ async function fetchNotifications() {
}
}
-export default function Topbar({ onOpenNav, searchRef, candidateView = false }) {
+export default function Topbar({ onOpenNav, searchRef }) {
const { theme, toggleTheme } = useTheme()
const { user, signOut } = useAuth()
const { toast } = useToast()
@@ -69,10 +68,6 @@ export default function Topbar({ onOpenNav, searchRef, candidateView = false })
return (
- {candidateView &&
-
- Utopia Brands HR Portal
- }
@@ -80,7 +75,7 @@ export default function Topbar({ onOpenNav, searchRef, candidateView = false })
- {!candidateView &&
- }
+
entry.userId === id)
+ return {
+ index,
+ total: entries.length,
+ prev: index > 0 ? entries[index - 1] : null,
+ next: index >= 0 && index < entries.length - 1 ? entries[index + 1] : null,
+ current: index >= 0 ? entries[index] : null,
+ entries,
+ }
+}
+
+export function candidatePath(userId, tab) {
+ const path = `/candidate/${encodeURIComponent(userId)}`
+ return tab ? `${path}?tab=${encodeURIComponent(tab)}` : path
+}
+
+export function rememberCandidateBrowse(rows) {
+ const entries = uniqueBrowseEntries(rows)
+ try {
+ sessionStorage.setItem(BROWSE_KEY, JSON.stringify({ entries }))
+ } catch { /* private mode / quota — browsing still works for this click */ }
+ return entries
+}
+
+export function readCandidateBrowse() {
+ try {
+ const parsed = JSON.parse(sessionStorage.getItem(BROWSE_KEY) || 'null')
+ return uniqueBrowseEntries(parsed?.entries || [])
+ } catch {
+ return []
+ }
+}
+
+export function openCandidateProfile(navigate, userId, rows, { replace = false, tab } = {}) {
+ if (rows) rememberCandidateBrowse(rows)
+ if (!userId) return
+ navigate(candidatePath(userId, tab), { replace })
+}
diff --git a/frontend/src/lib/queryKeys.js b/frontend/src/lib/queryKeys.js
index 32463ba..ff420a2 100644
--- a/frontend/src/lib/queryKeys.js
+++ b/frontend/src/lib/queryKeys.js
@@ -107,6 +107,7 @@ export const qk = {
applications: (email) => ['candidates', 'applications', email],
matching: (p = {}) => ['candidates', 'matching', p],
matchingDetail: (id) => ['candidates', 'matching', 'detail', id],
+ browse: (p = {}) => ['candidates', 'browse', p],
},
// Board rows come from the same endpoint as qk.candidates.list but are cached
// MAPPED (kanban cards, not the raw envelope), so they need their own key —
diff --git a/frontend/src/screens/CandidateBrowse.jsx b/frontend/src/screens/CandidateBrowse.jsx
new file mode 100644
index 0000000..7614780
--- /dev/null
+++ b/frontend/src/screens/CandidateBrowse.jsx
@@ -0,0 +1,31 @@
+import { Icon } from '../ui/primitives'
+
+export function CandidateBrowseNav({ browse, onBrowse }) {
+ if (!browse || browse.index < 0 || browse.total < 1) return null
+ const { index, total, prev, next } = browse
+ return (
+
+ prev && onBrowse(prev.userId)}
+ >
+
+
+ {index + 1} of {total}
+ next && onBrowse(next.userId)}
+ >
+
+
+
+ )
+}
diff --git a/frontend/src/screens/CandidatePage.jsx b/frontend/src/screens/CandidatePage.jsx
index 533eb2b..254cba9 100644
--- a/frontend/src/screens/CandidatePage.jsx
+++ b/frontend/src/screens/CandidatePage.jsx
@@ -4,21 +4,87 @@
need a real page with a real URL (shareable, refresh-safe). This is a thin
shell over CandidateProfile in `variant="page"` mode: the identity shell
carries only the userId and the live detail query fills everything else.
- Opened from Candidates, Talent Pool and the Pipeline board. */
+ Opened from Candidates, Talent Pool and the Pipeline board.
-import { useNavigate, useParams } from 'react-router-dom'
+ Previous / Next walk the session queue written when the recruiter opened
+ this profile from a list. A direct URL falls back to the first page of
+ candidates they can already see, so the arrows still do something. */
+import { useEffect } from 'react'
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
+import { useQuery } from '@tanstack/react-query'
+
+import { useAuth } from '../auth/AuthContext'
+import { isHiringManager } from '../auth/permissions'
+import { qk } from '../lib/queryKeys'
+import { candidatePath, neighborsOf, readCandidateBrowse, uniqueBrowseEntries } from '../lib/candidateBrowse'
+import * as candidatesApi from '../api/candidates'
+import * as pipelineApi from '../api/pipeline'
import CandidateProfile from './CandidateProfile'
+function useCandidateBrowse(userId) {
+ const { user } = useAuth()
+ const manager = isHiringManager(user)
+ const stored = readCandidateBrowse()
+ const fallback = useQuery({
+ queryKey: qk.candidates.browse({ manager }),
+ queryFn: async () => {
+ if (manager) {
+ const res = await candidatesApi.listForManager({ limit: 200, offset: 0 })
+ const rows = Array.isArray(res?.data) ? res.data : []
+ return uniqueBrowseEntries(rows.map((row) => ({
+ ...row,
+ stage: pipelineApi.STAGE_FROM_STATUS[String(row.application_status || '').toUpperCase()] || null,
+ })))
+ }
+ const res = await candidatesApi.list({ limit: 100, offset: 0 })
+ const rows = Array.isArray(res?.data) ? res.data.map(candidatesApi.toApplicationListView) : []
+ return uniqueBrowseEntries(rows)
+ },
+ enabled: stored.length === 0 && Boolean(userId),
+ staleTime: 30_000,
+ })
+ const entries = stored.length ? stored : (fallback.data ?? [])
+ return neighborsOf(entries, userId)
+}
+
export default function CandidatePage() {
const { userId } = useParams()
const navigate = useNavigate()
+ const [searchParams] = useSearchParams()
+ const browse = useCandidateBrowse(userId)
+ const tab = searchParams.get('tab') || undefined
+
+ function goTo(id) {
+ if (!id || String(id) === String(userId)) return
+ navigate(candidatePath(id, tab))
+ }
+
+ useEffect(() => {
+ function onKey(event) {
+ if (!event.altKey || event.metaKey || event.ctrlKey) return
+ const tag = event.target?.tagName
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable) return
+ if (event.key === 'ArrowLeft' && browse.prev) {
+ event.preventDefault()
+ goTo(browse.prev.userId)
+ }
+ if (event.key === 'ArrowRight' && browse.next) {
+ event.preventDefault()
+ goTo(browse.next.userId)
+ }
+ }
+ window.addEventListener('keydown', onKey)
+ return () => window.removeEventListener('keydown', onKey)
+ }, [browse.prev, browse.next, userId, tab])
return (
(window.history.length > 1 ? navigate(-1) : navigate('/candidates'))}
/>
)
diff --git a/frontend/src/screens/CandidateProfile.jsx b/frontend/src/screens/CandidateProfile.jsx
index 23632bc..2f07cbc 100644
--- a/frontend/src/screens/CandidateProfile.jsx
+++ b/frontend/src/screens/CandidateProfile.jsx
@@ -20,6 +20,7 @@ import * as pipelineApi from '../api/pipeline'
import * as s3Api from '../api/s3'
import CandidateFormsTab from './CandidateForms'
import CandidateWorkspaceOverview, { CandidateWorkspaceHero } from './CandidateWorkspace'
+import { CandidateBrowseNav } from './CandidateBrowse'
import { PreviousApplications, ReappliedBadge, candidateApplicationsOf } from '../components/ReapplicantHistory'
import { fmtDate, fmtTime, toDate } from '../lib/format'
import { companies, moneyK, pick } from '../data/seed'
@@ -109,18 +110,27 @@ function useProfileWrite({ userId, mutationFn, success, onDone }) {
*/
export default function CandidateProfile({
candidate: c, atsScore = null, recommendation = null, onClose, onAdvance, onToggleFav, onAtsMatch,
- variant = 'modal',
+ variant = 'modal', browse = null, onBrowse,
}) {
const { toast } = useToast()
const { can, user } = useAuth()
const isManager = isHiringManager(user)
const visibleTabs = isManager ? ['Forms', 'Notes'] : TABS
- const [searchParams] = useSearchParams()
+ const [searchParams, setSearchParams] = useSearchParams()
const [tab, setTab] = useState(() => tabFromSearch(
variant === 'page' ? searchParams.get('tab') : null,
visibleTabs,
isManager ? 'Forms' : 'Overview',
))
+ function changeTab(next) {
+ setTab(next)
+ if (variant !== 'page') return
+ setSearchParams((prev) => {
+ const params = new URLSearchParams(prev)
+ params.set('tab', next)
+ return params
+ }, { replace: true })
+ }
const tabId = useId()
const [dialog, setDialog] = useState(null)
const [stageReason, setStageReason] = useState('')
@@ -358,7 +368,7 @@ export default function CandidateProfile({
({ key: t, label: t === 'Interview' ? 'Interviews' : t, count: counts && ['Interview', 'Forms', 'Notes'].includes(t) ? counts[t] : undefined }))}
/>
@@ -366,7 +376,7 @@ export default function CandidateProfile({
{tab === 'Overview' && (guard || (variant === 'page' ?
setRating.mutate(n)}
atsScore={atsScore} recommendation={recommendation}
atsAction={canScoreAts && can('candidates.create') ? scoreAts.mutate()}> {scoreAts.isPending ? 'Scoring…' : 'Score with ATS'} : null}
@@ -642,15 +652,16 @@ export default function CandidateProfile({
return (
-
- Back
-
- Candidates / {live?.name || c.name || '…'}
+
+ Back
+
+ Candidates / {live?.name || c.name || '…'}
+
+
+ {!isManager && setFavorite.mutate(!favorite)}> {favorite ? 'Favorited' : 'Favorite'} }
+ {onBrowse && }
- {!isManager &&
- setFavorite.mutate(!favorite)}> {favorite ? 'Favorited' : 'Favorite'}
-
}
{body}
{dialog && live &&
{
- const id = location.state?.openCandidate
- if (id) navigate(`/candidate/${id}`, { replace: true })
- }, [location.state, navigate])
-
const listQuery = useQuery({
queryKey: qk.candidates.managerList(),
queryFn: async () => {
@@ -194,6 +190,11 @@ function HiringManagerCandidates() {
})
}, [rowsAll, q, jobId])
+ useEffect(() => {
+ const id = location.state?.openCandidate
+ if (id) openCandidateProfile(navigate, id, rows.length ? rows : undefined, { replace: true })
+ }, [location.state, navigate, rows])
+
const columns = [
{
key: 'name',
@@ -286,7 +287,7 @@ function HiringManagerCandidates() {
? 'No candidates match these filters.'
: 'No candidates are allocated to jobs opened from your requisitions yet.'
}
- onRowClick={(r) => r.user_id && navigate(`/candidate/${r.user_id}`)}
+ onRowClick={(r) => r.user_id && openCandidateProfile(navigate, r.user_id, rows)}
/>
)}
@@ -382,10 +383,10 @@ function RecruiterCandidates() {
// Real candidates get the full profile PAGE; the modal stays only as the
// fallback for rows without a user account.
const uid = c.userId
- if (uid) navigate(`/candidate/${uid}`)
+ if (uid) openCandidateProfile(navigate, uid, candidates)
else setProfileFor(c)
},
- [qc, navigate],
+ [qc, navigate, candidates],
)
// Deep links from Talent Pool, global search, dashboard…
@@ -393,8 +394,8 @@ function RecruiterCandidates() {
const st = location.state
if (!st) return
if (st.openAdd) setAdding(true)
- if (st.openCandidate) navigate(`/candidate/${st.openCandidate}`, { replace: true })
- }, [location.state, navigate])
+ if (st.openCandidate) openCandidateProfile(navigate, st.openCandidate, candidates.length ? candidates : undefined, { replace: true })
+ }, [location.state, navigate, candidates])
const rows = useMemo(() => {
const f = filters
diff --git a/frontend/src/screens/CvBank.jsx b/frontend/src/screens/CvBank.jsx
index 70cf1bf..f286d4e 100644
--- a/frontend/src/screens/CvBank.jsx
+++ b/frontend/src/screens/CvBank.jsx
@@ -36,6 +36,7 @@ import { exportStyledXlsx } from '../lib/exportXlsx'
import { friendlyAuthError } from '../lib/errors'
import * as candidatesApi from '../api/candidates'
import * as s3Api from '../api/s3'
+import { openCandidateProfile } from '../lib/candidateBrowse'
import { avatarColor, initials as initialsOf } from '../data/seed'
const SEARCH_DEBOUNCE_MS = 300
@@ -225,7 +226,7 @@ export default function CvBank() {
async function view(row) {
if (!row.isStoredCv) {
- if (row.userId) navigate(`/candidate/${row.userId}`)
+ if (row.userId) openCandidateProfile(navigate, row.userId, rows)
else toast('This applicant has no profile to open', 'info')
return
}
diff --git a/frontend/src/screens/Pipeline.jsx b/frontend/src/screens/Pipeline.jsx
index 9e257d7..b41fdb7 100644
--- a/frontend/src/screens/Pipeline.jsx
+++ b/frontend/src/screens/Pipeline.jsx
@@ -27,6 +27,7 @@ import { friendlyAuthError } from '../lib/errors'
import * as jobPostsApi from '../api/jobPosts'
import * as pipelineApi from '../api/pipeline'
import { ReappliedBadge } from '../components/ReapplicantHistory'
+import { openCandidateProfile } from '../lib/candidateBrowse'
/* Stage colours reference CSS tokens so the board re-tints with the theme. */
export const KANBAN_STAGES = [
@@ -276,7 +277,7 @@ export default function Pipeline() {
// The profile page keys off users.id, so an application
// with no linked account cannot deep-link.
if (!c.userId) return
- navigate(`/candidate/${c.userId}`)
+ openCandidateProfile(navigate, c.userId, candidates)
}}
>
diff --git a/frontend/src/styles/candidate-workspace.css b/frontend/src/styles/candidate-workspace.css
index ad286f2..eaf3889 100644
--- a/frontend/src/styles/candidate-workspace.css
+++ b/frontend/src/styles/candidate-workspace.css
@@ -1,219 +1,183 @@
-/* Candidate detail is a focused, full-width workspace. Tokens stay scoped so
- returning to the rest of the ATS preserves the user's selected theme. */
-.candidate-workspace[data-theme="dark"], .candidate-dialog {
- color-scheme: dark;
- --bg: #03171d;
- --bg-elev: #071e26;
- --bg-sunken: #0c2933;
- --border: #1b404b;
- --border-strong: #315764;
- --text: #edf7fa;
- --text-2: #b6ced7;
- --text-3: #9ebbc6;
- --primary: #ccfa70;
- --primary-600: #dcff98;
- --primary-fg: #14210b;
- --primary-soft: #ccfa7012;
- --primary-border: #ccfa7040;
- --accent-ink: #ccfa70;
- --success: #25e9a5;
- --success-soft: #07543866;
- --warning: #ffd16e;
- --warning-soft: #614d164d;
- --danger: #ff7c86;
- --danger-soft: #66222c66;
- --info: #82bcff;
- --info-soft: #123c6866;
- --purple: #b6a6ff;
- --purple-soft: #3e326666;
- --teal: #25e9a5;
- --teal-soft: #07543866;
- --avatar-fg: #071720;
- --shadow: none;
- color: var(--text);
- background: var(--bg);
-}
-.candidate-workspace .main-wrap { width: 100%; margin-left: 0; }
-.candidate-workspace .content { padding: 0 42px 40px; background: radial-gradient(ellipse at 50% 0, #0b29304d, transparent 58%), var(--bg); }
-.candidate-workspace .topbar { min-height: 67px; height: auto; padding: 12px 42px; gap: 22px; background: #03181e; border-bottom: 1px solid var(--border); }
-.candidate-brand { display: flex; align-items: center; gap: 13px; min-width: 230px; color: var(--text); text-decoration: none; }
-.candidate-brand .brand-mark { width: 36px; height: 36px; fill: #25e9a5; }
-.candidate-brand strong { display: block; font-size: 18px; line-height: 1.25; letter-spacing: -.4px; }
-.candidate-brand small { display: block; color: var(--text-3); font-size: 12px; margin-top: 3px; }
-.candidate-workspace .menu-toggle { display: inline-flex; order: -1; width: 32px; }
-.candidate-workspace .topbar-search { max-width: 520px; }
-.candidate-workspace .topbar-search input { height: 38px; border-radius: 8px; background: #0c2832; font-size: 13px; }
-.candidate-workspace .profile-btn .avatar { background: #a19df5; color: #071720; }
-.candidate-workspace .sidebar { position: fixed; top: 0; bottom: 0; left: 0; z-index: 60; transform: none; }
-.candidate-workspace .cand-page { max-width: 1740px; margin-inline: auto; font-size: 13px; }
-.candidate-workspace .cand-page-bar { gap: 16px; min-height: 58px; margin: 0; }
-.candidate-workspace .cand-page-crumb { font-size: 12px; }
+/* Candidate detail is a compact workspace that follows the app theme.
+ Previous / Next live in the page bar. There is no extra candidate list. */
+.candidate-workspace .content { padding: 0 24px 28px; background: var(--bg); }
+html[data-theme="dark"] .candidate-workspace .content { background: radial-gradient(ellipse at 50% 0, #0b29304d, transparent 58%), var(--bg); }
+.candidate-workspace .cand-page { max-width: 1680px; margin-inline: auto; font-size: 13px; min-width: 0; }
+.candidate-workspace .cand-page-bar { gap: 10px; min-height: 44px; margin: 0; padding-block: 8px; }
+.candidate-workspace .cand-page-crumb { font-size: 12px; display: flex; align-items: center; min-width: 0; white-space: normal; overflow: visible; }
.candidate-workspace .cand-page-crumb span { margin: 0 8px; }
-.candidate-workspace .cand-page-actions { width: auto; }
-.candidate-workspace .btn, .candidate-dialog .btn { min-height: 36px; padding: 7px 12px; border-radius: 7px; gap: 8px; font-size: 12px; font-weight: 500; box-shadow: none; white-space: nowrap; }
-.candidate-workspace .btn-secondary, .candidate-dialog .btn-secondary { border: 1px solid var(--border); color: var(--text); background: linear-gradient(120deg, #0c2730, #071e26); }
-.candidate-workspace .btn-secondary:hover:not(:disabled), .candidate-dialog .btn-secondary:hover:not(:disabled) { background: #13333d; border-color: #39606b; }
-.candidate-workspace .btn-primary, .candidate-dialog .btn-primary { color: var(--primary-fg); border: 1px solid #c5ed6e; background: linear-gradient(105deg, #d3fd80, #c9f86b); font-weight: 650; }
-.candidate-workspace .btn-primary:hover:not(:disabled), .candidate-dialog .btn-primary:hover:not(:disabled) { background: #dcff9b; }
-.candidate-workspace .btn-sm { min-height: 30px; padding: 5px 8px; font-size: 12px; }
-.candidate-workspace .btn svg, .candidate-dialog .btn svg { width: 16px; height: 16px; flex-shrink: 0; }
+.candidate-workspace .cand-page-crumb strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.candidate-workspace .cand-page-actions { width: auto; margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
+.candidate-workspace .btn, .candidate-dialog .btn { min-height: 32px; padding: 5px 10px; border-radius: 7px; gap: 6px; font-size: 12px; font-weight: 500; box-shadow: none; white-space: nowrap; }
+.candidate-workspace .btn-primary, .candidate-dialog .btn-primary { font-weight: 650; }
+.candidate-workspace .btn-sm { min-height: 28px; padding: 4px 8px; font-size: 12px; }
+.candidate-workspace .btn svg, .candidate-dialog .btn svg { width: 15px; height: 15px; flex-shrink: 0; }
.candidate-workspace :where(button, a, input, select, textarea):focus-visible, .candidate-dialog :where(button, a, input, select, textarea):focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
.candidate-workspace button:disabled, .candidate-dialog button:disabled { cursor: not-allowed; opacity: .45; }
-.candidate-workspace .star-btn.on { color: var(--primary); border-color: #788e49; }
-.cw-hero { display: flex; gap: 24px; padding: 22px 24px 20px; border: 1px solid var(--border); border-radius: 13px; background: linear-gradient(110deg, #09252e, #061d25 70%, #09252c); }
-.cw-hero .cw-avatar { width: 78px; height: 78px; font-size: 28px; flex-shrink: 0; }
+.candidate-workspace .star-btn.on { color: var(--primary); border-color: var(--primary-border); }
+.cw-browse-nav { display: flex; align-items: center; gap: 4px; }
+.cw-browse-arrow { width: 32px; padding: 5px; flex-shrink: 0; }
+.cw-browse-counter { min-width: 52px; text-align: center; font-size: 12px; color: var(--text-2); white-space: nowrap; }
+
+.cw-hero { display: flex; gap: 16px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 12px; background: linear-gradient(110deg, var(--bg-elev), var(--bg-sunken) 70%, var(--bg-elev)); }
+.cw-hero .cw-avatar { width: 56px; height: 56px; font-size: 20px; flex-shrink: 0; }
.cw-hero-body, .cw-identity { flex: 1; min-width: 0; }
-.cw-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
-.cw-name { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 8px; }
-.cw-name h1 { margin: 0; font-size: 28px; line-height: 1.2; letter-spacing: -.7px; font-weight: 650; overflow-wrap: anywhere; }
-.cw-contact { display: flex; flex-wrap: wrap; gap: 9px 22px; color: var(--text-3); font-size: 12px; }
-.cw-contact > * { display: inline-flex; align-items: center; gap: 8px; min-width: 0; overflow-wrap: anywhere; }
+.cw-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
+.cw-name { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
+.cw-name h1 { margin: 0; font-size: 22px; line-height: 1.2; letter-spacing: -.4px; font-weight: 650; overflow-wrap: anywhere; }
+.cw-contact { display: flex; flex-wrap: wrap; gap: 6px 16px; color: var(--text-3); font-size: 12px; }
+.cw-contact > * { display: inline-flex; align-items: center; gap: 6px; min-width: 0; overflow-wrap: anywhere; }
.cw-contact a { color: var(--text-3); text-decoration: none; }
.cw-contact a:hover { color: var(--text); }
-.cw-contact .cw-external, .cw-info a { color: #9dd3f1; text-decoration: underline; text-underline-offset: 3px; }
-.cw-contact svg { width: 16px; height: 16px; flex-shrink: 0; }
-.cw-hero-actions { display: flex; gap: 10px; flex-shrink: 0; }
-.cw-facts { display: grid; grid-template-columns: 1.1fr 1fr .9fr 1.2fr .8fr 1.1fr .8fr; margin-top: 22px; }
-.cw-fact { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 0 16px; border-left: 1px solid var(--border); }
+.cw-contact .cw-external, .cw-info a { color: var(--info); text-decoration: underline; text-underline-offset: 3px; }
+.cw-contact svg { width: 14px; height: 14px; flex-shrink: 0; }
+.cw-hero-actions { display: flex; gap: 8px; flex-shrink: 0; }
+.cw-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 132px), 1fr)); gap: 12px 0; margin-top: 14px; }
+.cw-hero, .cw-overview, .cw-card, .cw-column { min-width: 0; max-width: 100%; }
+.cw-fact { display: flex; align-items: center; gap: 10px; min-width: 0; padding: 0 12px; border-left: 1px solid var(--border); }
.cw-fact:first-child { border-left: 0; padding-left: 0; }
-.cw-fact:last-child { padding-right: 0; }
-.cw-fact > svg { width: 20px; height: 20px; color: #c3dce4; flex-shrink: 0; }
-.cw-fact span { display: block; color: var(--text-3); font-size: 12px; line-height: 1.4; margin-bottom: 4px; }
-.cw-fact strong { font-size: 13px; font-weight: 500; line-height: 1.4; overflow-wrap: anywhere; }
-.cw-tabs .tabs { gap: 10px; margin-bottom: 16px; }
-.cw-tabs .tab { min-height: 55px; padding: 12px 20px; margin: 0; font-size: 13px; font-weight: 400; border-bottom-width: 3px; }
+.cw-fact > svg { width: 16px; height: 16px; color: var(--text-3); flex-shrink: 0; }
+.cw-fact span { display: block; color: var(--text-3); font-size: 11px; line-height: 1.3; margin-bottom: 2px; }
+.cw-fact strong { font-size: 12px; font-weight: 500; line-height: 1.35; overflow-wrap: anywhere; }
+
+.cw-tabs { min-width: 0; max-width: 100%; }
+.cw-tabs .tabs { gap: 4px; margin-bottom: 12px; min-width: 0; max-width: 100%; }
+.cw-tabs .tab { min-height: 42px; padding: 8px 14px; margin: 0; font-size: 12.5px; font-weight: 400; border-bottom-width: 2px; }
.cw-tabs .tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
-.cw-tabs .tab-count { background: #153941; color: #cbdee4; font-size: 11px; min-width: 18px; }
-.cw-overview { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.65fr) minmax(0, .99fr); gap: 16px; align-items: start; }
-.cw-column { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
-.cw-card { min-width: 0; padding: 18px 17px; border: 1px solid var(--border); border-radius: 12px; background: linear-gradient(120deg, #09232c, #061e26 90%); }
-.cw-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 16px; }
-.cw-card-head h2 { font-size: 15px; font-weight: 650; letter-spacing: -.2px; line-height: 1.35; margin: 0; }
-.cw-link { display: inline-flex; align-items: center; gap: 6px; color: #b4ed91; text-decoration: underline; text-underline-offset: 3px; font-size: 12px; white-space: nowrap; }
-.cw-link svg { width: 15px; height: 15px; }
-.cw-info { display: grid; gap: 15px; margin: 0; }
-.cw-info > div { display: grid; grid-template-columns: minmax(115px, .9fr) minmax(0, 1.4fr); gap: 12px; line-height: 1.4; font-size: 12px; }
-.cw-info dt { display: flex; align-items: flex-start; gap: 10px; color: var(--text-3); }
-.cw-info dt svg { width: 15px; height: 15px; margin-top: 1px; flex-shrink: 0; }
+.cw-tabs .tab-count { background: var(--bg-sunken); color: var(--text-2); font-size: 11px; min-width: 16px; }
+
+.cw-overview { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.65fr) minmax(0, .95fr); gap: 12px; align-items: start; }
+.cw-column { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
+.cw-card { min-width: 0; padding: 14px; border: 1px solid var(--border); border-radius: 10px; background: linear-gradient(120deg, var(--bg-elev), var(--bg-sunken) 90%); }
+.cw-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; }
+.cw-card-head h2 { font-size: 13px; font-weight: 650; letter-spacing: -.1px; line-height: 1.3; margin: 0; }
+.cw-link { display: inline-flex; align-items: center; gap: 6px; color: var(--primary); text-decoration: underline; text-underline-offset: 3px; font-size: 12px; white-space: nowrap; }
+.cw-link svg { width: 14px; height: 14px; }
+.cw-info { display: grid; gap: 10px; margin: 0; }
+.cw-info > div { display: grid; grid-template-columns: minmax(108px, .9fr) minmax(0, 1.4fr); gap: 8px; line-height: 1.4; font-size: 12px; }
+.cw-info dt { display: flex; align-items: flex-start; gap: 8px; color: var(--text-3); }
+.cw-info dt svg { width: 14px; height: 14px; margin-top: 1px; flex-shrink: 0; }
.cw-info dd { margin: 0; overflow-wrap: anywhere; }
-.cw-skills { display: flex; gap: 8px; flex-wrap: wrap; }
-.cw-skills > span { padding: 6px 10px; border: 1px solid #284b57; border-radius: 12px; background: #102d38; color: #e0edf3; font-size: 12px; max-width: 100%; overflow-wrap: anywhere; }
-.cw-table-wrap { overflow: auto; }
+.cw-skills { display: flex; gap: 6px; flex-wrap: wrap; }
+.cw-skills > span { padding: 4px 8px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-sunken); color: var(--text-2); font-size: 11px; max-width: 100%; overflow-wrap: anywhere; }
+.cw-table-wrap { overflow: auto; min-width: 0; max-width: 100%; }
.cw-applications { width: 100%; border-collapse: collapse; font-size: 12px; text-align: left; }
-.cw-applications th { color: #bad1dc; text-transform: uppercase; letter-spacing: .4px; font-size: 11px; font-weight: 500; border-top: 1px solid #15343d; border-bottom: 1px solid #15343d; padding: 9px 6px; white-space: nowrap; }
-.cw-applications td { padding: 13px 6px; border-bottom: 1px solid #15343d; }
+.cw-applications th { color: var(--text-3); text-transform: uppercase; letter-spacing: .4px; font-size: 10px; font-weight: 500; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: 7px 6px; white-space: nowrap; }
+.cw-applications td { padding: 10px 6px; border-bottom: 1px solid var(--border); }
.cw-applications td:first-child, .cw-applications th:first-child { padding-left: 0; }
.cw-applications td:last-child, .cw-applications th:last-child { padding-right: 0; }
.cw-applications tr:last-child td { border-bottom: 0; }
.cw-applications td:nth-child(2) { color: var(--text-2); white-space: nowrap; }
-.cw-applications strong { display: block; font-size: 13px; font-weight: 550; }
-.cw-applications small { display: block; color: var(--text-3); font-size: 11px; margin-top: 4px; }
+.cw-applications strong { display: block; font-size: 12px; font-weight: 550; }
+.cw-applications small { display: block; color: var(--text-3); font-size: 11px; margin-top: 3px; }
.cw-applications .badge { font-size: 11px; padding: 3px 7px; }
-.cw-applications .is-current { background: linear-gradient(90deg, #12353438, transparent); }
-.cw-summary { margin: 0; color: var(--text-2); font-size: 13px; line-height: 1.8; white-space: pre-line; overflow-wrap: anywhere; }
-.cw-summary p + p { margin-top: 10px; }
-.cw-empty { color: var(--text-3); font-size: 13px; line-height: 1.7; margin: 0; }
-.cw-document { display: flex; align-items: center; gap: 11px; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: linear-gradient(100deg, #0d2c36, #0a232b); min-width: 0; }
-.cw-document-icon { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 29px; height: 36px; background: linear-gradient(135deg, #ff7575, #df424d); border-radius: 4px; color: #fff; flex-shrink: 0; }
-.cw-document-icon svg { width: 16px; height: 16px; }
+.cw-applications .is-current { background: linear-gradient(90deg, var(--primary-soft), transparent); }
+.cw-summary { margin: 0; color: var(--text-2); font-size: 12.5px; line-height: 1.65; white-space: pre-line; overflow-wrap: anywhere; }
+.cw-summary p + p { margin-top: 8px; }
+.cw-empty { color: var(--text-3); font-size: 12.5px; line-height: 1.6; margin: 0; }
+.cw-document { display: flex; align-items: center; gap: 10px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: linear-gradient(100deg, var(--bg-elev), var(--bg-sunken)); min-width: 0; }
+.cw-document-icon { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 26px; height: 32px; background: linear-gradient(135deg, #ff7575, #df424d); border-radius: 4px; color: #fff; flex-shrink: 0; }
+.cw-document-icon svg { width: 14px; height: 14px; }
.cw-document-icon small { font-size: 7px; line-height: 1.2; margin-top: 2px; }
.cw-document-name { flex: 1; min-width: 0; }
.cw-document-name strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 500; }
-.cw-document-name small { display: block; color: var(--text-3); font-size: 11px; margin-top: 4px; }
+.cw-document-name small { display: block; color: var(--text-3); font-size: 11px; margin-top: 2px; }
.cw-document-actions { display: flex; gap: 6px; flex-shrink: 0; }
-.cw-document-list { display: grid; gap: 9px; }
-.candidate-workspace .cw-icon-label { width: 30px; font-size: 0; gap: 0; }
-.cw-bottom-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 14px; }
+.cw-document-list { display: grid; gap: 8px; }
+.candidate-workspace .cw-icon-label { width: 28px; font-size: 0; gap: 0; }
+.cw-bottom-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 12px; }
.cw-bottom-grid > .cw-card { align-self: start; }
-.cw-bottom-grid .cw-card { padding: 17px; }
-.cw-rating { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
+.cw-rating { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cw-rating > span { font-size: 12px; color: var(--primary); }
-.cw-rating .rating-stars .rs svg { width: 18px; height: 18px; }
+.cw-rating .rating-stars .rs svg { width: 16px; height: 16px; }
.cw-recruiter { display: flex; align-items: center; gap: 10px; }
-.cw-recruiter .avatar { width: 32px; height: 32px; font-size: 12px; }
+.cw-recruiter .avatar { width: 28px; height: 28px; font-size: 11px; }
.cw-recruiter strong { display: block; font-size: 12px; font-weight: 500; }
-.cw-recruiter small { display: block; color: var(--text-3); font-size: 12px; margin-top: 3px; }
-.cw-action-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 9px; }
-.cw-action-grid .btn { font-size: 12px; justify-content: flex-start; padding: 8px; white-space: normal; text-align: left; }
-.candidate-workspace .cw-danger, .candidate-dialog .cw-danger { border: 1px solid #ae4a55; color: #ff7c86; background: #2a172055; }
-.candidate-workspace .cw-danger:hover:not(:disabled), .candidate-dialog .cw-danger:hover:not(:disabled) { background: #50232b; }
-.cw-active-application { font-size: 12px; color: var(--text-3); margin: -4px 0 12px; }
-.cw-status-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 10px; }
-.cw-field-label { display: block; color: var(--text-3); font-size: 12px; margin-bottom: 5px; }
-.cw-status-grid select, .cw-status-value { width: 100%; min-height: 37px; padding: 8px 10px; background: #0c2933; color: var(--text); border: 1px solid var(--border); border-radius: 7px; font-size: 12px; }
+.cw-recruiter small { display: block; color: var(--text-3); font-size: 11px; margin-top: 2px; }
+.cw-action-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
+.cw-action-grid .btn { font-size: 12px; justify-content: flex-start; padding: 7px 8px; white-space: normal; text-align: left; }
+.candidate-workspace .cw-danger, .candidate-dialog .cw-danger { border: 1px solid var(--danger); color: var(--danger); background: var(--danger-soft); }
+.candidate-workspace .cw-danger:hover:not(:disabled), .candidate-dialog .cw-danger:hover:not(:disabled) { background: var(--danger); color: var(--danger-fg); }
+.cw-active-application { font-size: 12px; color: var(--text-3); margin: -2px 0 10px; }
+.cw-status-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 8px; }
+.cw-field-label { display: block; color: var(--text-3); font-size: 11px; margin-bottom: 4px; }
+.cw-status-grid select, .cw-status-value { width: 100%; min-height: 34px; padding: 6px 8px; background: var(--bg-sunken); color: var(--text); border: 1px solid var(--border); border-radius: 7px; font-size: 12px; }
.cw-status-value { display: flex; align-items: center; gap: 8px; }
.cw-status-dot { width: 7px; height: 7px; background: var(--success); border-radius: 50%; flex-shrink: 0; }
.cw-status-dot.is-closed { background: var(--text-3); }
.cw-activity { list-style: none; margin: 0; padding: 0; }
-.cw-activity li { position: relative; padding: 0 0 23px 24px; }
+.cw-activity li { position: relative; padding: 0 0 16px 22px; }
.cw-activity li:last-child { padding-bottom: 0; }
-.cw-activity li::before { content: ''; position: absolute; left: 0; top: 4px; width: 10px; height: 10px; background: #59a8ff; border: 2px solid #245788; border-radius: 50%; z-index: 1; }
-.cw-activity li:not(:last-child)::after { content: ''; position: absolute; width: 1px; left: 4px; top: 15px; bottom: 3px; background: #315662; }
+.cw-activity li::before { content: ''; position: absolute; left: 0; top: 4px; width: 9px; height: 9px; background: var(--info); border: 2px solid var(--bg-elev); border-radius: 50%; z-index: 1; }
+.cw-activity li:not(:last-child)::after { content: ''; position: absolute; width: 1px; left: 4px; top: 14px; bottom: 2px; background: var(--border); }
.cw-activity-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.cw-activity strong { font-size: 12px; font-weight: 550; }
.cw-activity time { color: var(--text-3); font-size: 11px; white-space: nowrap; }
-.cw-activity p { color: var(--text-3); font-size: 12px; line-height: 1.65; margin: 5px 0 0; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
+.cw-activity p { color: var(--text-3); font-size: 12px; line-height: 1.55; margin: 4px 0 0; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.cw-activity small { color: var(--text-3); font-size: 11px; }
-.cw-screening { display: flex; align-items: center; gap: 18px; }
-.cw-match { display: grid; justify-items: center; gap: 6px; flex-shrink: 0; }
+.cw-screening { display: flex; align-items: center; gap: 14px; }
+.cw-match { display: grid; justify-items: center; gap: 4px; flex-shrink: 0; }
.cw-match small { color: var(--text-3); font-size: 12px; }
-.cw-tab-content { padding: 22px; background: var(--bg-elev); border: 1px solid var(--border); border-radius: 12px; }
+.cw-tab-content { padding: 16px; background: var(--bg-elev); border: 1px solid var(--border); border-radius: 10px; }
.candidate-dialog { border: 1px solid var(--border); width: min(620px, calc(100vw - 24px)); }
.candidate-dialog .form-field { margin-top: 16px; }
.candidate-dialog .empty-state { padding: 20px; }
.cw-dialog-actions { margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end; }
.cw-muted { color: var(--text-3); }
-@media (max-width: 1279px) {
- .candidate-workspace .content { padding-inline: 24px; }
- .candidate-workspace .topbar { padding-inline: 24px; }
- .candidate-brand { min-width: 200px; }
- .cw-overview { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); }
+/* Sidebar is 262px, so these fire at the same content width as a full-bleed 1279/900. */
+@media (max-width: 1560px) {
+ .candidate-workspace .content { padding-inline: 18px; }
+ .candidate-workspace .topbar { padding-inline: 18px; }
+ .cw-overview { grid-template-columns: minmax(0, 1fr) minmax(0, 1.55fr); }
.cw-column-info { grid-column: 1; grid-row: 1; }
.cw-column-main { grid-column: 2; grid-row: 1 / span 2; }
.cw-column-actions { grid-column: 1; grid-row: 2; }
.cw-hero-top { flex-wrap: wrap; }
- .cw-facts { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: 18px; }
- .cw-fact:nth-child(5) { border-left: 0; padding-left: 0; }
+ .cw-hero-actions { margin-left: auto; }
.cw-bottom-grid { grid-template-columns: minmax(0, 1fr); }
.cw-bottom-grid > .cw-column { display: grid; grid-template-columns: 1fr 1fr; }
- .cw-info > div { grid-template-columns: minmax(105px, .9fr) minmax(0, 1.4fr); }
+}
+@media (max-width: 1100px) {
+ .cw-overview { display: flex; flex-direction: column; }
+ .cw-overview > * { width: 100%; min-width: 0; }
+ .cw-column-actions { order: 0; }
+ .cw-hero { flex-wrap: wrap; }
+ .cw-hero-actions { width: 100%; flex-wrap: wrap; }
+ .cand-page-actions .star-btn { width: 32px; padding: 5px; font-size: 0; gap: 0; }
+ .cand-page-actions .star-btn svg { width: 15px; height: 15px; }
}
@media (max-width: 767px) {
- .candidate-workspace .content { padding: 0 16px 28px; }
- .candidate-workspace .topbar { padding: 12px 16px; gap: 10px; flex-wrap: wrap; }
- .candidate-brand { min-width: 0; flex: 1; gap: 9px; }
- .candidate-brand strong { font-size: 16px; }
- .candidate-brand .brand-mark { width: 30px; }
+ .candidate-workspace .content { padding: 0 12px 20px; }
+ .candidate-workspace .topbar { padding: 10px 12px; gap: 8px; flex-wrap: wrap; }
.candidate-workspace .topbar-search { order: 4; max-width: none; flex-basis: 100%; }
.candidate-workspace .topbar-actions { margin-left: 0; }
.candidate-workspace .topbar-divider, .candidate-workspace .profile-meta, .candidate-workspace .profile-btn .chev { display: none; }
- .candidate-workspace .cand-page-bar { gap: 10px; padding-block: 12px; }
- .candidate-workspace .cand-page-crumb { flex: 1; }
- .cw-hero { padding: 18px 16px; gap: 14px; flex-wrap: wrap; }
- .cw-hero .cw-avatar { width: 54px; height: 54px; font-size: 22px; }
- .cw-hero-body { display: contents; }
- .cw-hero-top { flex: 1; min-width: 0; }
- .cw-identity { flex-basis: 100%; }
- .cw-name { gap: 8px; }
- .cw-name h1 { font-size: 24px; }
- .cw-hero-actions { flex-wrap: wrap; flex-shrink: 1; max-width: 100%; }
- .cw-facts { width: 100%; margin-top: 6px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
- .cw-fact { padding: 0 12px; gap: 9px; }
- .cw-fact:nth-child(odd) { border-left: 0; padding-left: 0; }
+ .candidate-workspace .cand-page-bar { gap: 8px; padding-block: 6px; flex-wrap: wrap; }
+ .candidate-workspace .cand-page-crumb { flex: 1; min-width: 0; }
+ .candidate-workspace .cand-page-crumb .cw-crumb-path { display: none; }
+ .candidate-workspace .cand-page-actions { width: 100%; margin-left: 0; justify-content: flex-end; flex-wrap: nowrap; }
+ .candidate-workspace .cand-page-actions .btn { flex: 0 0 auto; }
+ .candidate-workspace .cand-page-crumb .btn { flex-shrink: 0; }
+ .cw-browse-nav { gap: 2px; }
+ .cw-browse-arrow { width: 30px; min-height: 30px; padding: 4px; }
+ .cw-browse-counter { min-width: 44px; font-size: 11px; }
+ .cw-hero { padding: 12px; gap: 12px; }
+ .cw-hero .cw-avatar { width: 44px; height: 44px; font-size: 16px; }
+ .cw-name h1 { font-size: 18px; }
+ .cw-hero-actions .btn { flex: 1 1 auto; justify-content: center; }
+ .cw-facts { margin-top: 8px; }
+ .cw-fact { border-left: 0; padding: 8px 0 0; }
.cw-tabs .tabs { gap: 0; }
- .cw-tabs .tab { padding-inline: 16px; }
- .cw-overview { display: flex; flex-direction: column; }
- .cw-overview > * { width: 100%; }
- .cw-column-actions { order: -1; }
- .cw-action-grid .btn { min-height: 44px; }
- .cw-info > div { grid-template-columns: 135px minmax(0, 1fr); font-size: 13px; }
- .cw-applications { min-width: 470px; }
+ .cw-tabs .tab { padding-inline: 12px; min-height: 40px; }
+ .cw-action-grid .btn { min-height: 40px; }
+ .cw-info > div { grid-template-columns: 120px minmax(0, 1fr); }
+ .cw-applications { min-width: 360px; }
.cw-document { flex-wrap: wrap; }
.cw-document-name { min-width: 100px; }
.cw-document-actions { margin-left: auto; }
.cw-bottom-grid > .cw-column { display: flex; }
- .cw-tab-content { padding: 16px; }
+ .cw-tab-content { padding: 12px; }
.cw-screening { align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {