/* Design tokens — the single source of truth.
   Loaded BEFORE app.css / modern.css / design-system-v2.css, which from here
   on contain component rules only. Adding a token declaration to any other
   stylesheet reintroduces the override chain this file exists to remove.

   Consolidated from app.css (119 custom properties), modern.css (17) and
   design-system-v2.css (103). Where a token name was declared more than once,
   the value kept here is the one that was live under the original load order
   (app.css -> modern.css -> design-system-v2.css, last declaration wins at
   equal selector specificity). See task-2-report.md for the few tokens whose
   winning value required resolving a second override within the same file. */
:root {
  /* Core brand palette (app.css) */
  --brand-teal: #00C0A5;
  --brand-cyan: #0190D5;
  --brand-blue: #055EB8;
  --brand-gradient: linear-gradient(135deg, #00C0A5 0%, #0190D5 50%, #055EB8 100%);
  --brand-gradient-reverse: linear-gradient(135deg, #055EB8 0%, #0190D5 50%, #00C0A5 100%);
  --brand-accent-gradient: linear-gradient(135deg, #00C0A5 0%, #0190D5 100%);
  --brand-gradient-soft: linear-gradient(135deg, rgba(0,192,165,0.16), rgba(1,144,213,0.18) 50%, rgba(5,94,184,0.16));

  /* Brand glows */
  --teal-glow: rgba(0, 192, 165, 0.12);
  --teal-glow-strong: rgba(0, 192, 165, 0.25);
  --blue-glow: rgba(1, 144, 213, 0.12);
  --blue-glow-strong: rgba(1, 144, 213, 0.25);

  /* Accent aliases (existing CSS/JS uses --accent broadly) */
  --accent: #00C0A5;
  --accent-strong: #0190D5;
  --accent-deep: #055EB8;
  --accent-soft: rgba(0, 192, 165, 0.12);
  --accent-glow: rgba(0, 192, 165, 0.30);

  /* Warm accent - Gold */
  --gold: #F5B041;
  --gold-deep: #E89B2C;
  --gold-gradient: linear-gradient(135deg, #F5B041, #E89B2C);
  --gold-soft: rgba(245, 176, 65, 0.12);
  --gold-glow: rgba(245, 176, 65, 0.30);

  /* Special-purpose accents (use 1-3 per page max) */
  --amber: #F59E0B;
  --amber-soft: rgba(245, 158, 11, 0.13);
  --orange: #F97316;
  --orange-soft: rgba(249, 115, 22, 0.13);
  --purple: #8B5CF6;
  --purple-light: #A78BFA;
  --purple-soft: rgba(139, 92, 246, 0.14);

  /* Status */
  --green: #10B981;
  --green-soft: rgba(16, 185, 129, 0.13);
  --red: #EF4444;
  --red-soft: rgba(239, 68, 68, 0.12);

  /* ======================================================================
     THE ACTION COLOUR AND THE IDENTITY ACCENT (task 5)

     A theme is TWO colours, because they do two different jobs:

       THE ACTION COLOUR  --primary-fill / --primary-ink
         A button fill that CARRIES A LABEL. It must clear WCAG AA 4.5:1
         against that label, in light AND in dark. Everything filled and
         clickable resolves through it: .btn-primary, .seg-btn.is-active,
         .filter-pill.is-active, the painted nav section header, the chat
         composer's send/drive buttons, the calendar cell's add button.

       THE IDENTITY ACCENT  --brand  (and its long-standing alias --accent)
         The nav marker stripe, the avatar ring, the active-tab underline.
         It NEVER carries text, so it can stay bright. It is also never the
         sole carrier of meaning: it decorates a state that something else
         already states. (The focus ring used to be drawn from it and is
         NOT any more — see THE FOCUS RING below. A focus ring has no
         redundant carrier, so it is a UI indicator with a real 3:1 floor,
         not a decoration.)

     That split is the whole point. Logic8 teal #00C0A5 is 2.31:1 against
     white — unusable as a fill, perfectly good as a marker. Before task 5
     the two jobs shared one token and the teal lost, so every filled
     control had already been hand-migrated to `var(--brand-deep,
     var(--accent-deep))`. This block names that pattern once.

       --accent-ink  is the third case: the accent WHERE IT CARRIES TEXT
       (the active nav row's label and icon, the PWA chat skin's links).
       On a light ground a bright accent is illegible, so it resolves to
       the action colour; on a dark ground the bright accent is already
       legible and stays itself.

     WHY THE `-light` PAIR EXISTS. The installed-PWA chat skin paints its
     own white surface regardless of `data-theme`, so it cannot inherit a
     dark-mode action colour (near-white on white). Every theme therefore
     states its light-ground values under `--primary-*-light`, which the
     shared `--primary-*` point at by default and `[data-theme='dark']`
     overrides. A surface that pins its own ground reads the `-light` pair
     directly.

     DEFAULTS ARE DELIBERATELY INERT. With no `data-brand` set, the action
     colour is still `var(--brand-deep, var(--accent-deep))` — the brand
     ramp's deep stop, the only stop that clears 4.5:1 against white
     (#055EB8 = 6.36:1) and what every one of these rules already used.
     Adding the token layer changed no pixel; the themes below are opt-in.
     ====================================================================== */
  --primary-fill-light:   var(--brand-deep, var(--accent-deep));
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  color-mix(in srgb, var(--primary-fill-light) 88%, #000);
  --primary-active-light: color-mix(in srgb, var(--primary-fill-light) 80%, #000);

  --primary-fill:   var(--primary-fill-light);
  --primary-ink:    var(--primary-ink-light);
  --primary-hover:  var(--primary-hover-light);
  --primary-active: var(--primary-active-light);

  /* The identity accent. --accent is the historical name and stays the one
     the component rules use; --brand is the name a theme sets. */
  --brand: var(--accent);

  /* The accent where it carries text. On a light ground that is the action
     colour taken one step darker: the action colour itself is tuned against
     plain white, but this text sits on a 12-22% accent TINT of white, which is
     darker than white and eats roughly a fifth of the ratio. */
  --accent-ink-light: color-mix(in srgb, var(--primary-fill-light) 86%, #000);
  --accent-ink: var(--accent-ink-light);

  /* ----------------------------------------------------------------------
     SEMANTIC ACTION COLOURS — destructive and confirming fills.

     `--red` and `--green` are PALETTE values: they are what a status dot, a
     soft tint or a chart series is painted with, and they are tuned to be
     seen on a surface, not to carry a white label. Measured, they do not:
     #EF4444 with white text is 3.76:1 and #10B981 is 2.54:1, both below AA.

     A fill that carries a label is a different colour from a fill that does
     not, exactly as with the action colour above. These darken the palette
     hue until the white label clears AA, and stay derived from `--red` /
     `--green` so a white-label owner's configured danger/success colour
     still reaches the buttons. The mix ratios were chosen against the
     shipped palette (5.51:1 and 5.34:1) with headroom for a lighter
     configured hue; a hue lighter than roughly #4FE0A8 would still need
     hand-checking.

     One value serves both modes. A darker fill's ratio against its own
     label does not change with the canvas, and the darkened hues still read
     at over 3:1 against the dark canvas, so there is no mode-flip here.
     ---------------------------------------------------------------------- */
  --danger-fill:   color-mix(in srgb, var(--red) 80%, #000);
  --danger-ink:    #FFFFFF;
  --danger-hover:  color-mix(in srgb, var(--red) 70%, #000);
  --danger-active: color-mix(in srgb, var(--red) 62%, #000);

  --success-fill:   color-mix(in srgb, var(--green) 66%, #000);
  --success-ink:    #FFFFFF;
  --success-hover:  color-mix(in srgb, var(--green) 58%, #000);
  --success-active: color-mix(in srgb, var(--green) 52%, #000);

  /* ----------------------------------------------------------------------
     THE PALETTE HUES WHERE THEY CARRY TEXT.

     Exactly the --accent / --accent-ink split, applied to the status
     palette. `--green`, `--red`, `--purple`, `--amber`, `--orange` and
     `--gold` are tuned to be SEEN — as a dot, a soft tint, a border, a
     chart series. They were also being used as the foreground of the
     canonical status pills, and measured against the very tint they sit on
     they do not clear AA on a light ground: .pill-green was 2.25:1,
     .pill-red 3.23, .pill-purple 3.56, .pill-amber 4.08, .pill-orange 4.21,
     and `.pill.status-active` (green on --green-soft) was the worst
     measured pill in the app at 2.20:1. Because these are the DESIGN
     SYSTEM'S OWN pills, the failure showed up on 13 routes at once.

     ON A DARK GROUND THESE ARE DELIBERATELY UNCHANGED. Each --*-ink here
     resolves to the palette hue itself, so dark mode is not merely
     "verified unaffected" — it computes to the identical value it always
     did. Only [data-theme='light'] below re-points them.

     Like --danger-fill/--success-fill, they stay DERIVED from the palette
     hue rather than being fresh literals, so a white-label owner's
     configured success/danger colour still reaches the pills.
     ---------------------------------------------------------------------- */
  --green-ink:  var(--green);
  /* Two exceptions to the pass-through, both measured. Every --*-ink is
     checked against the tint it actually sits on, composited over the
     LIGHTEST dark surface (--bg-card-hover #182040) -- that is the worst
     case, and a 12-14% tint eats about a fifth of the ratio. Four of the six
     clear 4.5 as the raw hue: green 5.11, amber 5.97, orange 4.81, gold 6.60.
     Red lands at 3.81 and purple at 3.21, which is what the audit measured on
     /dashboard's "Needs attention" count and /projects' purple pills. Lifting
     each 25% toward white takes red to 5.11 and purple to 4.77 while leaving
     it derived from the palette hue, so a white-label owner's configured
     danger colour still reaches the pill. */
  --red-ink:    color-mix(in srgb, var(--red) 75%, #FFFFFF);
  --purple-ink: color-mix(in srgb, var(--purple) 75%, #FFFFFF);
  --amber-ink:  var(--amber);
  --orange-ink: var(--orange);
  --gold-ink:   var(--gold);

  /* Surfaces */
  --bg-base: #070B16;
  --bg-elevated: #0C1424;
  --bg-card: #11192C;
  --bg-card-hover: #182040;
  --bg-input: #0A1124;
  --bg-overlay: rgba(7, 11, 22, 0.82);
  --bg-subtle: #0E1626;

  --border: #1C2440;
  --border-strong: #2A3354;
  --border-focus: var(--brand-teal);

  /* Text */
  --text-primary: #FFFFFF;
  --text-body: #E2E8F0;
  --text-secondary: #94A3B8;
  /* THE DARK MUTED. This value and the one in [data-theme='light'] were the
     SAME hex (#64748B) until the 2026-07-29 audit: it had been tuned against
     white, where it measures 4.76:1, and reused unchanged on the dark ground
     where it only reaches 3.83:1. That single reuse was 324 measured AA
     failures across all 28 routes — every .muted, .label, .lv-sort header,
     .kpi-band-label, .stat-card-label, kbd and the footer build tag — the
     largest defect in the app.

     #8A97AB is measured, not picked. Against every dark ground it can land
     on it clears 4.5:1 with margin: 6.16:1 on the composited page ground,
     6.59:1 on --au-canvas, 6.00:1 on --au-surface, and 5.38:1 on the worst
     case (--bg-card-hover #182040, the lightest dark surface in the set).
     It stays BELOW --text-secondary #94A3B8 (5.38 vs 6.21 worst-case), so
     muted still reads as the quieter of the two — the fix restores the
     legibility floor without collapsing the type hierarchy.

     If you retune this, re-measure against --bg-card-hover, not white. */
  --text-muted: #8A97AB;
  --text-inverse: #0F172A;

  /* Shadows — LEGACY. Task 4D moved every stylesheet rule onto --shadow-pop /
     --shadow-overlay (declared further down), so none of the --shadow-*,
     --shadow-soft-*, --v2-shadow-*, --au-shadow-* or --sx-sh-* values below are
     read by any CSS rule any more. They stay declared because inline styles in
     public/js/pages/{people,person-detail}.js still name --shadow-soft-lg, and
     because backend/routes/brand-settings.js re-declares --au-shadow-md/-lg per
     skin. Do not reach for one in new CSS; use a z-lift token. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.36), 0 2px 4px rgba(0, 0, 0, 0.2);
  --shadow-lg: 0 24px 48px rgba(0, 0, 0, 0.55), 0 8px 16px rgba(0, 0, 0, 0.3);
  --shadow-glow: 0 0 0 1px rgba(0, 192, 165, 0.20), 0 12px 36px rgba(0, 192, 165, 0.18);
  --shadow-brand: 0 10px 28px rgba(1, 144, 213, 0.30);
  --shadow-card-glow: 0 0 0 1px rgba(0, 192, 165, 0.10), 0 16px 40px rgba(1, 144, 213, 0.10);

  /* Radii — flat geometry (task 4C): 3-6px everywhere. See the ROLE RADII
     block near the bottom of this file for what each role resolves to.
     These two survive only because inline styles in public/js/pages/*.js name
     them; both draw small panels, so both follow the surface radius. */
  --r-sm: var(--r-card);
  --r-md: var(--r-card);

  /* Transitions */
  --t-fast: 130ms cubic-bezier(0.4, 0, 0.2, 1);
  --t-base: 220ms cubic-bezier(0.4, 0, 0.2, 1);
  --t-slow: 360ms cubic-bezier(0.4, 0, 0.2, 1);

  /* ======================================================================
     FONTS (task 4E) — ONE UI family + ONE mono, and that is the whole set.
     public/index.html loads exactly these two from Google Fonts:
       Inter          400 500 600 700 800   (the UI family)
       JetBrains Mono 400                   (the mono family)
     Plus Jakarta Sans and Space Grotesk were removed — every heading,
     KPI value and nav label that named one now resolves to --font-sans.
     A white-label skin still retunes the display face through
     --brand-font-heading (backend/routes/brand-settings.js injects it and
     its @import); the fallback behind it is --font-sans, never a third
     hardcoded family. Adding a family here means adding a webfont
     download to first paint — don't, without measuring it.
     ====================================================================== */
  --font-sans: 'Inter', 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;

  /* THE BODY TEXT METRIC — 14px on 24px leading (task 4E).
     Declared on <body> in app.css and inherited from there; a component that
     re-declares font-size is opting OUT of the app's type scale and needs a
     reason.

     --lh-body is a RATIO, not a length, and deliberately so: 1.714 x 14px is
     exactly the 24px leading the spec asks for, but line-height inherits, and
     a fixed `24px` would hand every 10-12px caption, tag and uppercase label
     in the app a 24px line box — every pill and chip in the UI would grow
     ~6px taller. The ratio gives body copy its 24px and lets small text keep
     proportional leading. */
  --fs-body: 14px;
  --lh-body: 1.714;   /* 14px x 1.714 = 24.0px */

  /* Logo avatar surface (app.css - Logic8 co-pilot chat panel) */
  --logo-bg: #111827;          /* deep charcoal */
  --logo-bg-hover: #1F2937;
  --logo-ring: rgba(255,255,255,0.06);

  /* THE RADIUS SCALE — and the white-label radius API.
     backend/routes/brand-settings.js re-declares exactly these names (plus the
     four --au-r-* below) per theme family, after every stylesheet, so a skin
     can retune the app's geometry. Every role token at the bottom of this file
     derives from one of them, which is what gives a skin its reach. */
  --r-xs: 4px;      /* inline labels: tags, code, calendar events */
  --r-input: 4px;   /* text fields */
  --r-btn: 4px;     /* buttons and other controls */
  --r-card: 6px;    /* surfaces: cards, tiles, panels, popovers, menus */
  --r-modal: 6px;   /* modals and drawers */

  /* Softer, layered shadows (less colored glow, more neutral depth) */
  --shadow-soft-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 1px rgba(15, 23, 42, 0.03);
  --shadow-soft-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.04);
  --shadow-soft-lg: 0 16px 40px rgba(15, 23, 42, 0.12), 0 4px 8px rgba(15, 23, 42, 0.06);
  --shadow-soft-xl: 0 32px 64px rgba(15, 23, 42, 0.18), 0 8px 16px rgba(15, 23, 42, 0.08);

  /* ======================================================================
     Z-LIFT SHADOWS (task 4D) — the only two shadows the app still uses.
     Everything that sits IN the page (cards, tiles, tables, buttons, KPI
     decks) gets its depth from a 1px hairline. A shadow now means one
     thing: this thing floats ABOVE the page and must read as detached.

       --shadow-pop      menus, dropdowns, popovers, tooltips, pickers
       --shadow-overlay  modals, drawers, sheets, toasts, the chat panel

     Deliberately NEUTRAL. A brand tint here is a glow, which is exactly
     what task 4D removed. Retune the app's whole sense of depth in this
     block — every floating layer resolves through one of these two.
     :root carries the light-theme values; [data-theme='dark'] overrides,
     the same shape as the --au-shadow-* scale above.
     ====================================================================== */
  --shadow-pop:     0 2px 6px rgba(16, 24, 48, 0.06), 0 8px 20px -6px rgba(16, 24, 48, 0.14);
  --shadow-overlay: 0 4px 10px rgba(16, 24, 48, 0.07), 0 24px 56px -16px rgba(16, 24, 48, 0.24);

  /* Legacy focus ring. --au-ring below is the one the components read; this
     alias survives for any inline style that still names it, and points at the
     same two-layer construction so it cannot be the odd one out. */
  --focus-ring: var(--au-ring);

  /* Refined easing curves */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* v2 tokens (design-system-v2.css) - restrained shadows */
  --v2-shadow-1: 0 1px 0 rgba(255,255,255,0.04) inset, 0 1px 2px rgba(0,0,0,0.30);
  --v2-shadow-2: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px -8px rgba(0,0,0,0.45);
  --v2-shadow-3: 0 1px 0 rgba(255,255,255,0.04) inset, 0 20px 48px -16px rgba(0,0,0,0.55);
  --v2-shadow-hero: 0 1px 0 rgba(255,255,255,0.05) inset, 0 30px 80px -30px rgba(1,144,213,0.35);

  /* Glass surfaces */
  --v2-glass-bg: color-mix(in srgb, var(--bg-elevated) 78%, transparent);
  --v2-glass-blur: saturate(180%) blur(20px);

  /* Hero gradient - diagonal brand-soft wash */
  --v2-hero-bg:
    radial-gradient(900px 320px at 8% 0%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 60%),
    radial-gradient(700px 280px at 96% 110%, color-mix(in srgb, var(--accent-deep) 18%, transparent), transparent 60%),
    linear-gradient(180deg, color-mix(in srgb, var(--bg-elevated) 96%, transparent), var(--bg-elevated));

  /* Semantic delta colors */
  --v2-up-bg: color-mix(in srgb, var(--green) 14%, transparent);
  --v2-up-fg: var(--green);
  --v2-down-bg: color-mix(in srgb, var(--red) 14%, transparent);
  --v2-down-fg: var(--red);
  --v2-flat-bg: color-mix(in srgb, var(--text-muted) 12%, transparent);
  --v2-flat-fg: var(--text-secondary);

  /* Density */
  --v2-tile-pad: var(--sp-5) var(--sp-6);        /* was 22px 24px */
  --v2-tile-pad-lg: var(--sp-6) var(--sp-7);     /* was 26px 28px */
  --v2-bento-gap: 16px;

  /* Hairline divider */
  --v2-hairline: color-mix(in srgb, var(--border) 70%, transparent);

  /* Aurora ("au-") tokens - brand-derived accent stops. EVERYTHING vibrant is
     built from the owner's brand palette (--brand-primary/--brand-accent/
     --brand-deep, which /api/brand-settings/css injects) via color-mix, so the
     whole UI re-tints automatically when the owner changes their brand colour
     in Settings. We do NOT hardcode any decorative hue. --brand-* fall back to
     the legacy --accent ramp if the brand CSS hasn't loaded yet. */
  --au-b1: var(--brand-primary, var(--accent));        /* primary stop  */
  --au-b2: var(--brand-accent,  var(--accent-strong));  /* mid stop      */
  --au-b3: var(--brand-deep,    var(--accent-deep));    /* deep stop     */
  /* Tonal variations of the SAME brand hue for KPI-card variety */
  --au-b-light: color-mix(in srgb, var(--au-b1) 78%, #ffffff);  /* lighter brand */
  --au-b-deep:  color-mix(in srgb, var(--au-b3) 80%, #0b1020);   /* darker brand  */
  --au-b-warm:  color-mix(in srgb, var(--au-b2) 72%, var(--brand-success, var(--green))); /* brand shifted toward success */
  --au-b-cool:  color-mix(in srgb, var(--au-b3) 78%, var(--brand-info, var(--accent-strong))); /* brand shifted toward info */

  /* Layered surfaces. :root values target the (default) light theme. */
  --au-canvas: #F4F6FB;
  --au-surface: #FFFFFF;
  --au-surface-2: #F7F9FC;
  --au-hairline: color-mix(in srgb, var(--border) 78%, transparent);

  /* Radii — the other half of the white-label radius API (see the scale above) */
  --au-r-card: var(--r-card);
  --au-r-card-lg: var(--r-card);
  --au-r-control: var(--r-btn);
  --au-r-control-sm: 3px;

  /* Layered soft shadow scale (tuned for light fintech) */
  --au-shadow-xs: 0 1px 2px rgba(16,24,48,0.05);
  --au-shadow-sm: 0 1px 2px rgba(16,24,48,0.05), 0 2px 6px rgba(16,24,48,0.05);
  --au-shadow-md: 0 2px 4px rgba(16,24,48,0.04), 0 10px 24px -8px rgba(16,24,48,0.14);
  --au-shadow-lg: 0 4px 8px rgba(16,24,48,0.05), 0 24px 48px -16px rgba(16,24,48,0.22);
  --au-shadow-xl: 0 8px 16px rgba(16,24,48,0.06), 0 40px 80px -24px rgba(16,24,48,0.30);
  /* ======================================================================
     THE FOCUS RING — the one indicator that has no redundant carrier.

     WHAT WAS WRONG. --au-ring was a single 3px band of the accent at 22%
     alpha. Composited against the surface it sits on, that measured
     1.22-1.51:1 in every brand and both modes (ink 1.22 light / 1.51 dark,
     harbour 1.30/1.37, forest 1.27/1.41, plum 1.27/1.36, ember 1.26/1.33)
     against the 3:1 WCAG 2.2 SC 1.4.11 asks of a non-text UI indicator.
     A keyboard user could not reliably see where focus was.

     WHY NOT JUST DROP THE ALPHA. The identity accent at full strength still
     does not clear 3:1 against white for every theme - measured 2.31 (ink),
     3.53 (harbour), 3.00 (forest), 3.50 (plum), 3.01 (ember). Three of the
     five sit on or under the line, and Ink's teal is the approved look and
     is not going to change. So the ring cannot be the raw accent.

     WHAT IT IS NOW. Two layers, both fully opaque:

       inner  2px  --ring-halo   the surface the control sits on
       outer  2px  --ring-colour --accent-ink, the accent WHERE IT CARRIES
                                 TEXT - i.e. the one accent-derived token
                                 already tuned for legibility against the
                                 ground in each mode (darkened on light,
                                 lifted toward white on dark)

     The halo does the job the alpha used to pretend to: it separates the
     ring from the control's own fill, so the ring is legible on a filled
     .btn-primary as well as on a bare input, and the pair reads as one
     indicator against BOTH adjacent colours. Total ring is 4px, one more
     than before; box-shadow costs no layout.

     Post-fix contrast of the outer band against the surface it is drawn on:
     see fix-themes-round1-report.md - every brand x mode clears 3:1 with the
     lowest at 4.30:1.
     ====================================================================== */
  --ring-halo:   var(--au-surface);
  --ring-colour: var(--accent-ink);
  /* The destructive ring follows the same shape. --danger-fill is tuned to
     carry a white label, which makes it dark enough to disappear on a dark
     canvas (2.92:1 against the dark card), so dark mode lifts the palette red
     toward white the way --accent-ink does. Measured 5.53 light / 5.06 dark. */
  --ring-danger-colour: var(--danger-fill);
  --au-ring: 0 0 0 2px var(--ring-halo), 0 0 0 4px var(--ring-colour);
  --au-ring-danger: 0 0 0 2px var(--ring-halo), 0 0 0 4px var(--ring-danger-colour);

  /* Motion */
  --au-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --au-t: 160ms var(--au-ease);
  --au-t-slow: 220ms var(--au-ease);

  /* Soft semantic tints (bg) */
  --au-green-bg: color-mix(in srgb, var(--green) 13%, var(--au-surface));
  --au-amber-bg: color-mix(in srgb, var(--amber) 15%, var(--au-surface));
  --au-red-bg:   color-mix(in srgb, var(--red) 12%, var(--au-surface));
  --au-blue-bg:  color-mix(in srgb, var(--accent-strong) 13%, var(--au-surface));

  /* Buttons v3 sizing. Every step is on the 4px scale — --btn-h-sm was 34px
     until task 4E, which is both off-scale and 2px too tall to sit inside a
     40px table row once the row's own padding is counted. */
  --btn-h: 40px;
  --btn-h-sm: 32px;
  --btn-h-lg: 48px;
  --btn-gap: 8px;
  /* Text fields match the standard button height (was 43px, off-scale, so a
     field sat 3px proud of the button beside it in every toolbar). */
  --field-h: var(--btn-h);

  /* ======================================================================
     TABLE DENSITY (task 4E) — row height is stated HERE and nowhere else.
     table.data used to be sized by cell padding, declared three separate
     times across the sheets (12px 14px in app.css, 13px 18px and 14px 16px
     in design-system-v2.css) plus an explicit `tbody tr { height: 52px }`.
     Now one rule in design-system-v2.css reads these tokens.

       --row-h-std      40px  the standard row
       --row-h-compact  32px  the dense row
       --row-pad-y       4px  breathing room; does NOT set the height
       --row-pad-x      16px  the side gutter

     How it composes: 14px body text on 24px leading + 2 x 4px padding = 32px
     of content, so a single-line standard row is held open to 40px by
     --row-h and a compact one lands exactly on 32px. A row whose cell holds
     two lines, or a control taller than 32px, grows past the token — row
     height is a floor, the way a table row always has been.

     A table opts into compact density with `class="data is-compact"`, which
     just re-points --row-h. No table in the app carries it yet: nothing in
     the markup distinguishes a dense table from a standard one, so task 4E
     made every table standard. See task-4e-report.md.
     ====================================================================== */
  --row-h-std: 40px;
  --row-h-compact: 32px;
  --row-pad-y: 4px;
  --row-pad-x: 16px;

  /* THE 4px SPACING SCALE. --sp-N is always N x 4px, so the name reads as the
     number of grid steps. Task 4E snapped every off-scale padding, gap and
     margin in the sheets onto these (see task-4e-report.md); --sp-5/-7/-9 were
     added there because 20/28/36px had no token to snap to.
     Values under 4px are deliberately NOT on the scale: 1-3px are optical
     nudges on inline labels (a tag's 2px of vertical padding), not spacing. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-7: 28px; --sp-8: 32px;
  --sp-9: 36px; --sp-12: 48px; --sp-16: 64px;

  /* Brand mesh stops: analogous teal / aqua / cyan, all derived from the
     owner's brand hue so the band re-tints with the brand. */
  --sx-teal: var(--brand-primary, var(--accent));
  --sx-aqua: color-mix(in srgb, var(--brand-accent, var(--accent-strong)) 70%, #14E0C0 30%);
  --sx-cyan: color-mix(in srgb, var(--brand-accent, var(--accent-strong)) 58%, #28C8FF 42%);
  --sx-deep: var(--brand-deep, var(--accent-deep));
  --sx-mint: color-mix(in srgb, var(--brand-primary, var(--accent)) 64%, #5EEAD4 36%);

  /* Ink + slate (not pure black) */
  --sx-ink:   #0A2A2E;
  --sx-slate: #425466;
  --sx-hair:  #EBEEF1;

  /* Soft navy-tinted shadow scale (Stripe depth) */
  --sx-sh-sm: 0 1px 2px rgba(10,42,46,.06);
  --sx-sh-md: 0 2px 5px rgba(10,42,46,.07), 0 8px 16px -6px rgba(10,42,46,.10);
  --sx-sh-lg: 0 8px 24px -8px rgba(10,42,46,.14), 0 16px 48px -16px rgba(10,42,46,.18);
  --sx-sh-band: 0 18px 40px -18px color-mix(in srgb, var(--sx-teal) 50%, transparent),
                0 2px 8px rgba(10,42,46,.06);

  /* Motion */
  --sx-ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* ======================================================================
     ROLE RADII (task 4B tokenised, task 4C flipped the values)
     Every border-radius in app.css / modern.css / design-system-v2.css
     resolves through one of the tokens below (or one of the pre-existing
     radii above). Tokens are named for what the element IS, not for its
     number, so the geometry of the whole app is a value edit in this block.

     THE RULE — flat geometry, 3-6px, no pill buttons:

       3px  small controls (.btn-sm, .seg-btn, filter/chip buttons, checkbox)
       4px  controls (buttons, segmented controls, selects, inputs, textareas)
       6px  surfaces (cards, tiles, panels, table wrappers, modals, drawers,
            popovers, menus)
       999px  ONLY things that LABEL and are not clicked: status pills, badges,
            counts, progress capsules, toggle tracks
       50%  avatars and genuinely circular things (dots, FABs, icon buttons)

     A pill shape now MEANS "this is a label". If the user clicks it, it is a
     control and takes 3-4px — filter pills and segmented controls included.
     ====================================================================== */

  /* Circular things. --r-circle is a true 50% circle; --r-disc is an element
     that is circular only because it is square with a huge radius. */
  --r-circle: 50%;
  --r-disc: 999px;

  /* Non-interactive capsules: status pills, badges, counts. These LABEL, so
     they keep the capsule shape. */
  --r-pill: 999px;
  --r-tag: var(--r-xs);       /* .tag, .chip, kanban tags/links, inline code, cal events */

  /* Progress / meter capsules */
  --r-progress: 999px;

  /* Toggle switch track */
  --r-switch: 999px;

  /* Interactive controls: buttons, segmented controls, menu options, nav rows */
  --r-control: var(--r-btn);
  --r-control-sm: var(--au-r-control-sm);
  --r-nav-item: var(--au-r-control);

  /* Inputs and the small boxes that live inside them */
  --r-checkbox: var(--au-r-control-sm);
  --r-kbd: var(--au-r-control-sm);   /* keyboard-shortcut hint in a search field */

  /* Surfaces: cards, tiles, panels, popovers, menus, drawers */
  --r-surface: var(--au-r-card);
  --r-surface-lg: var(--au-r-card-lg);  /* .hero, .kpi-card */
  --r-icon-chip: var(--au-r-card);      /* icon containers / tile icons */

  /* Chat bubbles: three round corners + one small "tail" corner. A speech
     bubble is a shape, not a control surface — it keeps its roundness. */
  --r-bubble: 18px;
  --r-bubble-tail: 4px;

  /* Hero band bottom sweep */
  --r-band: var(--au-r-card);

  /* .pwa-phone draws a phone. A device, not a panel — off the ladder on purpose. */
  --r-device: 22px;

  /* Scrollbars — a thumb is a capsule at every size. */
  --r-scrollbar-thumb: 999px;
  --r-scrollbar-track: 999px;

  /* Focus rings and hairline indicators */
  --r-focus: 4px;
  --r-indicator: 3px;         /* nav active bar, phone notch */
  --r-indicator-sm: 1px;
}

/* ==========================================================================
   DESIGN TOKENS - LIGHT MODE
   ========================================================================== */
[data-theme='light'] {
  --bg-base: #F8FAFC;
  --bg-elevated: #FFFFFF;
  --bg-card: #FFFFFF;
  --bg-card-hover: #F1F5F9;
  --bg-input: #FFFFFF;
  --bg-overlay: rgba(248, 250, 252, 0.85);
  --bg-subtle: #EEF2F7;

  --border: #E2E8F0;
  --border-strong: #CBD5E1;

  --text-primary: #0F172A;
  --text-body: #1E293B;
  --text-secondary: #475569;
  /* THE LIGHT MUTED. #64748B (the old shared value) clears 4.5:1 on white
     (4.76) and on --au-canvas (4.54) but NOT on the two tinted light
     surfaces muted text also lands on: --bg-subtle #EEF2F7 is 4.23:1 and
     --bg-card-hover #F1F5F9 is 4.34:1. Deepened one step so the token
     clears the floor on every ground rather than only the ones it happened
     to be measured against: worst case is now 4.83:1 on --bg-subtle,
     5.43:1 on white. Still lighter than --text-secondary #475569. */
  --text-muted: #5C6B80;
  --text-inverse: #FFFFFF;

  --teal-glow: rgba(0, 192, 165, 0.10);
  --teal-glow-strong: rgba(0, 192, 165, 0.20);
  --blue-glow: rgba(1, 144, 213, 0.10);

  --accent-soft: rgba(0, 192, 165, 0.10);
  --gold-soft: rgba(245, 176, 65, 0.14);
  --amber-soft: rgba(245, 158, 11, 0.14);
  --orange-soft: rgba(249, 115, 22, 0.13);
  --green-soft: rgba(16, 185, 129, 0.13);
  --red-soft: rgba(239, 68, 68, 0.10);
  --purple-soft: rgba(139, 92, 246, 0.13);

  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05);
  --shadow-md: 0 6px 18px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.04);
  --shadow-lg: 0 24px 48px rgba(15, 23, 42, 0.14), 0 6px 12px rgba(15, 23, 42, 0.05);
  --shadow-glow: 0 0 0 1px rgba(0, 192, 165, 0.22), 0 12px 32px rgba(0, 192, 165, 0.12);
  --shadow-brand: 0 8px 24px rgba(1, 144, 213, 0.18);
  --shadow-card-glow: 0 0 0 1px rgba(0, 192, 165, 0.10), 0 12px 32px rgba(1, 144, 213, 0.06);

  /* v2 light overrides */
  --v2-shadow-1: 0 1px 0 rgba(255,255,255,0.6) inset, 0 1px 2px rgba(15,23,42,0.05);
  --v2-shadow-2: 0 1px 0 rgba(255,255,255,0.6) inset, 0 8px 24px -8px rgba(15,23,42,0.12);
  --v2-shadow-3: 0 1px 0 rgba(255,255,255,0.6) inset, 0 20px 48px -16px rgba(15,23,42,0.16);
  --v2-shadow-hero: 0 1px 0 rgba(255,255,255,0.6) inset, 0 30px 80px -30px rgba(1,144,213,0.25);
  --v2-glass-bg: color-mix(in srgb, #ffffff 80%, transparent);

  /* au-* light overrides. design-system-v2.css declared [data-theme='light']
     twice (line ~1231 and line ~2177) - the second block runs later in the
     cascade so its values (au-canvas, au-surface-2) win over the first. */
  --au-canvas: #F4FBFA;
  --au-surface: #FFFFFF;
  --au-surface-2: #F6FBFB;
  --au-hairline: var(--sx-hair);
  --sx-ink:   #0A2A2E;
  --sx-slate: #425466;
}

/* Warm accents darken on light surfaces — #F5B041/#F97316 on white sit under
   2:1 contrast and read as invisible (priority badges, due-soon dates,
   "worth logging" hints). html[data-theme] outranks the plain [data-theme]
   selector above (and the :root palette the brand-settings stylesheet
   injects after this file) because it carries extra specificity. */
html[data-theme='light'] {
  --gold: #B45309;
  --gold-deep: #92400E;
  --orange: #C2410C;
  --amber: #B45309;

  /* THE PALETTE-AS-TEXT INK, LIGHT GROUND ONLY (see the block beside
     --success-fill for why these exist). Each is the palette hue darkened
     just far enough to clear 4.5:1 against the tint it actually sits on —
     which is the ground that matters, not white: a pill's own 12-15% tint of
     white is darker than the card behind it and eats roughly a fifth of the
     ratio. Every value is measured against all three grounds these hues land
     on (plain white, the pill's color-mix tint, and the rgba *-soft tint):

       green   #0A7350   5.86 white / 5.19 tint / 5.19 soft   (was 2.25)
       red     #B63434   5.95 / 5.10 / 5.10                   (was 3.23)
       purple  #6F4AC5   6.07 / 5.10 / 5.10                   (was 3.56)
       amber   #9E4908   6.16 / 5.00 / 5.00                   (was 4.08)
       orange  #AF3B0B   6.08 / 4.94 / 5.02                   (was 4.21)
       gold    #9E4908   6.16 / 5.00 / 5.09

     The mixes sit a few points darker than the minimum that clears, so a
     white-label owner configuring a lighter success/danger hue still has
     headroom before the derived ink drops under the floor.

     These re-point the TEXT only. The tint fills, borders and status dots
     keep the bright palette hue and are visually unchanged. */
  --green-ink:  color-mix(in srgb, var(--green) 62%, #000);
  --red-ink:    color-mix(in srgb, var(--red) 76%, #000);
  --purple-ink: color-mix(in srgb, var(--purple) 80%, #000);
  --amber-ink:  color-mix(in srgb, var(--amber) 88%, #000);
  --orange-ink: color-mix(in srgb, var(--orange) 90%, #000);
  --gold-ink:   color-mix(in srgb, var(--gold) 88%, #000);
}

/* ==========================================================================
   DESIGN TOKENS - DARK MODE
   ========================================================================== */
[data-theme='dark'] {
  --shadow-soft-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-soft-md: 0 6px 18px rgba(0, 0, 0, 0.34), 0 1px 3px rgba(0, 0, 0, 0.20);
  --shadow-soft-lg: 0 22px 48px rgba(0, 0, 0, 0.50), 0 6px 14px rgba(0, 0, 0, 0.26);
  --shadow-soft-xl: 0 36px 64px rgba(0, 0, 0, 0.60), 0 10px 20px rgba(0, 0, 0, 0.30);
  /* --focus-ring follows --au-ring, which is already mode-aware through
     --ring-halo (--au-surface) and --ring-colour (--accent-ink). */

  --au-canvas: #080C18;
  --au-surface: #0F1828;
  --au-surface-2: #0B1322;
  --au-hairline: color-mix(in srgb, var(--border) 90%, transparent);

  --au-shadow-xs: 0 1px 2px rgba(0,0,0,0.40);
  --au-shadow-sm: 0 1px 2px rgba(0,0,0,0.40), 0 2px 8px rgba(0,0,0,0.32);
  --au-shadow-md: 0 2px 6px rgba(0,0,0,0.40), 0 12px 28px -10px rgba(0,0,0,0.60);
  --au-shadow-lg: 0 4px 10px rgba(0,0,0,0.40), 0 28px 56px -18px rgba(0,0,0,0.70);
  --au-shadow-xl: 0 10px 20px rgba(0,0,0,0.45), 0 48px 90px -26px rgba(0,0,0,0.80);

  /* Z-lift (see the block in :root) — deeper on a dark canvas. */
  --shadow-pop:     0 2px 8px rgba(0, 0, 0, 0.40), 0 10px 26px -8px rgba(0, 0, 0, 0.56);
  --shadow-overlay: 0 6px 14px rgba(0, 0, 0, 0.44), 0 28px 64px -18px rgba(0, 0, 0, 0.68);

  --au-green-bg: color-mix(in srgb, var(--green) 18%, var(--au-surface));
  --au-amber-bg: color-mix(in srgb, var(--amber) 20%, var(--au-surface));
  --au-red-bg:   color-mix(in srgb, var(--red) 18%, var(--au-surface));
  --au-blue-bg:  color-mix(in srgb, var(--accent-strong) 20%, var(--au-surface));

  --sx-ink:   #EAF2F2;
  --sx-slate: #9FB2B6;
  --sx-hair:  color-mix(in srgb, var(--border) 92%, transparent);
  --sx-sh-sm: 0 1px 2px rgba(0,0,0,.40);
  --sx-sh-md: 0 2px 6px rgba(0,0,0,.40), 0 12px 28px -10px rgba(0,0,0,.55);
  --sx-sh-lg: 0 10px 30px -10px rgba(0,0,0,.55), 0 24px 60px -20px rgba(0,0,0,.65);
  --sx-sh-band: 0 24px 60px -22px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.4);
}

/* On a dark ground the bright identity accent is already legible as text, so
   it stops being routed through the action colour.
   `html[data-theme='dark']` (0,1,1) rather than `[data-theme='dark']` (0,1,0):
   /api/brand-settings/css is injected AFTER every stylesheet and declares
   --accent on plain `:root` (0,1,0), which wins over an equal-specificity
   selector purely by order. Anything here that has to survive that injection
   carries the extra `html`. */
html[data-theme='dark'] {
  /* Not the bare accent. A mid-value accent on a 12%-accent tint over a
     near-black card is a surprisingly tight pairing — Logic8 teal manages
     5.65:1 but the Harbour blue only reaches 3.70:1 — so the accent is lifted
     toward white before it is allowed to carry text. 74/26 is the smallest mix
     that clears 4.5:1 for all six accents in play on both the resting tint and
     the deeper hover tint. */
  --accent-ink: color-mix(in srgb, var(--brand) 74%, #FFFFFF);
  /* Same move for the destructive focus ring — see --ring-danger-colour. */
  --ring-danger-colour: color-mix(in srgb, var(--red) 85%, #FFFFFF);
}

/* ============================================================================
   THE FIVE THEMES

   A theme sets exactly two colours, because a theme has exactly two jobs:

     THE ACTION COLOUR   --primary-fill, under --primary-ink
       Every filled, labelled control. Verified >= 4.5:1 against its own label
       in BOTH modes, at rest, on hover and on press. Measured, not assumed —
       the numbers in each row below came out of the harness, not a swatch
       picker.

     THE IDENTITY ACCENT --brand (published to the app as --accent)
       The nav stripe, the avatar ring, the active tab. It carries no text
       and it is never the only thing saying what it says, so it stays
       bright. This is the whole reason Logic8 teal #00C0A5 survives as the
       accent of the Ink theme despite being 2.31:1 against white and
       therefore useless as a fill.

   HOW THE TWO MODES RELATE. On a light ground the action colour is dark and
   the label white. On a dark ground the near-black Ink action INVERTS to
   near-white and takes a dark label; the four coloured themes LIGHTEN and
   also take a dark label, because a fill that was tuned to sit on white is
   too heavy on a near-black canvas. Hover and press each move one further
   step in the same direction, which is why every hover ratio below is HIGHER
   than its resting ratio: the state change can only ever improve legibility.

   ORDERING AND SPECIFICITY — both of the traps this codebase has already
   paid for once:

     1. `html[data-brand=...]` is (0,1,1), and the light/dark blocks that must
        beat it are `html[data-theme='dark'][data-brand=...]` at (0,2,1). Both
        out-rank the plain `:root` (0,1,0) that /api/brand-settings/css injects
        AFTER every stylesheet, so a brand-settings palette can no longer
        silently overwrite a theme's action colour.
     2. Nothing here is a component rule. A theme sets tokens on the root
        element and nothing else, so it cannot out-specify a component variant
        it was never meant to touch. `[data-theme='dark'] .btn` at (0,2,0)
        out-specifying `.btn-primary` at (0,1,0) is exactly how dark mode once
        ate the fill off every filled button. Do not write a theme rule with a
        class in it.

   ADDING A SIXTH THEME. You supply ONE colour — the identity accent — and
   derive the rest:

     1. --brand is your accent, used as-is. It only ever marks, so brightness
        is a virtue. THE CONTRAST RULE, stated precisely, because an earlier
        version of this note said "check the accent clears 3:1" and three of
        the five shipped accents do not (ink 2.31, forest 3.00, ember 3.01,
        against white) — which made the rule read as broken when the themes
        were in fact fine:

          a) A colour that CARRIES TEXT must clear 4.5:1 against that text.
             That is --primary-fill/--primary-ink, --danger-*, --success-*
             and --accent-ink, and it is non-negotiable — step 2 below is
             how you get there.
          b) A colour used as a NON-TEXT UI INDICATOR — one that is the only
             thing telling the user about a state — must clear 3:1 against
             every colour adjacent to it. In this app that is exactly one
             thing: the focus ring, and it is drawn from --accent-ink, not
             from your accent, precisely so this holds for any accent (see
             THE FOCUS RING). Do not introduce a second one.
          c) PURE IDENTITY uses are exempt. The nav stripe, the avatar ring
             and the active-tab underline are decoration on a state that a
             label, a fill or a position already carries, so a dim accent
             costs nothing but sparkle. Your accent lives here.

        So: pick the accent you want. There is no threshold it must clear.
        If you find yourself about to use it as the sole signal for
        something, you are in case (b) and need --accent-ink instead.
     2. --primary-fill-light is that accent DARKENED until it clears 4.5:1
        against #FFFFFF. THE DARKENING RULE, which is the only arithmetic in
        this whole file:

          scale all three sRGB channels by 0.95, re-measure the WCAG ratio
          against white, and repeat until it reaches 4.5:1

        Multiplying the channels preserves the hue and moves only the value,
        which is why the result still reads as the same colour rather than a
        muddied one. It stops at the FLOOR — the first value that clears — so
        treat it as where to start, not where to land. Applied to the five
        accents above it gives, in iterations:

          #00C0A5 -> #008674 (7)   #0190D5 -> #017CB6 (3)
          #00A88F -> #00826F (5)   #8B7BE0 -> #7769C0 (3)
          #E8734A -> #B4593A (5)

        The shipped values sit one or two steps deeper than that (#055EB8,
        #0F6B4F, #5B3FA8, #B4451F) for headroom against hover tints and
        against a white-label owner's own colours. Aim for 5-8:1; much past
        that and the button stops reading as coloured at all.

        INK IS THE DELIBERATE EXCEPTION. Its action colour #16202B is a
        NEUTRAL near-black, not a darkened teal — the rule would have given
        #008674. A theme is allowed to pair a neutral action colour with a
        chromatic accent, and Ink is the case that proves the two colours are
        genuinely independent: the teal never has to survive as a fill because
        it is never asked to be one.

        (backend/lib/contrast.js exports darkenUntilAccessible(hex) once the
        plan's Task 5 backend half lands; the loop above is its whole body.)
     3. --primary-fill (dark mode) is the accent LIGHTENED — the same loop with
        the channels moved toward 255 — until it clears 4.5:1 against a dark
        label, and --primary-ink is that label: your hue at roughly 12%
        lightness, not pure black, so the button does not look like a hole
        punched in the canvas.
     4. Hover and press are one and two further steps in the same direction as
        the mode: darker on light, lighter on dark. Never the other way — the
        state change must not be able to lower the ratio.
     5. Add the id to L8.theme.BRANDS in public/js/app.js, then MEASURE. Do not
        ship a swatch you have only looked at:
          node theme-contrast.js <css-dir> --brands <your-id>
        It renders the real stylesheet stack plus the palette that
        /api/brand-settings/css injects, walks mode x state, and composites
        every layer down to a single ratio per element. Every row must be
        >= 4.5:1 in both modes before you commit it.
   ============================================================================ */

/* WHAT EVERY THEME DERIVES FROM ITS TWO COLOURS.

   A theme states --brand and --primary-*; this block re-derives everything the
   rest of the app builds out of the accent, so a theme is genuinely two values
   and not a scavenger hunt through the stylesheets.

   It has to exist because /api/brand-settings/css injects the OWNER'S palette
   — --accent-soft, --teal-glow, --border-focus, --brand-primary/-accent/-deep
   and the gradient stops — onto plain  after every stylesheet. Setting
   --accent alone would leave a Plum theme drawing violet text on a teal tint.
    is (0,1,1) and wins over that injection; without the
   attribute set, none of it applies and the owner's palette is untouched.

   The mix ratios are copied from the injected block so a themed install and a
   brand-configured install tint identically. */
html[data-brand] {
  --accent-soft:       color-mix(in srgb, var(--brand) 12%, transparent);
  --accent-glow:       color-mix(in srgb, var(--brand) 30%, transparent);
  --teal-glow:         color-mix(in srgb, var(--brand) 11%, transparent);
  --teal-glow-strong:  color-mix(in srgb, var(--brand) 22%, transparent);
  --blue-glow:         color-mix(in srgb, var(--brand) 11%, transparent);
  --blue-glow-strong:  color-mix(in srgb, var(--brand) 26%, transparent);
  --border-focus:      var(--brand);
  --accent-strong:     var(--brand);
  --accent-deep:       var(--primary-fill-light);

  /* The --brand-* trio the aurora and mesh tokens read through. Re-pointing
     them here is what re-tints the KPI decks, hero mesh and chart ramps. */
  --brand-primary: var(--brand);
  --brand-accent:  var(--brand);
  --brand-deep:    var(--primary-fill-light);
  --brand-cta:     var(--primary-fill-light);

  /* The injected block emits these as literal gradients, so they have to be
     restated rather than inherited. Two stops, accent to action colour. */
  --brand-accent-gradient: linear-gradient(135deg, var(--brand) 0%, var(--primary-fill-light) 100%);
  --brand-gradient:        linear-gradient(135deg, var(--brand) 0%, var(--brand) 50%, var(--primary-fill-light) 100%);

  /* ----------------------------------------------------------------------
     THE DECORATIVE RAMP — the six stops the KPI rails, tone gradients and
     mesh bands are painted from.

     These were the tokens the first cut of this block MISSED, and the miss
     was visible: /api/brand-settings/css emits all six as literal hex on
     plain `:root`, so under Ember the KPI accent rail resolved to
     `linear-gradient(180deg, #E8734A, #01A3C2)` — --sx-teal correctly became
     the Ember orange while --sx-cyan stayed the owner's cyan, i.e. an orange
     to teal ramp on every KPI deck in the app, under all five themes.

     THE DERIVATION RULE. A theme owns exactly two colours, so every stop is
     a position on the line between them:

       A = var(--brand)               the bright identity accent (light end)
       D = var(--primary-fill-light)  the action colour        (deep end)

     and the mix ratios are lifted verbatim from what brand-settings.js
     computes for the owner's own palette (its `isTech` branch), so a themed
     install and a brand-configured install ramp identically:

       --sx-aqua    82% A + white   (server: mixHex(accent, #FFF, .82))
       --sx-mint    72% A + white   (server: mixHex(primary, #FFF, .72))
       --sx-cyan    60% A + 40% D   (server: mixHex(accent, primary, .60))
       --au-b-warm  60% A + 40% D   (server has bWarm === sxCyan for tech)
       --au-b-cool  80% D + 20% A   (server: mixHex(deep, primary, .80))

     Because A and D are neighbours on the brand's own ramp rather than
     foreign hues, every stop stays analogous to the accent and the
     light-to-dark shape of each ramp is preserved: aqua/mint sit above the
     accent, cyan/warm in the middle, cool just above the action colour. No
     hardcoded teal, cyan or mint survives under a theme.

     --accent-2 is the injected alias of the mid brand stop; a theme has one
     chromatic stop, so it is the accent.
     ---------------------------------------------------------------------- */
  --sx-aqua:   color-mix(in srgb, var(--brand) 82%, #FFFFFF);
  --sx-mint:   color-mix(in srgb, var(--brand) 72%, #FFFFFF);
  --sx-cyan:   color-mix(in srgb, var(--brand) 60%, var(--primary-fill-light));
  --au-b-warm: color-mix(in srgb, var(--brand) 60%, var(--primary-fill-light));
  --au-b-cool: color-mix(in srgb, var(--primary-fill-light) 80%, var(--brand));
  --accent-2:  var(--brand);
}

/* INK — near-black action, Logic8 teal identity. The default house theme:
   the teal that fails as a fill at 2.31:1 is exactly the teal that works as a
   marker, and this split is what lets it stay.
   light 16.46 / 13.44 / 10.64   dark 14.09 / 15.71 / 16.46  (rest/hover/press) */
html[data-brand='ink'] {
  --brand: #00C0A5;
  --accent: var(--brand);
  --primary-fill-light:   #16202B;
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  #22303F;
  --primary-active-light: #2E4053;
}
html[data-theme='dark'][data-brand='ink'] {
  --primary-fill:   #E8EEF4;
  --primary-ink:    #16202B;
  --primary-hover:  #F7FAFC;
  --primary-active: #FFFFFF;
}

/* HARBOUR — the shipped blue, kept as a theme in its own right.
   light 6.36 / 8.28 / 10.53   dark 5.58 / 6.76 / 8.20 */
html[data-brand='harbour'] {
  --brand: #0190D5;
  --accent: var(--brand);
  --primary-fill-light:   #055EB8;
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  #0A4E95;
  --primary-active-light: #093F78;
}
html[data-theme='dark'][data-brand='harbour'] {
  --primary-fill:   #3A90E2;
  --primary-ink:    #08131F;
  --primary-hover:  #57A0E8;
  --primary-active: #70B1EE;
}

/* FOREST
   light 6.49 / 8.80 / 11.59   dark 6.12 / 7.74 / 9.12 */
html[data-brand='forest'] {
  --brand: #00A88F;
  --accent: var(--brand);
  --primary-fill-light:   #0F6B4F;
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  #0B5540;
  --primary-active-light: #084132;
}
html[data-theme='dark'][data-brand='forest'] {
  --primary-fill:   #2FAE87;
  --primary-ink:    #06201A;
  --primary-hover:  #4CC29D;
  --primary-active: #66D0B0;
}

/* PLUM
   light 7.72 / 9.88 / 12.05   dark 6.25 / 8.24 / 10.08 */
html[data-brand='plum'] {
  --brand: #8B7BE0;
  --accent: var(--brand);
  --primary-fill-light:   #5B3FA8;
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  #4A3389;
  --primary-active-light: #3B2A6E;
}
html[data-theme='dark'][data-brand='plum'] {
  --primary-fill:   #9B87F0;
  --primary-ink:    #160F2C;
  --primary-hover:  #B2A2F5;
  --primary-active: #C4B7F8;
}

/* EMBER — the one theme whose accent is bright enough to double as the dark
   action colour, which is why its dark fill and its accent are the same hex.
   light 5.51 / 7.30 / 9.49   dark 6.05 / 7.51 / 8.94 */
html[data-brand='ember'] {
  --brand: #E8734A;
  --accent: var(--brand);
  --primary-fill-light:   #B4451F;
  --primary-ink-light:    #FFFFFF;
  --primary-hover-light:  #963818;
  --primary-active-light: #7A2D13;
}
html[data-theme='dark'][data-brand='ember'] {
  --primary-fill:   #E8734A;
  --primary-ink:    #24100A;
  --primary-hover:  #F08C69;
  --primary-active: #F5A183;
}
