From cec3be73fb47df78aa34e97eda1e028c6dc80233 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Tue, 25 Aug 2026 19:46:34 +0500 Subject: [PATCH 01/22] Title tier of relevance_score accepts token containment Live case: the job titled "Generative Engineer" sourced a pool titled "Generative AI Engineer" -- the exact phrase never occurs in that title, so every genuine match fell through to the scattered 35 tier and the whole pool compressed into the 40s. A current title containing every job-title token now earns the full 55. The headline tier stays phrase-only so keyword-stuffed headlines still cap at the 35 tier. Co-Authored-By: Claude Fable 5 --- backend/talent/plugins.py | 22 ++++++++++++++++------ backend/tests/test_talent_plugins.py | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/backend/talent/plugins.py b/backend/talent/plugins.py index 2bca488..a0409fc 100644 --- a/backend/talent/plugins.py +++ b/backend/talent/plugins.py @@ -418,11 +418,17 @@ def _match_tokens(*texts) -> set[str]: def relevance_score(job: dict, profile: dict) -> int: """0-100 job-fit rank for sorting, computed when a profile is persisted. - Deterministic and free. Title component: the job title as an exact PHRASE - in the person's current title scores 55, in their headline 45; scattered + Deterministic and free. Title component: a current title CONTAINING every + job-title token scores 55 — containment, not exact phrase, because job + titles rarely reappear verbatim ("Generative Engineer" vs the pool's + "Generative AI Engineer"; seen live: the phrase rule dropped every real + match to the scattered tier and compressed the whole pool into the 40s). + The job title as an exact phrase in the headline scores 45; scattered token overlap caps at 35 — a keyword-stuffed headline ("AI/ML Engineer | Python | FastAPI | ...") must not outrank someone whose title IS the job - title, which is exactly what token overlap alone did on live data. + title, which is exactly what token overlap alone did on live data. The + headline tier stays phrase-only for the same reason: stuffed headlines + contain every token of every hot title. Skills component (up to 45): GRADED token overlap between the content words of the job's requirements + optional skills and the person's @@ -432,16 +438,20 @@ def relevance_score(job: dict, profile: dict) -> int: whole live pool on exactly 60. """ job_title = _clean_phrase(job.get("title")) + job_title_tokens = set(job_title.split()) title_text = _clean_phrase(profile.get("current_title")) headline_text = _clean_phrase(profile.get("headline")) - if job_title and job_title in title_text: + if job_title and job_title_tokens <= set(title_text.split()): title_component = 55.0 elif job_title and job_title in headline_text: title_component = 45.0 else: - title_tokens = set(job_title.split()) role_tokens = set(title_text.split()) | set(headline_text.split()) - ratio = len(title_tokens & role_tokens) / len(title_tokens) if title_tokens else 0.0 + ratio = ( + len(job_title_tokens & role_tokens) / len(job_title_tokens) + if job_title_tokens + else 0.0 + ) title_component = 35 * ratio job_tokens = _match_tokens( diff --git a/backend/tests/test_talent_plugins.py b/backend/tests/test_talent_plugins.py index 4aeef2a..e16c6d1 100644 --- a/backend/tests/test_talent_plugins.py +++ b/backend/tests/test_talent_plugins.py @@ -404,7 +404,25 @@ def test_headline_phrase_scores_below_title_phrase(): scattered = plugins.relevance_score(job, {"current_title": "Engineer", "headline": "Agentic AI | Python"}) assert in_title == 55 assert in_headline == 45 - assert scattered == 35 # both tokens present but never as the phrase + assert scattered == 35 # tokens split across title and headline never combine + + +def test_title_containment_scores_like_an_exact_title(): + # Live case: job "Generative Engineer", pool titled "Generative AI + # Engineer" — the exact phrase never occurs, so every genuine match fell + # to the scattered 35 tier and the whole pool compressed into the 40s. + job = {"title": "Generative Engineer", "requirements": [], "optional_skills": []} + interleaved = plugins.relevance_score(job, {"current_title": "Generative AI Engineer"}) + senior = plugins.relevance_score(job, {"current_title": "Senior Generative AI Engineer"}) + assert interleaved == 55 + assert senior == 55 + # Containment applies to the TITLE only: the same tokens scattered across + # a keyword-stuffed headline still cap at the 35 tier. + stuffed = plugins.relevance_score( + job, + {"current_title": "Developer", "headline": "Generative AI | Engineer | Python"}, + ) + assert stuffed == 35 # ---------------------------------------------------------------- detail extraction From 4ad964e1ffa11eacb04a65d3976eb34cb1ed3c16 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Tue, 25 Aug 2026 19:49:08 +0500 Subject: [PATCH 02/22] Talent filter uses the standard toolbar-search box className="input" matches no CSS rule, so the filter fell back to the browser default ~180px width and clipped its own placeholder ("Filter by name, headline, com"). Swap it for the toolbar-search pattern every other screen uses: search icon, full-width input, 340px cap. Co-Authored-By: Claude Fable 5 --- frontend/dist/index.html | 2 +- frontend/src/screens/Talent.jsx | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/dist/index.html b/frontend/dist/index.html index df37875..72268d5 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -23,7 +23,7 @@ - + diff --git a/frontend/src/screens/Talent.jsx b/frontend/src/screens/Talent.jsx index bc27470..5a51b45 100644 --- a/frontend/src/screens/Talent.jsx +++ b/frontend/src/screens/Talent.jsx @@ -543,13 +543,14 @@ export default function Talent() { ) : ( <>
- setSearch(e.target.value)} - /> +
+ + setSearch(e.target.value)} + /> +
{visible.length} of {profiles.length} profile{profiles.length === 1 ? '' : 's'} From bc5856005db6d79b62d6f6c3e13cf10df83984b4 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Tue, 25 Aug 2026 21:37:14 +0500 Subject: [PATCH 03/22] ui-polish 1/6: typography+spacing tokens, base headings, utilities - Type scale (--fs-2xs..--fs-3xl) and line-height tokens; the 20-size census collapses onto 9 steps. Spacing scale (--space-1..8) plus --gap keeping the signature 18px rhythm. Colors untouched. - Base h1-h6 rules: bare headings used to render at UA defaults, larger and bolder than the styled card titles beside them. - One eyebrow-label rule replacing eleven near-identical declarations at five sizes and five letter-spacings. - High-traffic families retrofitted to tokens (page/card/table/form/btn/ tabs/badge/sidebar/kpi); rare bolt-on sections left alone. - New utilities: truncate, clamp-2, justify-between, flex-wrap, flex-col, flex-1, min-w-0, w-full, mt/mb-8/12/16/24, cell-clip, row-click. - Defined phantom classes shipped in JSX (.dt, .route-loading, .tab-count) and a .skip-link. - Email/role cells clip with ellipsis on all viewports, not only <640px. - Content measure capped at 1440px on every desktop, not only >=1600. - Sidebar: clearer group separation, active item weight 600. - main.jsx design-contract comment amended: colors stay frozen, typography/spacing re-tokenized. Co-Authored-By: Claude Fable 5 --- frontend/src/main.jsx | 7 +- frontend/src/styles/styles.css | 241 ++++++++++++++++++++++----------- 2 files changed, 165 insertions(+), 83 deletions(-) diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index dbc7e4d..211f174 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,9 +3,10 @@ import { createRoot } from 'react-dom/client' import { QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -// The frozen design contract (ADR 0013 §1): 1,269 lines, 93 tokens, dual -// themes, WCAG 2.1 AA verified across 23 routes. Content-frozen — new CSS may -// only use existing var(--…) tokens. +// Design contract (ADR 0013 §1), amended 2026-08-25: the COLOR palette and +// color tokens remain frozen. Typography and spacing were re-tokenized in the +// ui-polish pass (--fs-*/--lh-*/--space-*/--gap); new CSS must use var(--…) +// tokens only — no raw font sizes, spacing, or colors. import './styles/styles.css' import './styles/auth.css' diff --git a/frontend/src/styles/styles.css b/frontend/src/styles/styles.css index 76b59c2..d59ebeb 100644 --- a/frontend/src/styles/styles.css +++ b/frontend/src/styles/styles.css @@ -87,6 +87,29 @@ --font-display: 'Belleza', 'Neue Montreal', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --mono: 'SF Mono', ui-monospace, 'Cascadia Code', Menlo, monospace; + /* Type scale — 9 steps that the 20-size census collapses onto. + Theme-invariant; declared once here, never redefined in dark. */ + --fs-2xs: 11px; /* eyebrow labels, micro-meta */ + --fs-xs: 12px; /* badges, chips, cell-sub */ + --fs-sm: 13px; /* secondary body, form labels, small buttons */ + --fs-base: 14px; /* body, tables, buttons, nav */ + --fs-md: 15px; /* card titles */ + --fs-lg: 18px; /* section headings, empty states */ + --fs-xl: 22px; /* modal titles */ + --fs-2xl: 25px; /* profile hero name, mobile page title */ + --fs-3xl: 30px; /* page title (Belleza) */ + + --lh-display: 1.15; /* Belleza display headings */ + --lh-heading: 1.25; /* h2–h6, card titles */ + --lh-body: 1.5; /* running copy */ + --lh-label: 1.35; /* eyebrows, form labels, table headers */ + + /* Spacing scale. --gap is the app's signature 18px stack rhythm + (grid gap, mt/mb-18, card-head padding) — a sanctioned step, kept. */ + --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; + --space-5: 20px; --space-6: 24px; --space-7: 32px; --space-8: 40px; + --gap: 18px; + /* Categorical chart series — brand hues darkened to hold >=3:1 against white so lines and small marks stay legible. */ --c1: #004d43; --c2: #0f9d76; --c3: #5b60e8; --c4: #6f8f14; @@ -180,8 +203,8 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); - font-size: 14px; - line-height: 1.5; + font-size: var(--fs-base); + line-height: var(--lh-body); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; /* Grey flash on tap (iOS/Android WebKit + Chromium); we provide our own @@ -195,7 +218,7 @@ button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; touch-action: manipulation; /* removes the legacy 300ms tap delay */ } -input, select, textarea { font-family: inherit; font-size: 14px; color: var(--text); } +input, select, textarea { font-family: inherit; font-size: var(--fs-base); color: var(--text); } a { color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent; } img, svg, video, canvas { max-width: 100%; } @@ -254,10 +277,11 @@ img, svg, video, canvas { max-width: 100%; } font-weight: 400; letter-spacing: 0; } -.page-title { font-size: 30px; line-height: 1.15; } -.modal-head h2 { font-size: 22px; } -.ph-name { font-size: 25px; } -.brand-name { font-size: 18px; font-weight: 400; letter-spacing: .2px; } +.page-title { font-size: var(--fs-3xl); line-height: var(--lh-display); } +.page-title-sm { font-size: var(--fs-xl); } /* embedded sub-page titles (h2) */ +.modal-head h2 { font-size: var(--fs-xl); } +.ph-name { font-size: var(--fs-2xl); } +.brand-name { font-size: var(--fs-lg); font-weight: 400; letter-spacing: .2px; } /* Metrics read as data, not prose — keep them in the UI face, tabular-lining so digits don't jitter as values update. */ @@ -266,6 +290,28 @@ table.data td, .k-count, .nav-badge { font-variant-numeric: tabular-nums; } +/* Base heading scale. Any heading a class doesn't reach lands on a sane + step instead of the UA default (which rendered bare h3/h4 LARGER and + bolder than the styled card titles beside them). Component rules + (.page-title, .modal-head h2, .card-head h3, …) win by specificity. */ +h1, h2, h3, h4, h5, h6 { line-height: var(--lh-heading); } +h1 { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-3xl); line-height: var(--lh-display); } +h2 { font-size: var(--fs-xl); font-weight: 600; } +h3 { font-size: var(--fs-md); font-weight: 600; } +h4 { font-size: var(--fs-base); font-weight: 600; } +h5, h6 { font-size: var(--fs-sm); font-weight: 600; } + +/* Eyebrow labels — one concept, one rule. Previously eleven near-identical + declarations at five sizes and five letter-spacings. Selectors keep only + their unique bits (padding, borders, sticky) at their own definitions. */ +.nav-section-label, .search-group-label, .form-section-title, +table.data thead th, .rbac-matrix th, .cal-dow, .info-item .il, +.hf-tile .hf-k, .hf-block-title, .hf-sign .hf-sign-role, +.hf-rate-head > div, .hf-rate-foot > div:first-child { + font-size: var(--fs-2xs); font-weight: 700; text-transform: uppercase; + letter-spacing: .6px; line-height: var(--lh-label); color: var(--text-3); +} + /* ============================================================ FOCUS & MOTION (accessibility) ============================================================ */ @@ -335,25 +381,26 @@ table.data td, .k-count, .nav-badge { .sidebar-nav::-webkit-scrollbar { width: 6px; } .sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); } .nav-section-label { - font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .9px; - color: var(--text-3); padding: 14px 12px 6px; + letter-spacing: .8px; /* brand moment — wider than the base eyebrow */ + padding: 20px 12px 6px; /* clear air between nav groups */ } +.sidebar-nav > div:first-child .nav-section-label { padding-top: 4px; } .nav-item { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border-radius: 9px; margin-bottom: 2px; - color: var(--sidebar-fg); font-weight: 500; font-size: 13.5px; + color: var(--sidebar-fg); font-weight: 500; font-size: var(--fs-base); transition: background .14s, color .14s; position: relative; white-space: nowrap; } .nav-item svg { width: 19px; height: 19px; flex-shrink: 0; stroke-width: 1.9; } .nav-item:hover { background: rgba(255,255,255,.05); color: #fff; } -.nav-item.active { background: var(--sidebar-active-bg); color: #fff; } +.nav-item.active { background: var(--sidebar-active-bg); color: #fff; font-weight: 600; } .nav-item.active::before { content: ''; position: absolute; left: -12px; top: 50%; transform: translateY(-50%); width: 3px; height: 20px; background: var(--sidebar-rail); border-radius: 0 3px 3px 0; } .nav-badge { margin-left: auto; background: rgba(255,255,255,.12); color: #fff; - font-size: 11px; font-weight: 600; padding: 1px 8px; border-radius: 20px; min-width: 22px; text-align: center; + font-size: var(--fs-xs); font-weight: 600; padding: 1px 8px; border-radius: 20px; min-width: 22px; text-align: center; } .nav-badge-alert { background: var(--danger); color: var(--danger-fg); } .nav-badge-ai { background: var(--brand-lime); color: var(--brand-ink); font-weight: 700; letter-spacing: .3px; } @@ -412,7 +459,7 @@ table.data td, .k-count, .nav-badge { box-shadow: var(--shadow-lg); max-height: 420px; overflow-y: auto; display: none; z-index: 80; padding: 6px; } .search-results.open { display: block; } -.search-group-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); padding: 8px 10px 4px; } +.search-group-label { padding: 8px 10px 4px; } .search-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer; } .search-item:hover { background: var(--bg-sunken); } .search-item .si-title { font-weight: 600; font-size: 13px; } @@ -458,7 +505,7 @@ table.data td, .k-count, .nav-badge { .dp-name { font-weight: 600; } .dp-email { font-size: 12px; color: var(--text-3); } .dropdown-divider { height: 1px; background: var(--border); margin: 6px 0; } -.dropdown-link { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px; font-size: 13.5px; font-weight: 500; width: 100%; text-align: left; color: var(--text); } +.dropdown-link { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px; font-size: var(--fs-base); font-weight: 500; width: 100%; text-align: left; color: var(--text); } .dropdown-link svg { width: 17px; height: 17px; color: var(--text-3); } .dropdown-link:hover { background: var(--bg-sunken); } .dropdown-link.danger { color: var(--danger); } @@ -471,8 +518,8 @@ table.data td, .k-count, .nav-badge { .notif-icn { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; flex-shrink: 0; } .notif-icn svg { width: 16px; height: 16px; } .notif-body { flex: 1; min-width: 0; } -.notif-title { font-size: 13px; font-weight: 600; } -.notif-text { font-size: 12.5px; color: var(--text-2); } +.notif-title { font-size: var(--fs-sm); font-weight: 600; } +.notif-text { font-size: var(--fs-sm); color: var(--text-2); } .notif-time { font-size: 11px; color: var(--text-3); margin-top: 3px; } .dd-scroll { max-height: 360px; overflow-y: auto; overscroll-behavior: contain; } @@ -480,16 +527,17 @@ table.data td, .k-count, .nav-badge { .content { flex: 1; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 26px 30px 60px; } .page { animation: fadeUp .3s ease; } @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } -.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 24px; flex-wrap: wrap; } -.page-sub { color: var(--text-2); font-size: 14px; margin-top: 3px; } +.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-5); margin-bottom: var(--space-6); flex-wrap: wrap; } +.page-head-main { min-width: 0; } +.page-sub { color: var(--text-2); font-size: var(--fs-base); line-height: var(--lh-body); margin-top: 3px; max-width: 65ch; } .page-head-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } -.breadcrumb { display: flex; gap: 8px; align-items: center; font-size: 12.5px; color: var(--text-3); margin-bottom: 10px; } +.breadcrumb { display: flex; gap: 8px; align-items: center; font-size: var(--fs-sm); color: var(--text-3); margin-bottom: 10px; } .breadcrumb svg { width: 14px; height: 14px; } /* ================= BUTTONS ================= */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; - padding: 9px 16px; border-radius: 10px; font-weight: 600; font-size: 13.5px; + padding: 9px 16px; border-radius: var(--radius); font-weight: 600; font-size: var(--fs-base); transition: .15s; white-space: nowrap; border: 1px solid transparent; } .btn svg { width: 17px; height: 17px; } @@ -502,26 +550,26 @@ table.data td, .k-count, .nav-badge { .btn-danger { background: var(--danger); color: var(--danger-fg); } .btn-danger:hover { filter: brightness(.94); } .btn-block { width: 100%; margin-top: 12px; } -.btn-sm { padding: 6px 12px; font-size: 12.5px; } +.btn-sm { padding: 6px 12px; font-size: var(--fs-sm); } .btn-icon { padding: 8px; width: 34px; height: 34px; } .btn:disabled { opacity: .5; cursor: not-allowed; } /* ================= CARDS ================= */ .card { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); } -.card-pad { padding: 20px; } -.card-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid var(--border); gap: 12px; } -.card-head h3 { font-size: 15px; font-weight: 700; letter-spacing: -.2px; } -.card-head .ch-sub { font-size: 12.5px; color: var(--text-3); font-weight: 400; } -.card-body { padding: 20px; } +.card-pad { padding: var(--space-5); } +.card-head { display: flex; align-items: center; justify-content: space-between; padding: var(--gap) var(--space-5); border-bottom: 1px solid var(--border); gap: var(--space-3); } +.card-head h3 { font-size: var(--fs-md); font-weight: 600; letter-spacing: -.2px; } +.card-head .ch-sub { font-size: var(--fs-sm); color: var(--text-3); font-weight: 400; } +.card-body { padding: var(--space-5); } -.grid { display: grid; gap: 18px; } +.grid { display: grid; gap: var(--gap); } .g-kpi { grid-template-columns: repeat(4, 1fr); } .g-3 { grid-template-columns: repeat(3, 1fr); } .g-2 { grid-template-columns: repeat(2, 1fr); } .g-2-1 { grid-template-columns: 2fr 1fr; } .g-1-2 { grid-template-columns: 1fr 2fr; } -.mt-18 { margin-top: 18px; } -.mb-18 { margin-bottom: 18px; } +.mt-18 { margin-top: var(--gap); } +.mb-18 { margin-bottom: var(--gap); } /* KPI card */ .kpi { @@ -530,12 +578,12 @@ table.data td, .k-count, .nav-badge { } .kpi:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); } .kpi-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } -.kpi-label { font-size: 12.5px; color: var(--text-2); font-weight: 500; } +.kpi-label { font-size: var(--fs-sm); color: var(--text-2); font-weight: 500; } .kpi-icn { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; } .kpi-icn svg { width: 20px; height: 20px; } .kpi-value { font-size: 28px; font-weight: 700; letter-spacing: -1px; line-height: 1; } -.kpi-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: 12.5px; } -.trend { display: inline-flex; align-items: center; gap: 3px; font-weight: 600; padding: 2px 7px; border-radius: 6px; font-size: 12px; } +.kpi-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; font-size: var(--fs-sm); } +.trend { display: inline-flex; align-items: center; gap: 3px; font-weight: 600; padding: 2px 7px; border-radius: var(--radius-sm); font-size: var(--fs-xs); } .trend svg { width: 13px; height: 13px; } .trend-up { color: var(--success); background: var(--success-soft); } .trend-down { color: var(--danger); background: var(--danger-soft); } @@ -550,7 +598,7 @@ table.data td, .k-count, .nav-badge { .i-teal { background: var(--teal-soft); color: var(--teal); } /* ================= BADGES ================= */ -.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; } +.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; } .badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } .badge-plain::before { display: none; } .b-green { color: var(--success); background: var(--success-soft); } @@ -566,24 +614,26 @@ table.data td, .k-count, .nav-badge { /* Horizontal scroll is the right answer for wide data tables on phones; make the gesture smooth and keep it from chaining to the page. */ .table-wrap { overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; } -table.data { width: 100%; border-collapse: collapse; font-size: 13.5px; } +table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-base); } table.data thead th { - text-align: left; padding: 12px 16px; font-size: 11.5px; font-weight: 700; - text-transform: uppercase; letter-spacing: .5px; color: var(--text-3); + text-align: left; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); white-space: nowrap; background: var(--bg-elev); position: sticky; top: 0; } table.data thead th.sortable { cursor: pointer; user-select: none; } table.data thead th.sortable:hover { color: var(--text); } .sort-ind { display: inline-block; margin-left: 4px; opacity: .4; font-size: 10px; } th.sorted-asc .sort-ind, th.sorted-desc .sort-ind { opacity: 1; color: var(--primary); } -table.data tbody td { padding: 13px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; } +table.data tbody td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); vertical-align: middle; } table.data tbody tr { transition: background .12s; } table.data tbody tr:hover { background: var(--bg-sunken); } table.data tbody tr:last-child td { border-bottom: none; } .cell-primary { font-weight: 600; color: var(--text); } -.cell-sub { font-size: 12px; color: var(--text-3); } -.cell-mono { font-family: var(--mono); font-size: 12.5px; color: var(--text-2); } -.user-cell { display: flex; align-items: center; gap: 11px; } +.cell-sub { font-size: var(--fs-xs); color: var(--text-3); line-height: 1.4; } +.cell-mono { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-2); } +.user-cell { display: flex; align-items: center; gap: 11px; min-width: 0; } +/* Long emails/roles under a name clip with an ellipsis on every viewport — + this was previously only applied inside the 640px media query. */ +.user-cell .cell-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; } .user-cell .avatar { width: 34px; height: 34px; font-size: 12px; } .row-actions { display: flex; gap: 4px; justify-content: flex-end; } .act-btn { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; color: var(--text-3); transition: .12s; } @@ -592,7 +642,7 @@ table.data tbody tr:last-child td { border-bottom: none; } .act-btn svg { width: 16px; height: 16px; } /* Toolbar */ -.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; } +.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: var(--space-4); } .toolbar-search { position: relative; flex: 1; min-width: 200px; max-width: 340px; } .toolbar-search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-3); } .toolbar-search input { width: 100%; padding: 8px 12px 8px 36px; border-radius: 9px; background: var(--bg-elev); border: 1px solid var(--border-strong); outline: none; } @@ -662,14 +712,14 @@ canvas { width: 100%; max-width: 100%; display: block; } .modal-head p { font-size: 13px; color: var(--text-3); margin-top: 3px; } .modal-close { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; color: var(--text-3); } .modal-close:hover { background: var(--bg-sunken); color: var(--text); } -.modal-body { padding: 24px; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } +.modal-body { padding: var(--space-6); overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } .modal-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 18px 24px; border-top: 1px solid var(--border); background: var(--bg-sunken); border-radius: 0 0 var(--radius-xl) var(--radius-xl); } /* ================= FORMS ================= */ -.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } .form-field { display: flex; flex-direction: column; gap: 6px; } .form-field.col-span-2 { grid-column: 1 / -1; } -.form-field label { font-size: 12.5px; font-weight: 600; color: var(--text-2); } +.form-field label { font-size: var(--fs-sm); font-weight: 600; color: var(--text-2); line-height: var(--lh-label); } .form-field label .req { color: var(--danger); } .form-field input, .form-field select, .form-field textarea { padding: 9px 12px; border-radius: 9px; background: var(--bg-elev); border: 1px solid var(--border-strong); outline: none; transition: .15s; width: 100%; @@ -678,9 +728,9 @@ canvas { width: 100%; max-width: 100%; display: block; } .form-field textarea { resize: vertical; min-height: 84px; } .form-field input:focus, .form-field select:focus, .form-field textarea:focus { border-color: var(--primary); box-shadow: var(--ring); } .form-field input.err, .form-field select.err, .form-field textarea.err { border-color: var(--danger); } -.field-error { font-size: 11.5px; color: var(--danger); display: none; } +.field-error { font-size: var(--fs-xs); color: var(--danger); display: none; } .field-error.show { display: block; } -.form-section-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); margin: 22px 0 4px; grid-column: 1/-1; } +.form-section-title { margin: var(--space-5) 0 var(--space-1); grid-column: 1/-1; } /* AI field assist (ui/AiFieldAssist.jsx) */ .field-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } @@ -718,14 +768,14 @@ canvas { width: 100%; max-width: 100%; display: block; } .setting-info p { font-size: 13px; color: var(--text-3); margin-top: 2px; } /* ================= TABS ================= */ -.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 22px; overflow-x: auto; } -.tab { display: inline-flex; align-items: center; gap: 12px; padding: 11px 16px; font-weight: 600; font-size: 13.5px; color: var(--text-2); border-bottom: 2px solid transparent; white-space: nowrap; transition: .15s; margin-bottom: -1px; } +.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: var(--space-5); overflow-x: auto; } +.tab { display: inline-flex; align-items: center; gap: 8px; padding: 11px 16px; font-weight: 600; font-size: var(--fs-base); color: var(--text-2); border-bottom: 2px solid transparent; white-space: nowrap; transition: .15s; margin-bottom: -1px; } .tab:hover { color: var(--text); } .tab.active { color: var(--primary); border-bottom-color: var(--primary); } .tab-pane { display: none; animation: fadeUp .25s; } .tab-pane.active { display: block; } .pill-tabs { display: inline-flex; gap: 4px; background: var(--bg-sunken); padding: 4px; border-radius: 11px; } -.pill-tab { padding: 7px 14px; border-radius: 8px; font-weight: 600; font-size: 13px; color: var(--text-2); transition: .15s; } +.pill-tab { padding: 7px 14px; border-radius: 8px; font-weight: 600; font-size: var(--fs-sm); color: var(--text-2); transition: .15s; } .pill-tab.active { background: var(--bg-elev); color: var(--text); box-shadow: var(--shadow-sm); } /* ================= KANBAN ================= */ @@ -734,7 +784,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .kanban-col-head { display: flex; align-items: center; gap: 8px; padding: 14px 16px; position: sticky; top: 0; } .kanban-col-head .k-dot { width: 9px; height: 9px; border-radius: 50%; } .kanban-col-head h4 { font-size: 13.5px; font-weight: 700; } -.k-count { margin-left: auto; background: var(--bg-elev); color: var(--text-2); font-size: 12px; font-weight: 700; padding: 1px 9px; border-radius: 20px; } +.k-count { margin-left: auto; background: var(--bg-elev); color: var(--text-2); font-size: var(--fs-xs); font-weight: 700; padding: 1px 9px; border-radius: 20px; } .kanban-cards { padding: 0 12px 12px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; min-height: 60px; } .kanban-cards.drag-over { background: var(--primary-soft); border-radius: 10px; outline: 2px dashed var(--primary); outline-offset: -4px; } .k-card { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 11px; padding: 13px; cursor: grab; box-shadow: var(--shadow-sm); transition: .15s; } @@ -742,11 +792,11 @@ canvas { width: 100%; max-width: 100%; display: block; } .k-card.dragging { opacity: .5; transform: rotate(2deg); cursor: grabbing; } .k-card-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .k-card-top .avatar { width: 32px; height: 32px; font-size: 11px; } -.kc-name { font-weight: 600; font-size: 13.5px; } +.kc-name { font-weight: 600; font-size: var(--fs-base); } .kc-role { font-size: 12px; color: var(--text-3); } .k-card-meta { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); } .k-tags { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 8px; } -.tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 6px; background: var(--bg-sunken); color: var(--text-2); } +.tag { font-size: var(--fs-xs); font-weight: 600; padding: 2px 8px; border-radius: var(--radius-sm); background: var(--bg-sunken); color: var(--text-2); } /* ================= MISC ================= */ .list-tight > * + * { border-top: 1px solid var(--border); } @@ -754,8 +804,8 @@ canvas { width: 100%; max-width: 100%; display: block; } .list-row:first-child { padding-top: 0; } .list-row .avatar { width: 38px; height: 38px; font-size: 13px; } .lr-main { flex: 1; min-width: 0; } -.lr-title { font-weight: 600; font-size: 13.5px; } -.lr-sub { font-size: 12.5px; color: var(--text-3); } +.lr-title { font-weight: 600; font-size: var(--fs-base); } +.lr-sub { font-size: var(--fs-sm); color: var(--text-3); } .lr-right { text-align: right; flex-shrink: 0; } .timeline { position: relative; padding-left: 28px; } .timeline::before { content: ''; position: absolute; left: 9px; top: 4px; bottom: 4px; width: 2px; background: var(--border); } @@ -763,13 +813,14 @@ canvas { width: 100%; max-width: 100%; display: block; } .tl-item:last-child { padding-bottom: 0; } .tl-dot { position: absolute; left: -28px; top: 2px; width: 20px; height: 20px; border-radius: 50%; background: var(--bg-elev); border: 2px solid var(--primary); display: grid; place-items: center; } .tl-dot svg { width: 11px; height: 11px; color: var(--primary); } -.tl-title { font-weight: 600; font-size: 13.5px; } +.tl-title { font-weight: 600; font-size: var(--fs-base); } .tl-meta { font-size: 12px; color: var(--text-3); margin-top: 2px; } .tl-desc { font-size: 13px; color: var(--text-2); margin-top: 5px; } .empty-state { text-align: center; padding: 60px 20px; color: var(--text-3); } .empty-state svg { width: 48px; height: 48px; margin-bottom: 14px; opacity: .5; } -.empty-state h3 { font-size: 18px; color: var(--text-2); margin-bottom: 6px; } +.empty-state h3 { font-size: var(--fs-lg); color: var(--text-2); margin-bottom: 6px; } +.empty-state p { max-width: 46ch; margin-inline: auto; line-height: var(--lh-body); } .avatar-stack { display: flex; } .avatar-stack .avatar { width: 30px; height: 30px; font-size: 11px; border: 2px solid var(--bg-elev); margin-left: -8px; } @@ -778,20 +829,52 @@ canvas { width: 100%; max-width: 100%; display: block; } .stat-mini { display: flex; flex-direction: column; gap: 4px; } .stat-mini-val { font-size: 22px; font-weight: 700; letter-spacing: -.5px; } -.stat-mini-lbl { font-size: 12.5px; color: var(--text-3); } +.stat-mini-lbl { font-size: var(--fs-sm); color: var(--text-3); } .divider { height: 1px; background: var(--border); margin: 16px 0; } .flex { display: flex; } +.flex-col { flex-direction: column; } .items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.flex-wrap { flex-wrap: wrap; } +.flex-1 { flex: 1 1 0%; min-width: 0; } +.min-w-0 { min-width: 0; } +.w-full { width: 100%; } +.gap-4 { gap: var(--space-1); } .gap-8 { gap: 8px; } .gap-12 { gap: 12px; } .gap-16 { gap: 16px; } +.mt-8 { margin-top: var(--space-2); } .mb-8 { margin-bottom: var(--space-2); } +.mt-12 { margin-top: var(--space-3); } .mb-12 { margin-bottom: var(--space-3); } +.mt-16 { margin-top: var(--space-4); } .mb-16 { margin-bottom: var(--space-4); } +.mt-24 { margin-top: var(--space-6); } .mb-24 { margin-bottom: var(--space-6); } .text-muted { color: var(--text-3); } .fw-600 { font-weight: 600; } -.text-sm { font-size: 12.5px; } +.text-sm { font-size: var(--fs-sm); } .mono { font-family: var(--mono); } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +/* Inline clamp for table cells (emails, long titles); pair with title="". */ +.cell-clip { display: inline-block; max-width: 26ch; vertical-align: bottom; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.row-click { cursor: pointer; } + +/* Classes referenced from JSX that previously had no definition. */ +.dt { display: flow-root; } +.route-loading { display: grid; place-items: center; min-height: 40vh; } +.tab-count { font-size: var(--fs-xs); font-weight: 600; padding: 1px 8px; + border-radius: 20px; background: var(--bg-sunken); color: var(--text-2); } +.tab.active .tab-count { background: var(--primary-soft); color: var(--primary); } + +/* Keyboard users jump straight past the 20+ sidebar links. */ +.skip-link { position: absolute; left: -9999px; z-index: 1000; } +.skip-link:focus { + left: 12px; top: 12px; position: fixed; background: var(--bg-elev); + color: var(--text); padding: 10px 16px; border-radius: var(--radius-sm); + box-shadow: var(--shadow-lg); outline: 2px solid var(--primary); +} /* Calendar */ .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; } -.cal-dow { background: var(--bg-elev); padding: 10px; text-align: center; font-size: 11.5px; font-weight: 700; text-transform: uppercase; color: var(--text-3); letter-spacing: .5px; } +.cal-dow { background: var(--bg-elev); padding: 10px; text-align: center; } .cal-cell { background: var(--bg-elev); min-height: 108px; padding: 8px; position: relative; transition: .12s; } .cal-cell:hover { background: var(--bg-sunken); } .cal-cell.other { background: var(--bg-sunken); } @@ -809,8 +892,8 @@ canvas { width: 100%; max-width: 100%; display: block; } .toast-icn { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; flex-shrink: 0; } .toast-icn svg { width: 18px; height: 18px; } .toast-body { flex: 1; } -.toast-title { font-weight: 600; font-size: 13.5px; } -.toast-msg { font-size: 12.5px; color: var(--text-3); } +.toast-title { font-weight: 600; font-size: var(--fs-base); } +.toast-msg { font-size: var(--fs-sm); color: var(--text-3); } .toast-close { color: var(--text-3); width: 24px; height: 24px; display: grid; place-items: center; border-radius: 6px; } .toast-close:hover { background: var(--bg-sunken); } @@ -837,8 +920,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .ph-role { color: var(--text-2); font-size: 14px; } .ph-tags { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; } .info-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 14px 24px; } -.info-item .il { font-size: 12px; color: var(--text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .4px; } -.info-item .iv { font-size: 14px; font-weight: 500; margin-top: 3px; } +.info-item .iv { font-size: var(--fs-base); font-weight: 500; margin-top: 3px; } /* ================= ENTERPRISE PHASE 2 ================= */ @@ -902,7 +984,7 @@ canvas { width: 100%; max-width: 100%; display: block; } 11px copy keeps its contrast in both modes. */ .source-chip { display: inline-flex; align-items: center; gap: 5px; - font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 20px; + font-size: var(--fs-xs); font-weight: 600; padding: 2px 8px; border-radius: 20px; --chip: var(--text-3); color: var(--text-2); background: var(--bg-sunken); /* fallback: color-mix needs Safari 16.2+ / Chrome 111+ */ @@ -911,7 +993,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .source-chip svg { width: 12px; height: 12px; color: var(--chip); } .source-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--chip); } -.integration-status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 20px; font-size: 12.5px; font-weight: 600; background: var(--success-soft); color: var(--success); } +.integration-status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 20px; font-size: var(--fs-sm); font-weight: 600; background: var(--success-soft); color: var(--success); } .integration-status.pending { background: var(--warning-soft); color: var(--warning); } .integration-status .pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; position: relative; } .integration-status .pulse::after { content: ''; position: absolute; inset: 0; border-radius: 50%; background: currentColor; animation: pulse 1.8s infinite; } @@ -953,7 +1035,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .ats-ring .ats-val { position: relative; z-index: 1; text-align: center; } .ats-ring .ats-num { font-size: 30px; font-weight: 800; letter-spacing: -1px; line-height: 1; } .ats-ring .ats-lbl { font-size: 11px; color: var(--text-3); font-weight: 600; } -.skill-pill { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 8px; } +.skill-pill { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-xs); font-weight: 600; padding: 4px 10px; border-radius: var(--radius-sm); } .skill-pill svg { width: 12px; height: 12px; } .skill-matched { background: var(--success-soft); color: var(--success); } .skill-missing { background: var(--danger-soft); color: var(--danger); } @@ -968,7 +1050,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .cand-name { font-weight: 700; font-size: 14.5px; letter-spacing: -.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cand-role { font-size: 12.5px; color: var(--text-3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cand-skills { display: flex; flex-wrap: wrap; gap: 6px; align-content: flex-start; height: 54px; overflow: hidden; margin-bottom: 12px; } -.cand-chip { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 10px; border-radius: 7px; font-size: 12px; font-weight: 600; background: var(--bg-sunken); color: var(--text-2); white-space: nowrap; } +.cand-chip { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 10px; border-radius: var(--radius-sm); font-size: var(--fs-xs); font-weight: 600; background: var(--bg-sunken); color: var(--text-2); white-space: nowrap; } .cand-chip svg { width: 11px; height: 11px; } .cand-chip.miss { background: var(--danger-soft); color: var(--danger); } .cand-chip.more { background: transparent; color: var(--text-3); padding: 0 4px; } @@ -1026,7 +1108,7 @@ canvas { width: 100%; max-width: 100%; display: block; } /* Role chips use the avatar ramp, which inverts with the theme. */ .role-badge { width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; color: var(--avatar-fg); flex-shrink: 0; } .rbac-matrix { width: 100%; border-collapse: collapse; font-size: 13px; } -.rbac-matrix th { padding: 12px 8px; font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--text-3); border-bottom: 1px solid var(--border); text-align: center; font-weight: 700; } +.rbac-matrix th { padding: 12px 8px; border-bottom: 1px solid var(--border); text-align: center; } .rbac-matrix th:first-child { text-align: left; padding-left: 16px; } .rbac-matrix td { padding: 10px 8px; border-bottom: 1px solid var(--border); text-align: center; } .rbac-matrix td:first-child { text-align: left; padding-left: 16px; font-weight: 600; } @@ -1121,14 +1203,14 @@ canvas { width: 100%; max-width: 100%; display: block; } /* Filter panel */ .filter-panel { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } -.filter-panel .form-field label { font-size: 11.5px; } +.filter-panel .form-field label { font-size: var(--fs-xs); } .star-btn { color: var(--text-3); transition: .12s; } .star-btn.on { color: var(--warning); } .star-btn.on svg { fill: currentColor; } /* Segmented control */ .seg { display: inline-flex; background: var(--bg-sunken); padding: 3px; border-radius: 10px; } -.seg button { padding: 6px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--text-2); } +.seg button { padding: 6px 14px; border-radius: 8px; font-size: var(--fs-sm); font-weight: 600; color: var(--text-2); } .seg button.active { background: var(--bg-elev); color: var(--text); box-shadow: var(--shadow-sm); } .rating-stars { display: inline-flex; gap: 3px; } @@ -1216,10 +1298,10 @@ canvas { width: 100%; max-width: 100%; display: block; } 400 small phone · plus a landscape-phone height rule ============================================================ */ -/* Ultrawide: stop dashboards stretching to unreadable line lengths. */ -@media (min-width: 1600px) { - .content > .page { max-width: 1560px; margin-inline: auto; } -} +/* Cap the measure on every desktop, not only ultrawide — between 900 and + 1600px the content used to run full-bleed. No-op below ~1500px viewports; + agrees with .cand-page's own 1440px cap. */ +.content > .page { max-width: 1440px; margin-inline: auto; } @media (max-width: 1200px) { .g-kpi { grid-template-columns: repeat(2, 1fr); } @@ -1256,7 +1338,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .g-kpi { grid-template-columns: 1fr; } .topbar { padding-left: max(12px, env(safe-area-inset-left)); padding-right: max(12px, env(safe-area-inset-right)); gap: 6px; } .form-grid, .info-grid, .filter-panel { grid-template-columns: 1fr; } - .page-title { font-size: 25px; } + .page-title { font-size: var(--fs-2xl); } .page-head { gap: 12px; margin-bottom: 18px; } .page-head-actions { width: 100%; } .page-head-actions .btn { flex: 1 1 auto; justify-content: center; } @@ -1416,7 +1498,7 @@ canvas { width: 100%; max-width: 100%; display: block; } /* Score summary: stat tiles, hero = combined overall */ .hf-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px; } .hf-tile { background: var(--bg-sunken); border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; min-width: 0; } -.hf-tile .hf-k { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.hf-tile .hf-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hf-tile .hf-v { font-size: 20px; font-weight: 700; margin-top: 4px; font-variant-numeric: tabular-nums; } .hf-tile .hf-v small { font-size: 12px; font-weight: 600; color: var(--text-3); margin-left: 2px; } .hf-tile .hf-sub { font-size: 11.5px; color: var(--text-3); margin-top: 4px; } @@ -1426,7 +1508,7 @@ canvas { width: 100%; max-width: 100%; display: block; } /* Bordered section card, titled like the paper form's section headers */ .hf-block { border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; margin-top: 14px; } -.hf-block-title { font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); margin-bottom: 12px; display: flex; align-items: center; gap: 10px; } +.hf-block-title { margin-bottom: 12px; display: flex; align-items: center; gap: 10px; } .hf-block-title label { display: flex; align-items: center; gap: 8px; cursor: pointer; text-transform: none; letter-spacing: 0; font-size: 13px; font-weight: 600; color: var(--text-2); } .hf-note { font-size: 12.5px; color: var(--text-3); margin: 2px 0 10px; } @@ -1434,7 +1516,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .hf-rate { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } .hf-rate-head, .hf-rate-row, .hf-rate-foot { display: grid; grid-template-columns: minmax(0, 1fr) repeat(4, 96px); align-items: center; } .hf-rate-head { background: var(--bg-sunken); } -.hf-rate-head > div { padding: 8px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px; color: var(--text-3); text-align: center; line-height: 1.25; } +.hf-rate-head > div { padding: 8px 10px; text-align: center; } .hf-rate-head > div:first-child { text-align: left; } .hf-rate-row { border-top: 1px solid var(--border); } .hf-rate-row > div:first-child { padding: 9px 10px; font-size: 13px; } @@ -1443,7 +1525,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .hf-dot:hover { border-color: var(--primary); } .hf-dot.on { background: var(--primary); border-color: var(--primary); box-shadow: inset 0 0 0 3.5px var(--bg-elev); } .hf-rate-foot { border-top: 1px solid var(--border); background: var(--bg-sunken); } -.hf-rate-foot > div:first-child { padding: 8px 10px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px; color: var(--text-3); } +.hf-rate-foot > div:first-child { padding: 8px 10px; } .hf-rate-foot .hf-avg { grid-column: 2 / -1; text-align: center; font-size: 13.5px; font-weight: 700; font-variant-numeric: tabular-nums; padding: 8px 0; } /* Completion dot on the form switcher */ @@ -1452,7 +1534,6 @@ canvas { width: 100%; max-width: 100%; display: block; } /* Approvals: four signature slots */ .hf-sign-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } .hf-sign { display: flex; flex-direction: column; gap: 6px; } -.hf-sign .hf-sign-role { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--text-3); } @media (max-width: 860px) { /* Rating columns keep their full 96px width here — the word headers still From bd16f8a775d28df7b9ce50f0a621650510c00d42 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Tue, 25 Aug 2026 21:39:31 +0500 Subject: [PATCH 04/22] ui-polish 2/6: PageHeader, DataTable dedup, Tabs/Toast/Dropdown a11y - New ui/PageHeader.jsx emitting the existing .page-head structure; screens adopt it in the next two passes. - DataTable: sortable thead extracted as DataTableHead (Candidates can drop its verbatim copy) and an onRowClick prop with keyboard support. - Tabs: index-based ids, aria-controls, roving tabindex, arrow-key and Home/End navigation; TabPanel labels its panel. - Toast root announces politely to screen readers (was silent). - Dropdown stamps aria-expanded/aria-haspopup on every trigger. - primitives: shared PRIORITY_CLASS (Dashboard and Tasks had identical private copies). - Shell: skip link to #main-content, sidebar nav labeled Primary, clickable notification rows are real buttons now. Co-Authored-By: Claude Fable 5 --- frontend/src/app/AppLayout.jsx | 1 + frontend/src/app/Sidebar.jsx | 2 +- frontend/src/app/Topbar.jsx | 6 +-- frontend/src/styles/styles.css | 4 +- frontend/src/ui/DataTable.jsx | 72 +++++++++++++++++++++------------- frontend/src/ui/Dropdown.jsx | 8 +++- frontend/src/ui/PageHeader.jsx | 24 ++++++++++++ frontend/src/ui/Tabs.jsx | 44 +++++++++++++++++---- frontend/src/ui/Toast.jsx | 2 +- frontend/src/ui/primitives.jsx | 4 ++ 10 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 frontend/src/ui/PageHeader.jsx diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx index de62040..22d4d3a 100644 --- a/frontend/src/app/AppLayout.jsx +++ b/frontend/src/app/AppLayout.jsx @@ -38,6 +38,7 @@ export default function AppLayout() { return ( -
diff --git a/frontend/src/styles/styles.css b/frontend/src/styles/styles.css index d59ebeb..5f6d99c 100644 --- a/frontend/src/styles/styles.css +++ b/frontend/src/styles/styles.css @@ -511,7 +511,9 @@ table.data thead th, .rbac-matrix th, .cal-dow, .info-item .il, .dropdown-link.danger { color: var(--danger); } .dropdown-link.danger svg { color: var(--danger); } .link-btn { color: var(--primary); font-size: 12px; font-weight: 600; } -.notif-row { display: flex; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: .12s; } +/* Rendered as a