From 80eb5b3bbc8be9f516dc3f75a07feff15b2b6ee8 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Fri, 11 Sep 2026 15:32:59 +0500 Subject: [PATCH] Keep the app sidebar on candidate profiles and add compact Previous/Next browsing. The profile was forcing its own chrome and a candidate list rail; recruiters now stay in the normal shell and step through the list they opened, with a denser layout that still fits a phone. Co-authored-by: Cursor --- backend/job/candidate/views.py | 4 +- frontend/candidate-browse.test.mjs | 42 +++ frontend/candidate-profile.test.mjs | 25 +- frontend/package.json | 3 +- frontend/src/app/AppLayout.jsx | 12 +- frontend/src/app/GlobalSearch.jsx | 3 +- frontend/src/app/Topbar.jsx | 11 +- frontend/src/lib/candidateBrowse.js | 64 +++++ frontend/src/lib/queryKeys.js | 1 + frontend/src/screens/CandidateBrowse.jsx | 31 +++ frontend/src/screens/CandidatePage.jsx | 70 ++++- frontend/src/screens/CandidateProfile.jsx | 33 ++- frontend/src/screens/Candidates.jsx | 21 +- frontend/src/screens/CvBank.jsx | 3 +- frontend/src/screens/Pipeline.jsx | 3 +- frontend/src/styles/candidate-workspace.css | 286 +++++++++----------- 16 files changed, 406 insertions(+), 206 deletions(-) create mode 100644 frontend/candidate-browse.test.mjs create mode 100644 frontend/src/lib/candidateBrowse.js create mode 100644 frontend/src/screens/CandidateBrowse.jsx 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(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 BrandsHR 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 ( +
+ + {index + 1} of {total} + +
+ ) +} 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') ? : null} @@ -642,15 +652,16 @@ export default function CandidateProfile({ return (
-
- Candidates / {live?.name || c.name || '…'} + + Candidates / {live?.name || c.name || '…'} +
+
+ {!isManager && } + {onBrowse && }
- {!isManager &&
- -
}
{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) {