/* ──────────────────────────────────────────────────────────────
   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. */
  /* Cobalt #2563EB — the iVE system default. Values match the
     `TWEAK_DEFAULTS.brand` block in every page's React mount, so a
     user with no localStorage brand sees the same colour from
     pre-paint hydration through the React commit. */
  --brand-h: 263;           /* hue 0..360 */
  --brand-c: 0.215;         /* chroma */
  --brand-l: 0.546;         /* 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. */
  /* Border color tuned with the operator on the yardlive page: 75/25
     mix of --line and --line-2 — a very small step above the default
     hairline that gives every table-card a slightly more defined
     frame without sliding into a heavy "boxed" look. Width bumps to
     1.5px on `.table-card` (see page.css) for the same reason. Both
     are now system-wide defaults — saved tests, reports, dashboards,
     system monitoring, etc. all inherit. */
  --tbl-frame-border:  color-mix(in oklab, var(--line) 75%, var(--line-2));
  --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);

  /* ─── CTA system (heights + icon sizes) ──────────────────────
     Three-tier height scale covering ~95% of button-like
     affordances. The canonical `.btn` system (page.css ~line 2782)
     still drives color variants; these tokens normalize the
     dimensional axis so scoped CTAs (`.ylc-*`, `.ylt-*`, `.dash-*`,
     `.alert-*`, etc.) stop drifting on padding/height/gap.

     Tiers and use cases:
       • sm (28px) — compact row controls, table action buttons,
                     modal close/back, pagination, toast actions.
                     Default padding: 4 × 10. Gap: 4. Icon: --icon-sm.
       • md (32px) — default for everything else: toolbar actions,
                     filter-bar icon buttons, card L1/L2 CTAs, topnav
                     icons, scoped page CTAs. Padding: 6 × 12. Gap: 6.
                     Icon: --icon-md.
       • lg (40px) — page-level / hero-form submits, primary CTAs
                     on landing surfaces. Padding: 10 × 16. Gap: 8.
                     Icon: --icon-lg.

     L2 text-link CTAs (transparent bg, no border, brand text)
     pin to `--cta-h-md` via `min-block-size` so they don't clip
     below their toolbar siblings. See `project_design_system_master.md`
     for the full standard.

     Icon sizes pair with the height tiers; --icon-xs is for
     inline-with-text (chips, badges, label glyphs). */
  --cta-h-sm: 28px;
  --cta-h-md: 32px;
  --cta-h-lg: 40px;
  --icon-xs:  12px;
  --icon-sm:  14px;
  --icon-md:  16px;
  --icon-lg:  20px;
}

/* ──────────────────────────────────────────────────────────────
   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 with a whisper brand tint.
     Two-tier chroma: body surfaces (--bg / -muted / -sunken) sit at
     0.001 — basically imperceptible — while raised surfaces
     (--bg-raised, used for sidebar / cards / popovers) sit at 0.002,
     giving them a barely-there lift in the brand-hue family without
     painting the whole page in brand color. Previous flat 0.002
     across all four felt slightly too colored on body in some
     viewports — this two-tier approach keeps brand coherence on the
     "designed" surfaces while letting the body breathe more neutral.
     --bg-raised lands at L=0.995 so there's headroom for chroma at
     near-white. */
  --bg: oklch(0.975 0.001 var(--brand-h));
  --bg-muted: oklch(0.965 0.001 var(--brand-h));
  --bg-raised: oklch(0.995 0.002 var(--brand-h));
  --bg-sunken: oklch(0.955 0.001 var(--brand-h));

  /* Ink */
  --ink: oklch(0.22 0.02 var(--brand-h));
  --ink-2: oklch(0.38 0.015 var(--brand-h));
  /* ink-3 darkened 0.55 → 0.51 to clear WCAG AA 4.5:1 for muted metadata
     (maneuver duration, violation points/time, relative timestamps) which
     measured 4.26:1 at 0.55. Still clearly subordinate to ink/ink-2. */
  --ink-3: oklch(0.51 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));

  /* Skeleton base — deliberately brand-NEUTRAL (chroma 0). Skeletons
     should read as "loading-system grey", not a tinted preview of
     the tenant brand. Both modes carry zero hue so the placeholder
     doesn't bleed brand color into the content it stands in for. */
  --skeleton-base: oklch(0.92 0 0);

  /* 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));

  /* Primary-CTA fill. The CTA carries white (--brand-ink) text, so the fill
     must stay dark enough for that text to clear WCAG AA (4.5:1). Rather than
     swap the text to dark on light brands (a per-tenant text exception), we
     bend the FILL: clamp its lightness to the lightest a brand can be while
     white text still passes. Dark brands (navy 0.33, indigo 0.55) sit below
     the ceiling untouched — they render their EXACT brand; only a light brand
     (EDC orange 0.70) is pulled down, by the minimum amount. Ceiling tuned to
     hue ~45 (orange = brightest current brand, the worst case for white text);
     a future yellow/lime brand would want it lowered. */
  --brand-cta-ceil: 0.57;
  --brand-cta: oklch(
    min(var(--brand-l), var(--brand-cta-ceil))
    var(--brand-c)
    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));

  /* Brake red — the live-test safety brake's solid-red fill. Light = the
     standard error red; dark deepens it (see the dark block). Scoped to
     the brake so the global --err / .btn-danger stay unchanged. */
  --brake-red: var(--err);

  /* Modal header tint — a subtle wash over the modal surface
     (--bg-raised) so every modal header reads as a branded band.
     Tones swap the hue for semantic modals (danger / warn / success).
     The border carries a touch more of the same hue so the head reads
     as one cohesive strip. Kept light (≤9%) so title text stays ≥4.5:1.
     Dark pairing lives in the [data-theme="dark"] block below. */
  --modal-head-bg:           color-mix(in oklab, var(--brand-500) 7%, var(--bg-raised));
  --modal-head-line:         color-mix(in oklab, var(--brand-500) 16%, var(--line));
  --modal-head-bg-danger:    color-mix(in oklab, var(--err) 8%, var(--bg-raised));
  --modal-head-line-danger:  color-mix(in oklab, var(--err) 20%, var(--line));
  --modal-head-bg-warn:      color-mix(in oklab, var(--warn) 9%, var(--bg-raised));
  --modal-head-line-warn:    color-mix(in oklab, var(--warn) 20%, var(--line));
  --modal-head-bg-success:   color-mix(in oklab, var(--ok) 8%, var(--bg-raised));
  --modal-head-line-success: color-mix(in oklab, var(--ok) 20%, var(--line));
  /* 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 — Option B "dark grey" refresh):
       — Lifted ~+0.05 L across the stack so the page reads as a
         soft dark grey (Notion / Claude territory), not pitch black.
         Earlier values (0.205 / 0.245) were technically not black
         but the surface stacking compounded into a heavy near-black
         feel. The +0.05 lift keeps the elevation hierarchy intact
         while moving the body firmly out of "off-TV neutral".
       — Faint brand chroma (0.005–0.008) tied to the active
         `--brand-h` so the dark UI feels like a designed surface.
         Apple / Linear / Stripe / Claude all use this trick.
       — Elevation hierarchy preserved: sunken < bg < bg-muted <
         bg-raised, with bigger steps from bg → raised so cards still
         clearly lift off the page. */
  /* Dark — two-tier chroma matching light. Body surfaces at 0.002,
     raised at 0.003 so the sidebar gets slightly more brand presence
     than the body, but both stay in "barely visible" territory.
     Previously flat 0.003-0.008 made dark mode read as much more
     tinted than light. */
  /* Chroma tightened to match the light-mode "whisper of brand hue"
     vocabulary (light --bg uses C 0.001, --bg-raised uses C 0.002).
     Previous dark values were C 0.002 / 0.002 / 0.003 — slightly
     more saturated than light, making dark feel cooler / more
     blue-tinted than the pleasingly-neutral light mode. Bringing
     them in line so both modes carry the same fractional-percent
     of brand chroma. */
  --bg: oklch(0.255 0.001 var(--brand-h));
  --bg-muted: oklch(0.275 0.001 var(--brand-h));
  --bg-raised: oklch(0.295 0.002 var(--brand-h));
  --bg-sunken: oklch(0.21 0.002 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));

  /* Lines lift in lockstep with surfaces — the previous 0.28 line
     became near-invisible against a 0.295 raised surface. New 0.36
     keeps the same hairline contrast as before (~0.07 above the
     raised body) so cards and inputs still have defined edges. */
  --line: oklch(0.36 0.015 var(--brand-h));
  --line-2: oklch(0.41 0.017 var(--brand-h));
  --line-strong: oklch(0.50 0.02 var(--brand-h));

  /* Skeleton base — dark mode, also brand-neutral (chroma 0). Same
     lightness as --line so visual mass stays consistent; the only
     change is dropping the brand-hue tint that read as a colored
     loader against the dark surface. */
  --skeleton-base: oklch(0.36 0 0);

  /* 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));
  --brand-cta: var(--brand-500); /* dark mode: brand-500 is a light ~0.68 fill carrying dark --brand-ink text — already clears AA, no clamp needed */
  /* 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));

  /* Brake red (dark) — a deeper, more saturated true red than the
     lightened dark --err (which reads coral/pink on the brake's large
     fill), keeping strong white-on-red contrast. */
  --brake-red: oklch(0.56 0.21 var(--err-h));

  /* Modal header tint — dark pairing. Higher mix % than light because a
     faint wash disappears on a dark surface; tuned so the band is clearly
     visible without overpowering the title. */
  --modal-head-bg:           color-mix(in oklab, var(--brand-500) 14%, var(--bg-raised));
  --modal-head-line:         color-mix(in oklab, var(--brand-500) 26%, var(--line));
  --modal-head-bg-danger:    color-mix(in oklab, var(--err) 15%, var(--bg-raised));
  --modal-head-line-danger:  color-mix(in oklab, var(--err) 30%, var(--line));
  --modal-head-bg-warn:      color-mix(in oklab, var(--warn) 15%, var(--bg-raised));
  --modal-head-line-warn:    color-mix(in oklab, var(--warn) 30%, var(--line));
  --modal-head-bg-success:   color-mix(in oklab, var(--ok) 15%, var(--bg-raised));
  --modal-head-line-success: color-mix(in oklab, var(--ok) 30%, var(--line));

  /* 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 */
  /* Arabic is cursive — letter-spacing splits the connected glyphs, so
     every tracking token collapses to 0 in RTL. Token-driven components
     (letter-spacing: var(--ls-*)) auto-correct; raw per-component
     letter-spacing values are caught by the global reset just below.
     --ls-body / -body-sm / -body-strong / -caption / -code are already
     0, so only the positive/negative ones need zeroing here. */
  --ls-display: 0; --ls-h1: 0; --ls-h2: 0; --ls-h3: 0;
  --ls-label:   0; --ls-kbd: 0;
}

/* ─── RTL letter-spacing neutralizer ───────────────────────────────
   Arabic must never be letter-spaced — tracking breaks the cursive
   joins (the canvas brake hint, telemetry labels, violation-table
   headers, and sidenav/rail/strip eyebrows were all rendering Arabic
   with stretched, disconnected letters). This catches the raw
   per-component `letter-spacing` values that bypass the --ls-* tokens
   above. The :not() chain lifts specificity so it reliably overrides
   component rules without !important. LTR-isolated runs opt out — the
   LTR-locked media bar + bidi-isolated mixed strings (dir="ltr") and
   mono / tabular numerics keep their intended tracking. */
[dir="rtl"] *:not([dir="ltr"]):not(.mono):not(.tnum):not(.ident) { letter-spacing: 0; }

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; }
/* `.ident` — IDENTIFIER strings you SCAN / MATCH rather than do math on:
   test IDs, plate numbers, request / reference numbers, EIDs, report IDs.
   Uses the REAL monospace face (--font-mono / JetBrains Mono) for crisp,
   evenly-aligned glyphs, sized 0.92em so the mono (which runs large per-px)
   optically matches adjacent sans labels and doesn't grow the line.
   Deliberately distinct from `.mono`, which is the SANS UI font + tabular
   digits for QUANTITIES (counts, %, durations, prices, KPI figures). */
.ident { font-family: var(--font-mono); font-size: 0.92em; font-variant-numeric: tabular-nums; letter-spacing: 0; }

::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.
   ═══════════════════════════════════════════════════════════════ */

/* Screen-reader-only — visually hidden but exposed to assistive tech.
   Used for heading structure on app-like surfaces (e.g. the test canvas)
   where a visible <h1>/<h2> would disrupt the dense layout. Standard clip
   pattern. */
.sr-only {
  position: absolute !important;
  inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skeleton,
.skel-bar, .skel-line, .skel-circle, .skel-rect {
  position: relative;
  overflow: hidden;
  background: var(--skeleton-base);
  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 {
    /* Static fallback when motion is reduced — neutral darker grey
       on top of --skeleton-base, keeping the placeholder brand-free. */
    animation: none;
    background: color-mix(in oklab, black 12%, var(--skeleton-base));
  }
}
.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;
}
