/* ──────────────────────────────────────────────────────────────
   iVE Design Tokens
   One master --brand-h hue drives the entire palette.
   Light & dark themes share the same tokens; values differ.
   ────────────────────────────────────────────────────────────── */

:root {
  /* Master brand — only thing clients change.
     Values MUST match `TWEAK_DEFAULTS.brand` in every page's React
     mount (and `_nav.html`'s nav-overlay). If they diverge, a user
     with no localStorage brand sees the skeleton paint with these
     defaults and React's mount immediately overwrites them — a
     visible brand-color flicker on cold loads / incognito / cleared
     cache. Keep the three blocks in sync. */
  --brand-h: 205;           /* hue 0..360 */
  --brand-c: 0.14;          /* chroma */
  --brand-l: 0.48;          /* base lightness */

  /* Type */
  --font-sans: 'Inter Tight', 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-ar: 'IBM Plex Sans Arabic', 'Inter Tight', system-ui, sans-serif;
  /* --font-ui is the single UI font: switches to the Arabic stack when RTL. */
  --font-ui: var(--font-sans);

  /* Radii */
  --r-xs: 6px;
  --r-sm: 8px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-xl: 20px;
  --r-pill: 999px;

  /* Table chrome — single source of truth for the 5 different table
     implementations (savedtests data table, reports tables, branch
     comparison, fleet, examiners, top-10). Each table still owns its
     row layout / column widths / special cells, but the surface
     chrome (frame border, header bg/fg, divider, row hover, sort
     indicator) all pulls from these tokens so a single change
     propagates. */
  --tbl-frame-border:  var(--line);
  --tbl-frame-radius:  var(--r-lg);
  --tbl-header-bg:     var(--bg-sunken);
  --tbl-header-fg:     var(--ink-3);
  --tbl-divider:       var(--line);
  --tbl-divider-soft:  var(--line);
  /* Section border — used at the boundary between thead/tbody/tfoot
     so the three sections read as clearly delineated bands inside the
     framed wrapper. Slightly stronger than the row divider so section
     boundaries pop while row dividers stay quiet. */
  --tbl-section-border: var(--line-strong, var(--line));
  --tbl-row-hover-bg:  color-mix(in oklab, var(--brand-600) 6%, transparent);
  --tbl-row-stripe-bg: color-mix(in srgb, var(--ink) 2.5%, transparent);
  --tbl-sort-active:   var(--brand-600);
  /* Table cell foreground — unified to full ink so all table body
     cells share the same legibility baseline. The legacy --ink-2
     (0.38 lightness) tier created an "examiners-only" semi-muted
     look that diverged from every other table in the app. Use
     `cell-muted` explicitly on individual cells when a column is
     contextual (e.g. Saved Tests dates / times) rather than
     primary. */
  --tbl-cell-fg:       var(--ink);
  --tbl-cell-strong:   var(--ink);

  /* Chart palette — anchor on iVE blue, balanced cool→warm spread.
     Promoted from dashboard.css (mobility-only) so dashboards (Tier
     1/2/3) and reports can share the same chart hues. Used for
     spark + area fills. */
  --chart-1: #0372E1;            /* anchor — iVE blue */
  --chart-2: #58A6F8;            /* light blue */
  --chart-3: #00C4D9;            /* cyan — examiners online */
  --chart-4: #14B8A6;            /* teal */
  --chart-5: #84CC16;            /* lime */
  --chart-6: #F59E0B;            /* amber */
  --chart-7: #F43F5E;            /* rose */
  --chart-8: #A855F7;            /* purple */

  /* Spacing scale */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-8: 32px;
  --s-10: 40px;
  --s-12: 48px;

  /* ─── Typography scale (9 semantic tiers) ─────────────────────
     Single source of truth for font-size/line-height/weight/letter-
     spacing across the system. Defined as packed shorthand for
     `font:` (`weight size/line-height family`) plus separate
     letter-spacing tokens since CSS `font:` shorthand doesn't
     include letter-spacing.

     When you need to set a font: use the matching `font: var(--fs-*)`
     and pair with `letter-spacing: var(--ls-*)`. See
     `project_design_system_master.md` for the cheat-sheet of when
     to use which tier.

     Sizes pinned at major rhythm points: 28 / 22 / 18 / 16 / 14 /
     12 / 11 px. The 14px body is the table-content baseline (May
     2026 re-tune from 13 → 14 for accessibility, matching Linear /
     Stripe / Vercel dashboards). The previous 10px `--fs-micro`
     tier was retired — anywhere it was used now snaps to caption
     (11px), establishing 11px as the absolute floor.

     Shorthand `--fs-*` tokens compose from `--fsz-*` via var() so
     the `[dir="rtl"]` token redef block (further down) auto-bumps
     shorthand-using rules in Arabic. `font:` shorthand accepts
     `var()` for the size slot — values resolve at use-time. */
  --fs-display:     700 var(--fsz-display)/1.1  var(--font-ui);
  --fs-h1:          700 var(--fsz-h1)/1.2       var(--font-ui);
  --fs-h2:          600 var(--fsz-h2)/1.25      var(--font-ui);
  --fs-h3:          600 var(--fsz-h3)/1.3       var(--font-ui);
  --fs-body:        400 var(--fsz-body)/1.5     var(--font-ui);
  --fs-body-strong: 600 var(--fsz-body-strong)/1.5 var(--font-ui);
  /* body-sm — for dense table cells, condensed meta rows, and any
     body-shaped text where the 14px baseline takes too much vertical
     room. Industry-standard B2B dashboards (Linear / Stripe / Notion)
     all carry this tier between body and label. */
  --fs-body-sm:     400 var(--fsz-body-sm)/1.5  var(--font-ui);
  --fs-label:       500 var(--fsz-label)/1.4    var(--font-ui);
  /* caption: supporting / contextual meta text. Same size as label
     (12px) but visually distinct via lower weight (400) and zero
     tracking. Use for sub-meta lines like "5 branches", "vs 134
     prev", "Test ID 16685930…" — anywhere that sits *under* a
     primary label or value and shouldn't compete with it.
     Previously was 11 / 600 / 0.04em (uppercase chrome label) —
     migrated October 2026; surfaces that still need uppercase
     chrome labels (TESTS TODAY etc.) apply text-transform +
     letter-spacing per-component, not at the token level. */
  --fs-caption:     400 var(--fsz-caption)/1.4  var(--font-ui);
  /* Mono-family tokens — for inline `code` and `kbd` elements.
     Both use --font-mono (JetBrains Mono). `code` matches body
     size for inline parity; `kbd` is a notch smaller and bolder
     to read as a discrete affordance. Industry pattern (Linear,
     Stripe Dashboard, Notion all carry these as separate tiers). */
  --fs-code:        400 13px/1.5  var(--font-mono);
  --fs-kbd:         600 12px/1.3  var(--font-mono);

  /* Letter-spacing tokens, paired with the size tiers above. */
  --ls-display:     -0.02em;
  --ls-h1:          -0.01em;
  --ls-h2:          -0.01em;
  --ls-h3:          -0.005em;     /* slight negative for visual rhythm at 16px (industry-standard) */
  --ls-body:         0;
  --ls-body-sm:      0;
  --ls-body-strong:  0;
  --ls-label:        0.02em;
  /* caption ls: 0 (was 0.04em). The 0.04em uppercase-style tracking
     was tied to caption's old uppercase-chrome role. Supporting meta
     text needs no tracking. Surfaces that still need uppercase
     chrome apply letter-spacing per-component. */
  --ls-caption:      0;
  --ls-code:         0;            /* mono fonts already track tightly — no adjust */
  --ls-kbd:          0.02em;       /* slight positive on shortcut display for clarity */
  /* --ls-micro retired with the --fs-micro tier */

  /* Size-only tokens — use these when migrating an existing rule
     that already declares `font-weight` / `font-family` / `line-height`
     and you only want to swap the size. The shorthand `--fs-*`
     above is for fresh code that wants the full type-role in one
     line; `--fsz-*` is for surgical migration. Same source values,
     decomposed.
     Per project_design_system_master.md typography migration plan. */
  --fsz-display:     28px;
  --fsz-h1:          22px;
  --fsz-h2:          18px;
  --fsz-h3:          16px;
  --fsz-body:        14px;
  --fsz-body-strong: 14px;  /* same size as body — weight differs at point of use */
  /* body-sm: aliased to body (14px) as of May 2026 — was 13px which
     created a single-pixel step against body (14). Single-pixel steps
     in a type scale don't read as discrete tiers. Kept as a token for
     backward compat with the ~47 existing callsites; new code should
     use --fsz-body for body content or --fsz-label for label-tier. */
  --fsz-body-sm:     14px;
  --fsz-label:       12px;
  /* caption: aliased to label (12px) — was 11px which created a
     single-pixel step against label. Same rationale as body-sm:
     1px steps don't register as discrete tiers, and 10px would
     fall below WCAG comfort floor. Token kept for backward compat
     with ~80 existing callsites; new code should use --fsz-label. */
  --fsz-caption:     12px;
  /* chart-label: 10px — DELIBERATE OFF-SCALE EXCEPTION for SVG /
     data-viz / map-overlay labels (radar axes, bullet HQ markers,
     gauge labels, compass-rose, map zone overlays, calendar
     weekday cells). NOT for body or UI chrome — those use --fsz-
     label (12px). Banking-grade dashboards (Datadog, Bloomberg)
     all run smaller-than-body labels in data viz; 10px keeps chart
     density without violating the canonical UI text floor. */
  --fsz-chart-label: 10px;
  /* --fsz-micro retired May 2026 — was 10px, replaced everywhere
     with --fsz-caption (11px). 11px is now the absolute floor. */

  /* ─── Z-index scale (8 named layers) ────────────────────────
     Replaces the 30+ ad-hoc z-index values that accumulated
     before this scale was introduced. Every new floating /
     stacked element MUST use one of these tokens. If you need a
     layer that isn't here, the right move is to revisit the
     hierarchy — adding a new z-index token is a project-level
     conversation. */
  --z-base:     0;
  --z-raised:   10;     /* sticky table headers, raised cards */
  --z-dropdown: 100;    /* select menus, autocomplete */
  --z-sticky:   200;    /* sticky page headers */
  --z-overlay:  500;    /* full-screen scrims, drawers */
  --z-modal:    1000;   /* modal dialogs */
  --z-toast:    1500;   /* transient toasts above modals */
  --z-tooltip:  2000;   /* portaled tooltips above everything */

  /* Shadows (soft, layered, not glass) */
  --sh-xs: 0 1px 2px -1px color-mix(in oklab, var(--ink) 10%, transparent),
           0 1px 1px color-mix(in oklab, var(--ink) 4%, transparent);
  --sh-sm: 0 2px 4px -2px color-mix(in oklab, var(--ink) 10%, transparent),
           0 1px 2px color-mix(in oklab, var(--ink) 5%, transparent);
  --sh-md: 0 8px 24px -12px color-mix(in oklab, var(--ink) 18%, transparent),
           0 2px 6px -2px color-mix(in oklab, var(--ink) 8%, transparent);
  --sh-lg: 0 24px 48px -20px color-mix(in oklab, var(--ink) 22%, transparent),
           0 6px 12px -6px color-mix(in oklab, var(--ink) 10%, transparent);
  --sh-brand: 0 10px 30px -10px color-mix(in oklch, var(--brand-600) 50%, transparent);

  /* ── Avatar ring ──
     Every avatar surface in the app — top-nav user bubble, table
     photos, live-test tile, notification panel, client rail, etc. —
     carries this 1px ring so a white photo on a white card still has
     a defined edge. Rendered OUTSET (not inset) so it sits on top of
     the avatar circle's edge — `inset` shadows are clipped behind
     <img> children with `overflow: hidden`, making the ring invisible
     on photo avatars. Outset adds 1px beyond the circle, which is
     visible regardless of inner content. Hover/active rules compose
     with this base via `box-shadow: var(--avatar-ring), <other rings>`. */
  --avatar-ring: 0 0 0 1px var(--line);

  /* Status (fixed semantics, subtly harmonized) */
  --ok-h: 150;
  --warn-h: 40;
  --err-h: 18;
  --info-h: 256;   /* Tailwind blue family — vivid info, brand-independent across tenants */

  /* Transitions */
  --t-fast: 120ms cubic-bezier(.4,0,.2,1);
  --t-med: 200ms cubic-bezier(.4,0,.2,1);
}

/* ──────────────────────────────────────────────────────────────
   TOKEN SYSTEM BOUNDARY — `--*` vs `--d-*`
   ────────────────────────────────────────────────────────────────
   Two parallel token systems intentionally coexist in this codebase:

     • `--*` (this file)              — brand-driven, multi-tenant.
       Hue tracks `--brand-h`; surfaces, text, lines, and brand
       ramp shift to match the tenant's brand color.

     • `--d-*` (page.css line ~10113) — hue-locked at 250 (neutral
       slate-blue). Used inside `.app:has(.dash-header)` so the
       Tier-1/2/3 dashboards keep a calm, consistent, neutral
       chrome regardless of the tenant brand. If a tenant picks an
       extreme brand (pink, red, lime), the dashboards still read
       as "professional dashboards" rather than "everything is
       pink".

   This is NOT redundancy — they serve different roles. Don't merge.
   When deciding which to use:
     • Use `--*` for elements that should reflect tenant brand
       (CTAs, highlights, brand signage, login chrome, marketing).
     • Use `--d-*` for the data-dense dashboard surface (cards, ink,
       table chrome, gauges) where neutrality > brand expression.

   The categorical tokens (`--male / --female / --indigo / --purple`,
   plus `--chart-1` … `--chart-8`) are also hue-locked but live in
   the global namespace because they're reused across both surfaces
   (chart palettes, gender bars, KPI accents).

   Documented for new contributors so this boundary doesn't drift.
   See `memory/project_design_system_master.md` for the full rule.
   ────────────────────────────────────────────────────────────── */

/* ─────────── LIGHT THEME ─────────── */
[data-theme="light"] {
  color-scheme: light;

  /* Canvas — slightly darker than white but stays neutral, with a
     barely-there cool cast (chroma 0.002 at fixed cool hue 250).
     Earlier we tried brand-hue tinting at 0.008 chroma which made
     the canvas read pink/red when the brand color was red — at this
     low chroma the tint is imperceptible regardless of brand. */
  --bg: oklch(0.975 0.002 250);
  --bg-muted: oklch(0.965 0.002 250);
  --bg-raised: oklch(1 0 0);
  --bg-sunken: oklch(0.955 0.002 250);

  /* Ink */
  --ink: oklch(0.22 0.02 var(--brand-h));
  --ink-2: oklch(0.38 0.015 var(--brand-h));
  --ink-3: oklch(0.55 0.012 var(--brand-h));
  --ink-4: oklch(0.70 0.010 var(--brand-h));

  /* Borders */
  --line: oklch(0.92 0.006 var(--brand-h));
  --line-2: oklch(0.88 0.008 var(--brand-h));
  --line-strong: oklch(0.80 0.01 var(--brand-h));

  /* Tooltip surface — INVERTED contrast so tooltips read as a separate
     overlay on top of the page chrome. Light page → dark tooltip
     (slate near-black + light text). Brand-independent so the
     tooltip surface stays stable across tenants. */
  --tooltip-bg:        oklch(0.22 0.008 250);
  --tooltip-fg:        oklch(0.98 0 0);
  --tooltip-fg-muted:  oklch(0.75 0.005 250);
  --tooltip-border:    oklch(0.30 0.01 250);
  --tooltip-shadow:    0 8px 24px -6px rgba(0, 0, 0, 0.30), 0 2px 6px -2px rgba(0, 0, 0, 0.20);

  /* Brand scale — derived */
  --brand-50:  oklch(0.97 calc(var(--brand-c) * 0.15) var(--brand-h));
  --brand-100: oklch(0.93 calc(var(--brand-c) * 0.30) var(--brand-h));
  --brand-200: oklch(0.86 calc(var(--brand-c) * 0.55) var(--brand-h));
  --brand-300: oklch(0.76 calc(var(--brand-c) * 0.75) var(--brand-h));
  --brand-400: oklch(0.65 calc(var(--brand-c) * 0.90) var(--brand-h));
  --brand-500: oklch(var(--brand-l) var(--brand-c) var(--brand-h));
  /* brand-600 — the CTA fill. Clamp the lightness so a CTA filled with this
     token is ALWAYS dark enough for white text to read, no matter how light
     a brand the user picks. min() takes the darker of the two values: dark
     brands keep their natural shade, light brands (e.g. sky #0ea5e9) get
     pulled down to the 0.52 ceiling. Without this clamp, light brands land
     in a mid-tone no-man's-land where neither white nor black text reads. */
  --brand-600: oklch(
    min(calc(var(--brand-l) - 0.06), 0.52)
    var(--brand-c)
    var(--brand-h)
  );
  /* brand-700 — high-contrast text/border color. Clamp the lightness so it
     stays readable against light backgrounds even when the user picks a
     near-white brand. min() picks the darker of the two values, so dark
     brands stay dark and light brands snap to a 0.40 floor. */
  --brand-700: oklch(
    min(calc(var(--brand-l) - 0.14), 0.40)
    calc(var(--brand-c) * 0.9)
    var(--brand-h)
  );
  --brand-900: oklch(0.22 calc(var(--brand-c) * 0.5) var(--brand-h));

  /* CTA ink — sits on top of --brand-600. Because brand-600 is clamped to
     ≤0.52 lightness, near-white ink is always the right call. A small hint
     of the brand hue keeps the text feeling cohesive without compromising
     contrast. */
  --brand-ink: oklch(0.98 0.01 var(--brand-h));
  /* ─── Hover / active tint tokens (system standard) ───────────────────
     Single source of truth for every interactive surface's hover and
     active background. Defined in oklab + TRANSPARENT so they:
       1. read the same across light / dark (no need for theme overrides),
       2. don't darken / shift unexpectedly when the user picks a new
          brand color via the tweaks panel,
       3. lay cleanly on top of whatever surface is behind (page bg,
          card, modal, sidenav, popover) without baking the bg in.
     Vocabulary (per project memory + designsystem.html):
       --brand-tint    →  hover  : 6% brand-600  (subtle, on-style)
       --brand-tint-2  →  active : 12% brand-500 (stronger, ties to
                                    the brand-600 ink + 3px strip
                                    that components add on top)
     ANY component reaching for these tokens automatically picks up
     the right hover/active behavior — no per-component overrides,
     no theme forks. If a hover surface is "the wrong color", the fix
     is to make sure it uses `var(--brand-tint)`, NOT to invent a new
     local value. */
  --brand-tint:   color-mix(in oklab, var(--brand-600)  6%, transparent);
  --brand-tint-2: color-mix(in oklab, var(--brand-500) 12%, transparent);

  /* Status */
  --ok: oklch(0.58 0.14 var(--ok-h));
  --ok-tint: color-mix(in oklch, var(--ok) 12%, var(--bg));
  --ok-soft: oklch(0.96 0.05 var(--ok-h));
  --warn: oklch(0.65 0.14 var(--warn-h));
  --warn-tint: color-mix(in oklch, var(--warn) 16%, var(--bg));
  --warn-soft: oklch(0.97 0.05 var(--warn-h));
  --err: oklch(0.58 0.19 var(--err-h));
  --err-tint: color-mix(in oklch, var(--err) 12%, var(--bg));
  --err-soft: oklch(0.96 0.06 var(--err-h));
  /* Tailwind blue-500 text on a pale blue-50/100 surface. The text
     carries the blue semantic (vivid, hue 256), the bg is light enough
     to read as "calm informational tile" rather than a saturated
     stripe. */
  --info: oklch(0.60 0.20 var(--info-h));
  --info-tint: color-mix(in oklch, var(--info) 12%, var(--bg));
  --info-soft: oklch(0.97 0.03 var(--info-h));

  /* Categorical tokens — promoted from the dashboard-scoped --d-*
     system so global components (charts, KPIs, gender bars) can
     reference them without crossing the dashboard token boundary.
     Hue values are FIXED (not brand-driven) — these are categorical,
     not brand expressions. */
  --indigo:      oklch(0.55 0.18 268);
  --indigo-soft: oklch(0.96 0.04 268);
  --purple:      oklch(0.55 0.18 290);
  --male:        oklch(0.55 0.16 240);
  --female:      oklch(0.62 0.20 350);

  /* Focus ring — uses brand-700 (contrast-clamped) so the ring stays visible
     even when the user picks a near-white brand. */
  --ring: color-mix(in oklch, var(--brand-700) 40%, transparent);
}

/* ─────────── DARK THEME ─────────── */
[data-theme="dark"] {
  color-scheme: dark;

  /* Body surfaces (May 2026 refresh):
       — Lifted lightness across the stack so the page no longer
         reads as pitch-black; the lift is small enough that
         brand-colored elements (charts, hero numbers, status pills)
         still pop against it.
       — Introduced a faint brand chroma (0.005–0.008) tied to the
         active `--brand-h` so the dark UI feels like a designed
         surface rather than off-TV neutral. Apple / Linear / Stripe
         dark themes all use this trick. Chroma is tiny enough that
         a teal vs magenta brand reads as "slightly warm/cool" — not
         as a colored page.
       — Elevation hierarchy preserved: sunken < bg < bg-muted <
         bg-raised, with bigger steps from bg → raised so cards still
         clearly lift off the page. */
  --bg: oklch(0.205 0.006 var(--brand-h));
  --bg-muted: oklch(0.225 0.006 var(--brand-h));
  --bg-raised: oklch(0.245 0.008 var(--brand-h));
  --bg-sunken: oklch(0.16 0.005 var(--brand-h));

  --ink: oklch(0.97 0.005 var(--brand-h));
  --ink-2: oklch(0.82 0.008 var(--brand-h));
  --ink-3: oklch(0.65 0.010 var(--brand-h));
  --ink-4: oklch(0.50 0.012 var(--brand-h));

  --line: oklch(0.28 0.015 var(--brand-h));
  --line-2: oklch(0.33 0.017 var(--brand-h));
  --line-strong: oklch(0.42 0.02 var(--brand-h));

  /* Dark mode tooltip — inverted (light surface on dark page). */
  --tooltip-bg:        oklch(0.94 0.004 250);
  --tooltip-fg:        oklch(0.20 0.010 250);
  --tooltip-fg-muted:  oklch(0.42 0.010 250);
  --tooltip-border:    oklch(0.86 0.005 250);
  --tooltip-shadow:    0 8px 24px -6px rgba(0, 0, 0, 0.55), 0 2px 6px -2px rgba(0, 0, 0, 0.40);

  --brand-50:  oklch(0.24 calc(var(--brand-c) * 0.25) var(--brand-h));
  --brand-100: oklch(0.30 calc(var(--brand-c) * 0.40) var(--brand-h));
  --brand-200: oklch(0.38 calc(var(--brand-c) * 0.60) var(--brand-h));
  --brand-300: oklch(0.48 calc(var(--brand-c) * 0.80) var(--brand-h));
  --brand-400: oklch(0.60 calc(var(--brand-c) * 0.95) var(--brand-h));
  --brand-500: oklch(0.68 var(--brand-c) var(--brand-h));
  --brand-600: oklch(0.74 calc(var(--brand-c) * 0.95) var(--brand-h));
  --brand-700: oklch(0.82 calc(var(--brand-c) * 0.80) var(--brand-h));
  --brand-900: oklch(0.94 calc(var(--brand-c) * 0.3) var(--brand-h));

  --brand-ink: oklch(0.15 0.02 var(--brand-h));
  /* Dark-mode tints intentionally use the SAME definition as light
     mode — the oklab-+-transparent system above is theme-agnostic
     (transparency lets the dark page bg show through naturally).
     Kept here as explicit declarations rather than relying on
     inheritance so the dark-theme block stays self-contained and a
     dev scanning the file sees the same token shape in both blocks. */
  --brand-tint:   color-mix(in oklab, var(--brand-600)  6%, transparent);
  --brand-tint-2: color-mix(in oklab, var(--brand-500) 12%, transparent);

  --ok: oklch(0.80 0.16 var(--ok-h));
  --ok-tint: color-mix(in oklch, var(--ok) 18%, var(--bg));
  --ok-soft: oklch(0.30 0.06 var(--ok-h));
  --warn: oklch(0.78 0.14 var(--warn-h));
  --warn-tint: color-mix(in oklch, var(--warn) 22%, var(--bg));
  --warn-soft: oklch(0.32 0.07 var(--warn-h));
  --err: oklch(0.70 0.18 var(--err-h));
  --err-tint: color-mix(in oklch, var(--err) 20%, var(--bg));
  /* Dark mirror of the Tailwind blue family — brighter text (blue-300
     equivalent), deeper saturated bg so the chip reads as a clear
     blue tile with strong readability against the dark surface. */
  --info: oklch(0.78 0.15 var(--info-h));
  --info-tint: color-mix(in oklch, var(--info) 20%, var(--bg));
  --info-soft: oklch(0.34 0.10 var(--info-h));
  --err-soft: oklch(0.32 0.08 var(--err-h));

  /* Categorical tokens — dark-mode lifts of the light-mode values so
     gender / indigo / purple stay readable on dark surfaces. Same
     rationale as light: hue is fixed, NOT brand-driven. */
  --indigo:      oklch(0.70 0.18 268);
  --indigo-soft: oklch(0.28 0.07 268);
  --purple:      oklch(0.72 0.18 290);
  --male:        oklch(0.68 0.16 240);
  --female:      oklch(0.74 0.20 350);

  --ring: color-mix(in oklch, var(--brand-500) 55%, transparent);
}

/* ─────────── BASE ─────────── */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--ink);
  font-feature-settings: "cv11", "ss01", "ss03";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background var(--t-med), color var(--t-med);
}

[dir="rtl"], [dir="rtl"] body { --font-ui: var(--font-ar); }

/* ─────────────────────────────────────────────────────────────
   NO-TRANSITIONS-DURING-LOAD GUARD
   The inline hydration script in every page's <head> sets
   `class="is-loading"` on the <html> element BEFORE first paint.
   While that class is present, every transition + animation in
   the document is disabled. The class is removed only after
   React's SideNav has mounted (see components.jsx — first
   useEffect in <SideNav>), so the skeleton + React-mount swap
   happens without ANY animated color/background change. This
   kills the perceived "brand color changes while loading" flash:
   even if React's mount re-applies the same brand value via
   useEffect, the CSS transition on body bg/color (200ms) and
   the various component-level transitions can't fire while the
   guard is active.

   Why a class on the html instead of body: the inline hydration
   script runs in <head> before <body> is parsed — so body doesn't
   exist yet. <html> is always present.
   ───────────────────────────────────────────────────────────── */
html.is-loading,
html.is-loading *,
html.is-loading *::before,
html.is-loading *::after {
  transition: none !important;
  animation-duration: 0ms !important;
  animation-delay: 0ms !important;
}
/* Exception — the skeleton shimmer IS the loading affordance. The
   guard above was killing the only animation that's supposed to
   run during the load window, so users saw a static gray block
   instead of the sweeping shimmer. Re-enable just the skeleton
   `::after` animation here so it overrides the `!important` 0ms
   duration above. */
html.is-loading .skeleton::after,
html.is-loading .skel-bar::after,
html.is-loading .skel-line::after,
html.is-loading .skel-circle::after,
html.is-loading .skel-rect::after {
  animation-duration: 1.4s !important;
  animation-delay: 0s !important;
}

/* ─────────────────────────────────────────────────────────────
   CROSS-DOCUMENT VIEW TRANSITIONS
   Smoothly cross-fade between pages instead of the browser's
   default hard-cut. The white flash you'd otherwise see between
   "page A unloads" and "page B first paints" gets replaced by a
   short fade — masks the brand-color/font/skeleton swap that the
   user perceives as flicker during navigation.

   Browser support: Chrome 126+, Edge 126+, Safari 18+. In older
   browsers the @view-transition at-rule is silently ignored and
   navigation falls back to the default hard-cut — no harm done.

   `same-origin` is the default scope (only navigations within
   ux.ivexaminer.ai animate; external links don't). 180ms matches
   the design system's motion vocabulary for chrome transitions.
   ───────────────────────────────────────────────────────────── */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: ease-out;
}
/* Respect reduced-motion — skip the fade for users with the OS
   preference set. The hard-cut is what they implicitly asked for. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 0ms; }
}

/* ─────────────────────────────────────────────────────────────
   REDUCED-MOTION SAFETY NET (a11y)
   When the OS preference is `prefers-reduced-motion: reduce`,
   collapse every animation and transition to ~instant. Component-
   specific rules can still opt-in with their own `no-preference`
   query if they need motion for affordance, but the floor is
   "everything still works without motion".
   Per project_design_system_master.md a11y commitment.
   ───────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:    0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration:   0.001ms !important;
    scroll-behavior:       auto !important;
  }
}

/* ─────────── RTL TYPOGRAPHY BUMP ───────────
   Arabic glyphs (IBM Plex Sans Arabic) read ~1px smaller than Latin
   glyphs at the same nominal size. Historically the codebase
   compensated with ~70 explicit `[dir="rtl"] .foo { font-size: Npx; }`
   overrides scattered across the stylesheet. With the typography
   token system in place, we redefine the body-tier `--fsz-*`
   tokens once at the root RTL scope — every rule using those
   tokens now auto-bumps in Arabic. The explicit per-rule
   overrides become redundant and were deleted.

   Display/h1/h2 are NOT bumped — at 18px+ the size differential
   is negligible and bumping them creates layout pressure on
   page headers and KPI hero numbers.
   ───────────────────────────────────────────────────────────── */
[dir="rtl"], [dir="rtl"] body {
  --fsz-h3:      17px; /* was 16 — RTL bump preserves +1 over LTR */
  --fsz-body:    15px; /* was 14 */
  --fsz-body-sm: 15px; /* aliased to body (was 14) — see LTR comment */
  --fsz-label:   13px; /* was 12 */
  --fsz-caption: 13px; /* aliased to label (was 12) — see LTR comment */
  /* --fsz-micro retired — see token block above */
}

button { font-family: inherit; }
input, select, textarea { font-family: inherit; font-size: inherit; }

/* ─── code / kbd elements ───
   Inline code samples and keyboard-shortcut affordances. Both use
   --font-mono (JetBrains Mono). Background tints prevent them
   from blending into surrounding body prose; subtle border on
   `kbd` makes it read as a discrete keyboard cap. */
code {
  font: var(--fs-code);
  letter-spacing: var(--ls-code);
  background: var(--bg-sunken);
  color: var(--ink);
  padding: 1px 6px;
  border-radius: 4px;
}
kbd {
  font: var(--fs-kbd);
  letter-spacing: var(--ls-kbd);
  background: var(--bg-raised);
  color: var(--ink);
  padding: 2px 7px;
  border: 1px solid var(--line);
  border-block-end-width: 2px;
  border-radius: 5px;
  display: inline-flex;
  align-items: center;
  min-block-size: 18px;
}

/* ─────────── UTILITY ─────────── */
.mono { font-family: var(--font-ui); font-variant-numeric: tabular-nums; letter-spacing: 0; }
.tnum { font-variant-numeric: tabular-nums; }

::selection { background: var(--brand-200); color: var(--brand-900); }

/* Scrollbars */
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border-radius: 999px;
  border: 2px solid var(--bg);
}
*::-webkit-scrollbar-thumb:hover { background: var(--line-strong); }

/* ═══════════════════════════════════════════════════════════════
   SKELETON LOADERS
   Mounted in <div id="root"> on each page BEFORE React boots so the
   user never sees a blank canvas while Babel transpiles and the
   bundle hydrates. React.createRoot.render() then replaces the
   skeleton children with the real app.

   Two primitives:
     .skeleton  — shimmer-animated placeholder block (rounded
                  corners, takes its size from the host element)
     .skel-*    — semantic placeholders (line, bar, circle, rect)
                  for building richer skeletons inline

   The shimmer is a moving linear-gradient ::after that runs purely
   on the GPU (no JS, no layout thrash). Respects prefers-
   reduced-motion — falls back to a static muted background.
   ═══════════════════════════════════════════════════════════════ */
.skeleton,
.skel-bar, .skel-line, .skel-circle, .skel-rect {
  position: relative;
  overflow: hidden;
  background: var(--line);
  border-radius: 8px;
}
.skeleton::after,
.skel-bar::after, .skel-line::after, .skel-circle::after, .skel-rect::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0,
    color-mix(in oklab, white 35%, transparent) 50%,
    transparent 100%
  );
  animation: skel-shimmer 1.4s linear infinite;
}
[data-theme="dark"] .skeleton::after,
[data-theme="dark"] .skel-bar::after,
[data-theme="dark"] .skel-line::after,
[data-theme="dark"] .skel-circle::after,
[data-theme="dark"] .skel-rect::after {
  /* Peak opacity bumped 8% → 18% so the sweep is actually visible
     against the now-tinted dark surface. 8% was inherited from the
     earlier pitch-black dark mode; against the new lifted bg it was
     reading as no animation at all. 18% matches the perceived
     contrast of the light-mode shimmer (35% white on light gray). */
  background: linear-gradient(
    90deg,
    transparent 0,
    color-mix(in oklab, white 18%, transparent) 50%,
    transparent 100%
  );
}
@keyframes skel-shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
[dir="rtl"] .skeleton::after,
[dir="rtl"] .skel-bar::after,
[dir="rtl"] .skel-line::after,
[dir="rtl"] .skel-circle::after,
[dir="rtl"] .skel-rect::after {
  animation-direction: reverse;
}
@media (prefers-reduced-motion: reduce) {
  .skeleton::after,
  .skel-bar::after, .skel-line::after, .skel-circle::after, .skel-rect::after {
    animation: none;
    background: color-mix(in oklab, var(--line-strong) 50%, var(--line));
  }
}
.skel-line   { block-size: 12px; border-radius: 4px; }
.skel-line.is-sm   { block-size: 8px; }
.skel-line.is-lg   { block-size: 16px; }
.skel-bar    { block-size: 6px; border-radius: 3px; }
.skel-circle { border-radius: 50%; }
.skel-rect   { border-radius: 10px; }
/* Generic "page boots" container — used by every page entry's
   pre-React skeleton. Top padding + max-width matching the real
   shells so the swap to React doesn't cause a visible reflow. */
.page-skeleton {
  max-width: 1280px;
  margin: 0 auto;
  padding: 24px 24px 80px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.page-skeleton-topnav {
  block-size: 56px;
  border-radius: 0;
  margin-block-end: 16px;
}
