/*
 * @bsuite/theme — CSS custom properties (:root + .dark)
 * Version 0.6.0
 *
 * Architecture — 5 layers (inner → outer):
 *   1. Palette  — frozen oklch primitives (--neon-electric-*, --braden-*)
 *   2. Surface  — light/dark bg + text primitives (--light-*, --dark-*)
 *   3. Role     -- semantic aliases consumed by Tailwind + components (--role-*)
 *   4. Shadcn   — shadcn/ui bridge mapping (--background, --foreground, etc.)
 *   5. App      -- per-app overrides (--app-primary, --app-accent)
 *
 * Consumers should bind to Layer 3 (roles) or Layer 4 (shadcn) — never
 * directly to palette primitives. Roles make white-labelling safe.
 *
 * Colourblind policy (WCAG 1.4.1) — REWRITTEN 2026-08-02, contract 0.7.0:
 *   The invariant is SEPARATION, not a fixed hue. --role-error must be
 *   maximally separated from --role-primary under deuteranopia and
 *   protanopia. D2C's primary is blue, so error is RED.
 *   The previous "error = purple" rule was actively harmful: under
 *   protanopia purple measured DeltaE 0.006 against primary blue — the
 *   destructive colour and the primary action colour were the SAME swatch.
 *   Purple and indigo are therefore quarantined from semantics entirely.
 *   Warm states (red/amber) separate by LIGHTNESS, not hue — under
 *   deuteranopia everything warm collapses to yellow, so error is the dark
 *   one and warning the light one. Never put two cool hues in opposition.
 *   Colour is never the sole signifier: shape + label always.
 *   TENANT OVERRIDE BLOCKED on --role-error / --role-destructive.
 *
 * Eye-strain / pure-endpoint policy:
 *   Pure white (L=1.0) and pure black (L=0) are BANNED in EVERY role —
 *   text, surface, border, ring and shadow alike (operator ruling
 *   2026-08-02). The prior text-only scoping of this rule is what let
 *   --light-bg-accent ship as oklch(1 0 0.5) — theme-audit-ok: naming the
 *   value that was REMOVED — and render every shadcn card
 *   and popover pure white. "White" here means oklch(0.982 0.002 248)
 *   theme-audit-ok: naming the equivalent hex for a reader's reference, not
 *   consuming it — (#f8f9fa); the lightest surface is bg-panel at L=0.994.
 *
 * OKLCH format is MANDATORY for all new tokens. No hex/rgb/hsl.
 * Syntax: oklch(L C H) — L=lightness 0–1, C=chroma 0–0.4, H=hue 0–360.
 */

/* ============================================================
   LAYER 1 — PALETTE PRIMITIVES (frozen — do not edit)
   ============================================================ */
:root {
  /* D2C Neon Electric — 11 colours (FROZEN per D2C spec v1.00A) */
  --neon-electric-blue:     oklch(0.546 0.215 262.9);
  --neon-electric-cyan:     oklch(0.769 0.132 191.7);
  --neon-electric-indigo:   oklch(0.511 0.23  277);
  --neon-electric-purple:   oklch(0.568 0.202 283.1);
  --neon-electric-magenta:  oklch(0.742 0.167 359.5);
  --neon-electric-pink:     oklch(0.656 0.212 354.3);
  --neon-electric-coral:    oklch(0.669 0.219  20.9);
  --neon-electric-orange:   oklch(0.728 0.168  22.5);
  --neon-electric-yellow:   oklch(0.868 0.125  81.4);
  --neon-electric-green:    oklch(0.723 0.192 149.6);
  --neon-electric-lavender: oklch(0.736 0.141 285.6);

  /* Semantic hues — added 2026-08-02 (contract 0.7.0). The 11 above were
     chosen for looks; these four were chosen for SEPARATION, measured via
     Vienot-Brettel dichromat simulation. Worst-case DeltaE(OKLab) across
     normal/deuteranopia/protanopia for every semantic pair is >= 0.156;
     the pairing these replace scored 0.006. Never bind a component to
     these directly — bind to the --role-* alias. */
  --neon-electric-red:   oklch(0.580 0.230  25);   /* error / destructive */
  --neon-electric-amber: oklch(0.800 0.150  75);   /* warning */
  --neon-electric-teal:  oklch(0.600 0.130 195);   /* success */
  --neon-electric-deep:  oklch(0.380 0.140 270);   /* secondary action */

  /* The two words the estate uses loosely, pinned to values. Operator ruling
     2026-08-02 ("white is primary: #f8f9fa" — theme-audit-ok: naming the
     ruling's literal, not consuming it; the tokens below are oklch) in terms:
     "define --white and --black once in the token source and reference them everywhere; never
     write either literal inline." This is that definition — it did not exist
     until 2026-08-13, which is why the sanctioned replacement values named in
     packages/eslint-config/rules/no-hardcoded-colours.js had no token to point
     at and every legacy pure-white literal got fixed by inventing a fresh
     near-white instead.
     CHOSEN FROM THE CONTRACT, NOT INVENTED — both are now recorded in
     packages/theme/docs/d2c-theme-source-of-truth.html (L1), which is what the
     palette gate reads. Mode-invariant on purpose: these name the colours, not
     a surface role, so they are NOT overridden under .dark. If you want "the
     page background", that is --role-bg-body and it does flip.
     Note the banned endpoints are DESCRIBED here and never spelled out. C1
     scans comments and counts a literal whether or not the sentence around it
     says "never" — writing one to forbid it raises the ratchet. */
  --white:               oklch(0.982 0.002 247.8); /* the near-white, #f8f9fa */
  --black:               oklch(0.166 0.026 269.4); /* the near-black, #0a0e1a */

  /* ============================================================
     LAYER 2 — SURFACE PRIMITIVES (light mode)
     ============================================================ */

  /* Backgrounds */
  --light-bg-primary:   oklch(0.961 0     0.5);
  --light-bg-secondary: oklch(0.982 0.002 248);
  --light-bg-tertiary:  oklch(0.963 0.003 228.9);
  /* bg-panel. theme-audit-ok: prose. Was oklch(1 0 0.5) — pure white, and since --card and
     --popover bridge to it, every shadcn card and popover in all five D2C
     apps rendered pure white in light mode from this one line.

     2026-08-10: that first fix moved it to oklch(0.994) — 0.6 lightness points
     off pure white. It passed the audit, which matches on literal white, and it
     did not pass the operator, who looked at a rendered page and said "pure
     white displayed, which is forbidden". He was right: 0.6 points is below any
     perceptual threshold, so the panel still READ as white even though it no
     longer WAS white. A colour rule enforced by string match will keep accepting
     values like that.

     Now oklch(0.98 0.006 260) — 2.0 points below white and 1.9 above
     --light-bg-primary (0.961), so the panel reads as a raised surface from both
     directions. Three times the chroma of the old value, which is what actually
     stops it reading as white: at this lightness the tint does more work than
     the lightness step.

     CHOSEN FROM THE CONTRACT, NOT INVENTED. The first attempt here used
     oklch(0.976 0.003 260) and the palette gate rejected it — correctly. Only
     colours in packages/theme/docs/d2c-theme-source-of-truth.html are permitted
     (operator ruling 2026-08-03): the document is the source of truth, not the
     code. This value is already in it. */
  --light-bg-accent:    oklch(0.98 0.006 260);

  /* D2C light text scale — hue 260 (cool blue-grey), capped L=0.22.
     Measured WCAG contrast vs --light-bg-primary oklch(0.961 0 0.5), OKLCH ->
     linear-sRGB -> WCAG relative-luminance pipeline, round-trip-verified
     against 6 known hex references (packages/theme/docs/TOKEN-MAPPING.md §8).
     Ratios re-measured 2026-07-17 (W3 §3.1) — supersede prior approximate
     inline values; vs --light-bg-accent (bg-panel, pure white) is always
     HIGHER, so bg-body is the conservative floor for every tier below. */
  --light-text-primary:   oklch(0.22 0.015 260);  /* 15.46:1 ✓ AAA (large+normal) */
  --light-text-secondary: oklch(0.38 0.018 260);  /*  8.94:1 ✓ AAA (large+normal) */
  --light-text-muted:     oklch(0.45 0.018 260);  /*  6.64:1 on body · 4.77:1 worst, on primary/20 over sunken ✓ AA */
  --light-text-subtle:    oklch(0.60 0.012 260);  /*  3.52:1 ✓ AA large only (3:1) — ✗ AA normal */
  --light-text-disabled:  oklch(0.72 0.010 260);  /*  2.21:1 ✗ fails AA at any size — MUST pair with icon cue */

  --light-border: oklch(0.942 0.005 247.9);
  --light-hover:  oklch(0.963 0.003 228.9);

  /* ============================================================
     LAYER 2 — SURFACE PRIMITIVES (dark mode — defined in .dark)
     Values declared in .dark below.
     ============================================================ */

  /* ============================================================
     LAYER 3 — ROLE ALIASES (semantic, light-mode defaults)
     Consumers must only bind to these — never to palette names.
     Roles are what make white-labelling safe.
     ============================================================ */

  /* Action colours */
  --role-primary:     var(--neon-electric-blue);    /* oklch(0.546 0.215 262.9) */
  --role-secondary:   var(--neon-electric-deep);    /* was indigo — 0.037 from primary */
  --role-accent:      var(--neon-electric-cyan);    /* oklch(0.769 0.132 191.7) */

  /* ⚠️  SEPARATION POLICY (contract 0.7.0). The invariant is NOT a fixed hue —
         it is that error must be maximally separated from PRIMARY under
         deuteranopia and protanopia. D2C's primary is blue, so error is RED.
         Purple scored DeltaE 0.006 against primary under protanopia: the
         destructive colour and the primary action colour were the same swatch.
         Purple and indigo are quarantined from semantics entirely.
         Warm states separate by LIGHTNESS (under deuteranopia red and amber
         both read yellow — error is the dark one, warning the light one).
         TENANT OVERRIDE BLOCKED: BrandingProvider rejects --role-error changes. */
  --role-error:       var(--neon-electric-red);     /* was purple — 0.006 from primary */
  --role-destructive: var(--neon-electric-red);     /* synonym — same value      */
  --role-warning:     var(--neon-electric-amber);   /* was orange h22.5 — 0.059 from red */
  --role-success:     var(--neon-electric-teal);    /* was green — 0.043 from red */
  --role-info:        var(--neon-electric-cyan);    /* oklch(0.769 0.132 191.7) */

  /* ═══ TYPE ═══ Lifted from the D2C contract, which specified a font
     stack this package never shipped. Apps were left to pick their own, so
     the estate ran system-ui, Inter and JetBrains Mono simultaneously while
     the contract asked for one family. Same shape of gap as the heading
     ramp: written down, never implemented. */
  --font-display:  'Geist Variable', Geist, system-ui, sans-serif;
  --font-body:     'Geist Variable', Geist, system-ui, sans-serif;
  --font-mono:     'Geist Mono Variable', 'Geist Mono', ui-monospace, monospace;
  --track-display: -0.028em;
  --track-heading: -0.018em;
  --track-body:    0em;
  --track-eyebrow: 0.09em;

  /* ═══ INFORMATIONAL SCALE — categorical, NOT semantic ═══
     Added 2026-08-03 by operator ruling: "informational hues might be handy
     to stand out against the style colour palette."

     Role tokens describe STATE. They cannot describe CATEGORY — which
     pipeline stage, which user role, which product tile. Apps that needed a
     category reached for raw Tailwind instead, which is how one role-badge
     table came to use purple, indigo, blue, violet and sky as its only
     distinguishing signal.

     NUMBERED, NOT NAMED, deliberately. Call one of these "moss green" and
     it will be used for success within a week. A number says what these
     are: interchangeable slots carrying no meaning of their own.
     NEVER use an --info-* for state. State has role tokens.

     Derived, not picked by eye: Vienot-Brettel-Mollon dichromat simulation,
     DeltaE in OKLab, scored as the WORST case across normal, deuteranope and
     protanope vision. Two constraints — perceptual distance keeps them
     DISTINGUISHABLE, a >=28 degree hue gap keeps them looking CATEGORICAL
     rather than like a light/dark pair of one colour. Every one is also
     held away from the brand roles, which is what "stand out against the
     style palette" requires: an informational badge must not read as a
     primary action or an error.
     Reproduce with scripts/design-informational-hues.py

     LIMIT: worst-case separation is ~0.06-0.07, against >=0.156 for the
     semantic pairs. That is 10x better than the purple-vs-blue pairing this
     system rejected (0.006), but it is NOT the semantic standard and cannot
     be — 8 mutually-distinct categories that also avoid 6 reserved brand
     colours is a harder problem than 6 roles. So: colour is never the sole
     signifier for a category. Always pair with a label or an icon. Past 8
     categories, stop adding hues and add a second dimension. */
  --info-1: oklch(0.460 0.140   0.0);
  --info-2: oklch(0.700 0.140  42.0);
  --info-3: oklch(0.540 0.140 138.0);
  --info-4: oklch(0.700 0.140 168.0);
  --info-5: oklch(0.780 0.140 228.0);
  --info-6: oklch(0.700 0.140 258.0);
  --info-7: oklch(0.500 0.140 300.0);
  --info-8: oklch(0.460 0.140 330.0);

  /* ═══ STAGE RAMP — progression, one hue, ascending lightness ═══
     For an ORDERED sequence (pipeline stages), not a category set. A ramp
     encodes direction: "further along" is visible at a glance, and it
     survives greyscale and both dichromacies because lightness carries it.
     Arbitrary per-stage hues cannot do that — a reader has to LEARN that
     sky comes after amber.

     Progression only. OUTCOMES are state, not stage: a hired candidate is
     --role-success, a rejected one --role-error, a withdrawn one muted.
     Do not extend this ramp to cover them. */
  --stage-1: oklch(0.860 0.130 262.9);
  --stage-2: oklch(0.780 0.130 262.9);
  --stage-3: oklch(0.700 0.130 262.9);
  --stage-4: oklch(0.620 0.130 262.9);
  --stage-5: oklch(0.540 0.130 262.9);
  --stage-6: oklch(0.460 0.130 262.9);

  /* AA-safe text renderings of the saturated roles on light surfaces.
     A saturated fill colour is not a legible text colour; these are the
     text variants and are the ONLY correct token for coloured type.

     VERIFIED AGAINST THE TINTED PILL, NOT JUST THE PAGE BACKGROUND. These
     previously sat at ~4.70:1 measured against --role-bg-body, which is the
     right floor for a bare coloured word on the page and the wrong one for
     where they are actually used most: `bg-success/10 text-success-text`, a
     status pill captioned on a 10% wash of its own hue. That wash pulls the
     backdrop toward the text, and every one of these measured BELOW 4.5:1 in
     that setting — error worst at 3.64:1 live. A token can be AA in the
     context it was measured in and fail in the context it ships in.

     Each is now dropped in lightness until it clears 4.5:1 on its own tint
     with margin. Darkening only ever RAISES contrast against a light
     backdrop, so the original page-background figures improve rather than
     regress; both are quoted below. Dark mode is untouched — its variants sit
     at L 0.67-0.79 and already measure clean on tint (7:1+ AAA).

     RETUNED 2026-08-27, and the reason is the same short-list failure one axis
     over. The figures above were taken at ONE alpha (/10) against ONE surface
     (the panel). The estate ships three alphas — 103 sites at /10, 28 at /15
     and 27 at /20 — over five surfaces, and a heavier wash on a DARKER surface
     is strictly worse. Measured across that full cross product, 12 of the 21
     role x alpha cells were below 4.5:1: every role failed at /20, and
     --role-secondary failed even at /10 (4.44:1). The worst reading was
     secondary at /20 on --role-bg-sunken, 3.72:1.

     --role-secondary-text also stopped aliasing --neon-electric-indigo and
     carries a tuned triple like its six siblings. Aliasing a brand colour meant
     it could not be corrected without moving the brand, and it was the only one
     of the seven that could not be tuned in place.

     The figures quoted are now the WORST cell across all five surfaces and all
     three alphas, not the comfortable one. Gate: src/text-contrast.test.ts. */
  --role-primary-text: oklch(0.471 0.215 262.9);  /* 4.62:1 worst tint · 6.43:1 body ✓ AA */
  --role-accent-text:  oklch(0.456 0.132 191.7);  /* 4.61:1 worst tint · 5.61:1 body ✓ AA */
  --role-error-text:   oklch(0.430 0.230  25);    /* 4.61:1 worst tint · 6.83:1 body ✓ AA */
  --role-warning-text: oklch(0.501 0.150  75);    /* 4.61:1 worst tint · 5.44:1 body ✓ AA */
  --role-success-text: oklch(0.436 0.130 195);    /* 4.61:1 worst tint · 6.11:1 body ✓ AA */
  /* Pair for --role-info. Every other semantic role had a -text token; info
     was the only gap, which forced apps to invent a local copy (R80.4). */
  --role-info-text:    oklch(0.462 0.120 192);    /* 4.60:1 worst tint · 5.59:1 body ✓ AA */
  /* Pair for --role-secondary, and the last gap in this family.
     --role-secondary is a FILL: light text sits ON it, so it is tuned dark
     (L 0.380) and measures 9.81:1 against --text-on-light-fill. Used as a
     STROKE on a dark canvas it inverts into the problem — 1.78:1 against
     --role-bg-panel, failing 1.4.11's 3:1 for a graphical object. The
     schema-builder inherits_from edge drew exactly that, in dark mode.
     Lightening --role-secondary itself was measured and rejected: at L 0.60 the
     stroke clears (4.56:1) but every secondary BUTTON's label drops to 3.82:1,
     trading a non-text failure for a worse text one across the estate. A
     stroke-safe sibling costs nothing and moves no existing surface.
     Both variants are existing NAMED palette entries rather than new values —
     the whitelist audit rejected two hand-tuned literals here, correctly. */
  --role-secondary-text: oklch(0.463 0.230 277);  /* 4.61:1 worst tint · 6.95:1 body ✓ AA */

  /* Text roles — six-tier scale (W3 §3.1 contract). Every tier below is
     measured against --role-bg-body (the conservative floor); all tiers
     score HIGHER against --role-bg-panel/--role-bg-surface. */
  /* Six-level heading ramp — contract §heading-ramp. Each level shifts hue as
     well as lightness, so the hierarchy reads in greyscale and under both
     dichromacies, not only by size. --role-text-heading is h1 by definition;
     it used to carry its own literal (chroma 0.018 against the contract's
     0.020) which is how a token drifts off-palette one hair at a time. */
  --role-h1: oklch(0.20 0.020 263);
  --role-h2: oklch(0.26 0.026 258);
  --role-h3: oklch(0.31 0.034 248);
  --role-h4: oklch(0.36 0.042 236);
  --role-h5: oklch(0.41 0.050 222);
  --role-h6: oklch(0.45 0.058 208);
  --role-text-heading:     var(--role-h1);          /* 16.17:1 ✓ AAA */
  --role-text-body:        var(--light-text-primary);   /* 15.46:1 ✓ AAA */
  --role-text-secondary:   var(--light-text-secondary); /*  8.94:1 ✓ AAA */
  --role-text-muted:       var(--light-text-muted);     /*  6.64:1 ✓ AA normal (4.77:1 on primary/20 over sunken) */
  --role-text-subtle:      var(--light-text-subtle);    /*  3.52:1 ✓ AA large only */
  --role-text-disabled:    var(--light-text-disabled);  /*  2.21:1 ✗ icon cue required */

  /* Surface roles */
  --role-bg-body:      var(--light-bg-primary);
  --role-bg-surface:   var(--light-bg-secondary);
  --role-bg-panel:     var(--light-bg-accent);
  --role-bg-input:     var(--light-bg-tertiary);
  --role-bg-sunken:    oklch(0.938 0.004 250);
  --role-border:       var(--light-border);
  --role-border-strong: oklch(0.880 0.008 250);

  /* --role-border-interactive — TH-5 / bsuite#1958, the operator's
     "unstyled button".
     ------------------------------------------------------------------
     WCAG 1.4.11 (non-text contrast) requires 3:1 for the visual boundary
     that IDENTIFIES a user-interface component — the outline of a text
     input, a select, an `outline`-variant button. It does NOT require 3:1
     of a decorative hairline separating two filled surfaces, which is what
     `--role-border` is for. Those are two different jobs and they had one
     token, so the stricter job inherited the looser value.

     MEASURED, light mode, OKLCH -> linear-sRGB -> WCAG relative luminance
     (the same pipeline the text scale above is measured with, positive-
     controlled by reproducing this file's own recorded dark figures of
     3.36:1 and 3.61:1 for --role-border-strong to two decimals):

       --role-border        vs --role-bg-panel   1.12:1   <- the filed defect
       --role-border        vs --role-bg-surface 1.13:1
       --role-border        vs --role-bg-sunken  1.01:1   <- worst live case
       --role-border-strong vs --role-bg-panel   1.35:1

     So re-pointing at the strongest EXISTING light token does not fix it —
     1.35:1 is still less than half the floor. This role needs a value that
     does not exist in the light scale yet, and here it is.

     oklch(0.56 0.015 260) is from the D2C source-of-truth document (the
     palette whitelist accepts it) and clears 3:1 on every light surface
     role with margin, so no call site has to know which surface it landed on:

       vs --role-bg-surface 4.42:1     vs --role-bg-panel  4.39:1
       vs --role-bg-input   4.18:1     vs --role-bg-body   4.15:1
       vs --role-bg-sunken  3.88:1  <- worst case, still +29% over the floor

     Bound to shadcn's `--input` below, which is what `border-input` on every
     Input/Select/Textarea and the Button `outline` variant resolves through.
     That binding is the whole point: a token nothing invokes is TH-8. */
  --role-border-interactive: oklch(0.56 0.015 260);

  /* Scrim — the veil behind a modal, and the reason `bg-black/N` survived   theme-audit-ok: prose
     everywhere else. It was the single most duplicated pure endpoint in the
     tree (9 sites across nav-core, page-builder and schema-builder) for one
     reason: no overlay role existed, so every author reached for the only
     thing that was always available. A banned value with no sanctioned
     replacement does not get obeyed, it gets re-typed.

     VALUE AND SHAPE ADOPTED FROM crm7, which had already solved this locally
     (crm7/src/index.css --color-overlay, with the note "pure black banned").
     Lifting their value rather than minting a competing one is the whole
     point: two scrim tokens with different values is the duplication this
     change exists to remove, and theirs was here first.

     OPAQUE — the alpha belongs at the CALL SITE, not in the token. I first
     baked 0.55 in to stop the /40-vs-/50 drift between page-builder and
     schema-builder, which would have silently halved crm7's existing
     `bg-overlay/50` to 0.275 by compounding. Call-site alpha is also simply
     more useful: a full-page modal scrim and a caption strip over a photo
     want the same colour at different strengths.

     Declared ONCE — no .dark override. A scrim is not a surface: it veils
     whatever is behind it, and the dialog above it is --role-bg-panel, which
     already flips. Giving it a dark twin would move the veil and the panel
     together and cancel out the separation this exists to create. */
  --role-overlay:      oklch(0.145 0.022 268);

  /* Inverse text on coloured fills. Two values only — the fill is either
     light enough to need dark text, or dark enough to need light text.
     Neither is a pure endpoint. */
  --text-on-light-fill:  oklch(0.98 0.006 260);
  --text-on-dark-fill:   oklch(0.19 0.020 255);
  --text-on-primary:     var(--text-on-light-fill);  /* on blue  L≈0.55 — 4.88:1 ✓ AA */
  --text-on-error:       var(--text-on-light-fill);  /* on red   L≈0.58 — 4.55:1 ✓ AA */
  --text-on-accent:      var(--text-on-dark-fill);   /* on cyan  L≈0.77 — 9.38:1 ✓ AAA */
  --text-on-overlay:     var(--text-on-light-fill);  /* on scrim L≈0.15 — the scrim is dark in BOTH modes,
                                                        so this never flips. crm7 declared it as a bare
                                                        oklch(0.99 0 0); routed through the shared token
                                                        instead so there is one near-white, not two. */
  --text-on-warning:     var(--text-on-dark-fill);   /* on amber L≈0.80 — 9.68:1 ✓ AAA */
  --text-on-success:     var(--text-on-dark-fill);   /* on teal  L≈0.60 — 5.17:1 ✓ AA */

  /* ============================================================
     LAYER 4 — SHADCN/UI BRIDGE (light mode)
     Maps shadcn's expected CSS var names to our role tokens.
     Do NOT redefine these in app CSS — override the role above.
     ============================================================ */
  --background:         var(--role-bg-body);
  --foreground:         var(--role-text-body);
  --card:               var(--role-bg-panel);
  --card-foreground:    var(--role-text-body);
  --popover:            var(--role-bg-panel);
  --popover-foreground: var(--role-text-body);
  --primary:            var(--role-primary);
  --primary-foreground: var(--text-on-primary);
  --secondary:          var(--role-secondary);
  --secondary-foreground: var(--text-on-primary);
  --muted:              var(--role-bg-input);
  --muted-foreground:   var(--role-text-muted);
  --accent:             var(--role-accent);
  --accent-foreground:  var(--text-on-accent);
  --destructive:        var(--role-destructive);
  --destructive-foreground: var(--text-on-error);
  --border:             var(--role-border);
  /* `--input` is shadcn's INTERACTIVE boundary — `border-input` is on every
     Input, Select, Textarea and the Button `outline` variant. It pointed at
     --role-border, the decorative hairline, which measures 1.12:1 against the
     panel: WCAG 1.4.11 wants 3:1 and the operator simply called it "an
     unstyled button" (bsuite#1958). Now bound to the role built for the job.
     `--border` deliberately stays on --role-border: separating two filled
     surfaces is not a component boundary and does not carry the 3:1 duty. */
  --input:              var(--role-border-interactive);
  --ring:               var(--role-primary);

  /* ============================================================
     CANONICAL ACCENT GRADIENT (W3 §3.1) — exactly ONE gradient
     exists suite-wide. Marketing/pre-auth heroes ONLY — never on
     functional text (labels, body copy, buttons). Consumed via the
     `.bsuite-accent-gradient` utility (utilities.css). App-local
     decorative gradients (e.g. crm7's `.crm7-gradient`) are
     quarantined and must not be used for new work — see
     docs/TOKEN-MAPPING.md §8.
     ============================================================ */
  --gradient-accent: linear-gradient(135deg, var(--role-primary), var(--role-accent));

  /* HEADING gradient — the same two hues, but built from the *-text role
     variants rather than the raw ones, and that difference is load-bearing.
     Measured against the mode backgrounds:

       --role-accent (cyan) on the LIGHT bg ......... 1.76:1   unreadable
       --role-accent-text   on the LIGHT bg ......... 4.70:1   AA

     So painting a heading with --gradient-accent verbatim leaves the cyan half
     of the word illegible in light mode. The *-text variants are AA-verified in
     BOTH modes and already flip under .dark, so one token is correct in both:
       light  4.71:1 -> 4.70:1        dark  7.01:1 -> 9.78:1
     No new colour values, so the palette whitelist is unaffected. */
  --gradient-heading: linear-gradient(135deg, var(--role-primary-text), var(--role-accent-text));

  /* ============================================================
     GLOW + SHADOW MULTIPLIERS
     ============================================================ */
  --glow-strength:   0.4;
  --shadow-strength: 1;

  /* ============================================================
     SHADOW INK — the paint every elevation is made of.
     ============================================================
     Added 2026-08-12. Until now --shadow-strength had ZERO consumers
     anywhere in the monorepo or the six apps: a dial wired to nothing.
     The elevation ramp in preset-v4.css is now the thing it turns.

     WHY A TOKEN AND NOT A LITERAL PER SHADOW
     theme-audit-ok: prose. Quoting the upstream default this ink replaces.
     Tailwind's own shadow scale is built on `rgb(0 0 0 / a)` — pure black,
     which the contract bans in EVERY role, alpha forms included. That ban
     was silently unenforced for shadows because the value lives inside
     Tailwind's dist, not in our source, so no scanner here could ever see
     it. preset-v4.css now rebinds the whole --shadow-* namespace onto this
     ink, which is how the ban starts applying to the 292 `shadow-sm/md/lg`
     call sites that were painting pure black.

     --shadow-color carries BOTH the hue and the mode's BASE opacity. The
     ladder below multiplies that base; `alpha` in relative-colour syntax
     reads it back, so one token sets a mode's whole shadow density and the
     four steps keep their relationship to each other.

     WHY THE BASE DIFFERS SO MUCH BETWEEN MODES (0.10 vs 0.70)
     A shadow is only visible as a difference from the surface under it. In
     light mode the surface is L 0.96 and the ink is L 0.166, so 10% alpha
     already reads. In dark mode the surface IS L 0.166 — ink at 10% would
     be invisible, which is exactly what shipped. The dark ink therefore
     goes BELOW the page (L 0.08) and much denser. These are not invented
     numbers: they are the values crm7 and business-suite-unified had each
     hand-tuned in their own local copies of this ramp, reconciled.

     NOT pure black at either end — L 0.166 is the estate's near-black
     (--dark-bg-primary), L 0.08 the contract's deepest surface tone. Both
     are hue-matched to the surface family (~268), so a shadow reads as the
     page's own shade rather than a grey hole punched in it.

     ⚠ THE FOUR STEPS ARE RE-DECLARED VERBATIM UNDER .dark, AND MUST BE.
     This looks like duplication begging to be removed. It is not. A var()
     inside a custom property is substituted at the element where THAT
     PROPERTY is declared, and the resolved value is what inherits. Declared
     only here, --shadow-ink resolves against :root's --shadow-color — the
     LIGHT one — and every .dark descendant inherits that already-resolved
     light value. Measured: with the ladder at :root alone, all five
     elevations rendered identical oklch(0.166 … / 0.1) in dark mode.
     Deleting the .dark copy silently restores that bug. */
  --shadow-color:     oklch(0.166 0.026 269.4 / 0.10);
  --shadow-ink-faint: oklch(from var(--shadow-color) l c h / calc(alpha * 0.4 * var(--shadow-strength)));
  --shadow-ink-soft:  oklch(from var(--shadow-color) l c h / calc(alpha * 0.6 * var(--shadow-strength)));
  --shadow-ink:       oklch(from var(--shadow-color) l c h / calc(alpha * 1.0 * var(--shadow-strength)));
  --shadow-ink-deep:  oklch(from var(--shadow-color) l c h / calc(alpha * 1.6 * var(--shadow-strength)));

  /* ============================================================
     CARD SURFACE GLOW — subtle neon separation, dark mode only.
     Light mode keeps the existing border-only treatment (--role-border);
     these resolve to `none` here so any consumer applying the utility
     unconditionally (i.e. without a `dark:` guard) degrades safely.
     ============================================================ */
  --card-glow-source: var(--app-accent, var(--role-accent));
  --glow-card:         none;
  --glow-card-hover:   none;

  /* ============================================================
     CARD CORNER RADIUS — ONE var, read by BOTH card surfaces.

     `@bsuite/page-builder`'s grid item and an app's own card must agree on a
     radius or their corners misalign — 28px inside 24px is the "misshapen
     corner" report. Aligning them on a LITERAL would fix the corner and lock
     white-label out at the same time, so they align on a var instead.

     1.5rem is exactly the `rounded-3xl` this replaces: no visual change.

     NOT WIRED TO A TENANT YET, deliberately. `--radius-preset` is written by
     THREE branding resolvers (crm7 useBranding.ts:354, BSU :302, braden
     useTenantBranding.ts:64) and read by NOTHING — measured across the whole
     estate 2026-08-25, zero readers. It also carries a preset NAME ('md'), not
     a length, so `border-radius: md` is what a naive wiring would produce.
     Wiring it needs the resolver to emit a length and a visual gate on two
     tenants; doing it blind is how a live white-label gets wrecked. Recorded,
     not guessed.
     ============================================================ */
  --radius-card: 1.5rem;

  /* ------------------------------------------------------------
     D2C Neon Electric has NO sm/md/lg radius scale, deliberately not
     invented here. Braden Corporate defines 4/6/10 (braden.css); D2C
     defines only this card radius. preset-v4.css maps the utilities to
     `--role-radius-*` with Tailwind's own values as the fallback, so D2C
     renders exactly as it does today and Corporate gets its language.

     Choosing D2C numbers would be synthesising a design decision to make
     the table tidy. When the D2C scale is decided, define the three
     --role-radius-* here and the utilities pick it up with no call-site
     change. 3,400+ raw `rounded-*` uses estate-wide are already pointed
     at these utilities, which is why this is worth getting right rather
     than guessing.
     ------------------------------------------------------------ */
}

/* ============================================================
   LAYER 5 — PER-APP ACCENT
   Added 2026-08-02: the 0.7.0 contract specifies these blocks but the
   package had never shipped them — only a `var(--app-accent, …)` fallback
   reference, so every app resolved to the shared cyan and the per-app
   identity existed on paper only.

   Set `data-app` on <body>. The FILL is the brand hue; the TEXT variants
   are solved per mode, because a saturated fill colour is not a legible
   text colour and the accent must never render below AA.
   ============================================================ */
[data-app="crm7"] {
  --app-accent:        oklch(0.769 0.132 191.7);
  --app-accent-text-l: oklch(0.505 0.132 191.7);
  --app-accent-text-d: oklch(0.769 0.132 191.7);
}
[data-app="bsu"] {
  --app-accent:        oklch(0.541 0.247 293);
  --app-accent-text-l: oklch(0.564 0.247 293);
  --app-accent-text-d: oklch(0.745 0.247 293);
}
[data-app="conduit"] {
  --app-accent:        oklch(0.723 0.192 149.6);
  --app-accent-text-l: oklch(0.505 0.192 149.6);
  --app-accent-text-d: oklch(0.723 0.192 149.6);
}
[data-app="r80"] {
  --app-accent:        oklch(0.666 0.157 58.3);
  --app-accent-text-l: oklch(0.546 0.157 58.3);
  --app-accent-text-d: oklch(0.703 0.157 58.3);
}
/* R8.04 — the rates engine replacing R80.3. Deliberately REUSES r80's
   orange: it serves the same users doing the same job, and a rates engine
   is not where people should be re-learning which app they are in.
   Requested by the r804 lane 2026-08-02. */
[data-app="r804"] {
  --app-accent:        oklch(0.666 0.157 58.3);
  --app-accent-text-l: oklch(0.546 0.157 58.3);
  --app-accent-text-d: oklch(0.703 0.157 58.3);
}
[data-app="throughput"] {
  --app-accent:        oklch(0.656 0.212 354.3);
  --app-accent-text-l: oklch(0.564 0.212 354.3);
  --app-accent-text-d: oklch(0.731 0.212 354.3);
}
/* Light/dark rebinding of the text variant. */
:root      { --app-accent-text: var(--app-accent-text-l); }
.dark      { --app-accent-text: var(--app-accent-text-d); }

/* ============================================================
   NO sRGB FALLBACK — DELIBERATELY REMOVED 2026-08-02.
   Precedent: precedent__bsuite__20260802__theme_conformance_is_absolute.

   theme-audit-ok: prose. The `@supports not (color: oklch(0 0 0))` block that stood here shipped
   a parallel hex palette. It is gone, and must not be reinstated:

     · OKLCH has been Baseline-available in every evergreen engine since
       Firefox 113 (May 2023), after Safari 15.4 (2022) and Chrome 111
       (Mar 2023). It guarded a population that no longer exists.
     · It was a second source of truth for the same colours, so it drifted:
       it still bound --role-error to purple after the semantic set moved
       to red. A fallback that disagrees with the tokens is worse than none.

   If a colour needs to exist in hex (email HTML, PDF, canvas, manifest),
   derive it at that call site from the oklch token — do not re-introduce a
   second palette here.
   ============================================================ */

/* ============================================================
   DARK MODE — Layer 2 surface overrides + Layer 3 role rebind
   ============================================================ */
.dark {
  /* Dark surface primitives.
     --dark-bg-tertiary, -quaternary, -accent and --dark-hover were DELETED
     2026-08-03: four tokens, zero consumers anywhere in the monorepo or the
     five apps, and none of their values appears in the contract. A palette
     entry nobody binds to is not a spare part — it is a second source of
     truth waiting for someone to reach for it. */
  --dark-bg-primary:    oklch(0.166 0.026 269.4);

  /* D2C dark text scale — hue 260, capped L=0.94.
     Pure white (L=1.0) is BANNED as a text token.
     Eye-strain: 16:1 → ~14:1; perceived glare drops significantly.
     Measured WCAG contrast vs --dark-bg-primary oklch(0.166 0.026 269.4)
     via the same OKLCH -> linear-sRGB -> WCAG pipeline as the light scale
     above (re-measured 2026-07-17, W3 §3.1 — supersedes prior approximate
     inline values). vs --role-bg-surface/--role-bg-panel is always HIGHER,
     so bg-body is the conservative floor. */
  --dark-text-primary:   oklch(0.94 0.012 260);  /* body     — 16.16:1 ✓ AAA */
  --dark-text-secondary: oklch(0.82 0.015 260);  /* labels   — 11.04:1 ✓ AAA */
  --dark-text-muted:     oklch(0.68 0.018 260);  /* metadata —  6.69:1 ✓ AA (normal) */
  --dark-text-subtle:    oklch(0.56 0.015 260);  /* placeholders —  4.14:1 ✓ AA large only — ✗ AA normal */
  --dark-text-disabled:  oklch(0.44 0.010 260);  /* disabled —  2.48:1 ✗ fails AA at any size — MUST pair with icon cue */

  --dark-border: oklch(0.428 0.015 248.2);

  /* Six-level heading ramp — contract §heading-ramp. Every level is a
     DIFFERENT hue as well as a different lightness, so the hierarchy survives
     greyscale and both dichromacies rather than relying on size alone. */
  --role-h1: oklch(0.95 0.016 262);
  --role-h2: oklch(0.92 0.018 264);
  --role-h3: oklch(0.88 0.026 240);
  --role-h4: oklch(0.84 0.040 220);
  --role-h5: oklch(0.80 0.052 200);
  --role-h6: oklch(0.75 0.062 195);

  /* Rebind roles to dark-mode surface values — six-tier scale (W3 §3.1). */
  --role-text-heading:   var(--role-h1);
  --role-text-body:      var(--dark-text-primary);   /* 16.16:1 ✓ AAA */
  --role-text-secondary: var(--dark-text-secondary); /* 11.04:1 ✓ AAA */
  --role-text-muted:     var(--dark-text-muted);     /*  6.69:1 ✓ AA normal */
  --role-text-subtle:    var(--dark-text-subtle);    /*  4.14:1 ✓ AA large only */
  --role-text-disabled:  var(--dark-text-disabled);  /*  2.48:1 ✗ icon cue required */

  --role-bg-body:    var(--dark-bg-primary);
  --role-bg-surface: oklch(0.212 0.028 269);
  --role-bg-panel:   oklch(0.190 0.020 260);
  --role-bg-input:   oklch(0.240 0.020 260);
  --role-bg-sunken:  oklch(0.145 0.022 268);
  --role-border:     var(--dark-border);
  /* Dark-mode `--role-border-strong` was MISSING, so `.dark` inherited the
     light value from `:root` — oklch(0.880 0.008 250), i.e. #d4d8dd. Measured
     against `--role-bg-panel` that is 12.88:1: a near-white hairline on navy,
     in the 50 files across crm7, conduit, BSU and throughput that use
     `border-strong`. Not a subtle miss — the brightest thing on the panel.

     `braden.css` has carried both variants since it was written (0.900/0.840
     light, 0.320/0.420 dark). Only the D2C theme was half-defined, which is
     why nobody reading the corporate theme would spot it.

     Value WAS oklch(0.52 0.02 248) from the D2C source-of-truth document,
     recorded here as 3.36:1 against `--role-bg-panel` and 3.61:1 against
     `--role-bg-sunken`. Both figures re-measured exactly on 2026-08-17 —
     which is how the instrument for TH-5 was positive-controlled.

     WHAT THAT PAIR OF FIGURES MISSED (TH-5, 2026-08-17): only two of the five
     dark surface roles were ever checked. Against the third —
     `--role-bg-input` oklch(0.240 0.020 260), the LIGHTEST dark surface and
     the one an interactive boundary actually sits on — 0.52 measures 2.99:1.
     Under the 3:1 floor, by a rounding margin, on precisely the surface the
     rule exists for. Nothing was wrong with the arithmetic; the surface list
     was short, so the worst case was never in it.

     Raised to oklch(0.55 0.015 250) — also from the source-of-truth document,
     +0.03 L, visually indistinguishable, and decisive:

       vs --role-bg-body   3.98:1     vs --role-bg-surface 3.65:1
       vs --role-bg-panel  3.81:1     vs --role-bg-sunken  4.09:1
       vs --role-bg-input  3.40:1  <- the case that used to be 2.99:1

     `--role-border` itself is 2.26:1, which is why "strong" has to exist.
     The contract test now measures all five surfaces in both modes, so a
     short surface list cannot hide a sub-3:1 pair again. */
  --role-border-strong: oklch(0.55 0.015 250);

  /* Dark half of --role-border-interactive (see the light block for the full
     rationale). oklch(0.66 0.018 250), source-of-truth palette, measured
     against every dark surface role:

       vs --role-bg-sunken  6.38:1     vs --role-bg-body   6.20:1
       vs --role-bg-panel   5.95:1     vs --role-bg-surface 5.69:1
       vs --role-bg-input   5.30:1  <- worst case

     Deliberately a wider margin than the light half's 3.88:1: a hairline on
     navy is the case this estate has already got wrong twice (the near-white
     `border-strong` above, and the 0.09-alpha `--border-shell`), and the cost
     of a boundary that is slightly too visible is nothing next to one that
     disappears. */
  --role-border-interactive: oklch(0.66 0.018 250);

  /* Role text variants rebind for the dark surface — all AAA on navy. */
  --role-primary-text: oklch(0.721 0.215 262.9);  /*  7.01:1 ✓ AAA */
  --role-accent-text:  var(--neon-electric-cyan); /*  9.78:1 ✓ AAA */
  --role-error-text:   oklch(0.786 0.230  25);    /*  7.00:1 ✓ AAA */
  --role-warning-text: var(--neon-electric-amber);/* 10.10:1 ✓ AAA */
  --role-success-text: oklch(0.671 0.130 195);    /*  6.99:1 ✓ AAA */
  --role-info-text:    oklch(0.780 0.120 192);    /*  7.0:1 ✓ AAA on dark panel */
  --role-secondary-text: var(--neon-electric-lavender); /* 6.78:1 input · 7.62:1 panel ✓ AAA */

  /* Glow/shadow scale up in dark mode */
  --glow-strength:   0.6;
  --shadow-strength: 0.8;

  /* Shadow ink for the dark surface. Only the SOURCE rebinds — the four
     --shadow-ink-* steps are declared once at :root and resolve per element
     through this token and --shadow-strength, so they follow .dark for free.
     L 0.08 sits BELOW --dark-bg-primary (L 0.166); a shadow at or above the
     page's own lightness is not a shadow. Base opacity 0.70, which after
     --shadow-strength 0.8 puts the main layer at 0.56 — inside the 0.5-0.8
     band crm7 and BSU had each arrived at independently.

     The four steps below are textually identical to their :root twins and
     are NOT redundant — see the substitution-timing note at the :root
     declaration. Only --shadow-color above differs; the ladder is repeated
     so it re-resolves against it. */
  --shadow-color:     oklch(0.08 0.02 268 / 0.70);
  --shadow-ink-faint: oklch(from var(--shadow-color) l c h / calc(alpha * 0.4 * var(--shadow-strength)));
  --shadow-ink-soft:  oklch(from var(--shadow-color) l c h / calc(alpha * 0.6 * var(--shadow-strength)));
  --shadow-ink:       oklch(from var(--shadow-color) l c h / calc(alpha * 1.0 * var(--shadow-strength)));
  --shadow-ink-deep:  oklch(from var(--shadow-color) l c h / calc(alpha * 1.6 * var(--shadow-strength)));

  /*
   * Card glow — subtle neon separation for card surfaces, dark mode only.
   * Derived from the app's accent colour (--app-accent, falling back to
   * the shared --role-accent default) via color-mix() so per-tenant
   * BrandingProvider overrides and per-app brand accents (Conduit green,
   * R80.3 amber, etc.) automatically re-colour the glow. Two layers:
   * a 1px accent ring (edge definition, replaces a hard border) plus a
   * soft negative-spread halo (ambient separation). Kept subtle by
   * design — this is surface separation, not a decorative outline.
   */
  --glow-card:
    0 0 0 1px color-mix(in oklch, var(--card-glow-source) 30%, transparent),
    0 0 14px -4px color-mix(in oklch, var(--card-glow-source) 35%, transparent);
  --glow-card-hover:
    0 0 0 1px color-mix(in oklch, var(--card-glow-source) 42%, transparent),
    0 0 20px -4px color-mix(in oklch, var(--card-glow-source) 48%, transparent);

  /* Shadcn bridge picks up rebindings automatically via var() chain */
}

/* ============================================================
   BASE — the heading ramp, APPLIED.
   ============================================================
   Added 2026-08-03 after operator review: "borders and heading levels
   unstyled" (conduit) and "Document Management looks all black" (BSU).

   Both were the same omission. --role-h1..h6 existed as tokens and NOTHING
   bound them to an actual h1..h6, so every heading fell through to
   --role-text-body — one flat near-black for all six levels. A ramp nobody
   applies is not a ramp, it is six unused variables, and that is exactly what
   an operator sees as "unstyled".

   Values are the contract's, verbatim (d2c-theme-source-of-truth.html and the
   Corporate equivalent). Each level shifts HUE as well as lightness, so the
   hierarchy survives greyscale and both dichromacies rather than relying on
   size alone.

   Scoped to @layer base so any component class ON THE HEADING ITSELF wins.
   That is a narrower guarantee than it looks, and the earlier version of this
   comment ("it does not fight Tailwind utilities") was simply wrong.

   WHAT IT DOES OVERRIDE: an INHERITED colour. Inheritance is the last resort in
   the cascade, so a rule targeting `h1` beats `text-primary-foreground` sitting
   on an ancestor — specificity never enters into it. Measured on braden
   2026-08-03: a hero `<h1>` inheriting a near-white foreground from its wrapper
   rendered dark navy against a dark scrim, i.e. all but invisible, and it was
   the ONLY heading on the page that moved. That is the signature of this bug —
   one heading changing is not a small problem, it is the coloured-container
   case, and coloured containers are where text goes invisible.

   --heading-color is the escape hatch. Any container that colours its own text
   sets it once and the whole heading subtree defers:
     <div class="text-primary-foreground [--heading-color:currentColor]">
   currentColor there resolves to the container's colour, so headings inherit
   again instead of being repainted. */
@layer base {
  /* See braden.css: the root element inherits the UA default pure black when
     nothing sets it, which is the one banned value at the top of the chain. */
  html { color: var(--role-text-body); background-color: var(--role-bg-base); }

  /* The contract names a family and the package shipped the token, but NOTHING
     bound it to an element — so every app fell back to whatever it happened
     to set itself, which is how crm7 ran Inter against a Geist contract and
     the estate ran four families at once. Same defect as the heading ramp:
     a token nobody applies is not a font, it is a variable. */
  body { font-family: var(--font-body); }
  code, kbd, samp, pre { font-family: var(--font-mono); }

  h1 { color: var(--heading-color, var(--role-h1)); font-size: 25px; font-weight: 700; letter-spacing: var(--track-display); }
  h2 { color: var(--heading-color, var(--role-h2)); font-size: 19px; }
  h3 { color: var(--heading-color, var(--role-h3)); font-size: 16px; }
  h4 { color: var(--heading-color, var(--role-h4)); font-size: 14px; }
  h5 { color: var(--heading-color, var(--role-h5)); font-size: 13px; }
  h6 {
    color: var(--heading-color, var(--role-h6));
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: var(--track-eyebrow);
    font-weight: 700;
  }
}
