/* ============================================================================
   Nova.tokens.css - minimal runtime token file for Blazor apps.
   Import this ONCE from App.razor or _Host.cshtml before any component CSS.
   Derived from colors_and_type.css - this is the handoff subset Claude Code
   should consume and extend. See tokens/nova.tokens.json for the full source.

   v2 additions (22 Apr 2026):
     - Chart-series palette (--nova-chart-1 … --nova-chart-6)
     - Container tints for severity banners (--nova-container-*)
     - Tertiary accents (--nova-accent-teal, --nova-accent-violet)
     - Neutral interstitials (--nova-canvas-rail, --nova-canvas-sunken)

   v2.12 additions (25 Apr 2026):
     - Tone-keyed value text (--nova-value-tone-*) - primary surface
       for stat-card / hero-metric Sub+Tone (SUB-GAP-039)
     - Chart axes / gridlines (--nova-chart-axis, --nova-chart-grid,
       --nova-chart-grid-strong, --nova-chart-tooltip-*)
     - Glass surface chrome shared by NovaGlassPopup + NovaFilterPopup
       (--nova-glass-surface, --nova-glass-border, --nova-glass-blur)
     - Segmented-control track + thumb (--nova-segmented-*)
     - Treemap text-flip threshold + ring track (--nova-ring-track)
     - SUB-GAP-033 reconciliation: --nova-text-secondary,
       --nova-text-muted, --nova-text-primary now have ONE canonical
       value. app.css MUST stop redefining these at :root / dark.
   ============================================================================ */

/* IBM Plex ships INSIDE the package. It used to arrive from fonts.googleapis.com, which was a
   render-blocking cross-origin round trip before the woff2 URLs were even discoverable, an external
   runtime dependency on quaysides and vessels that are not reliably online, and - on the carrier
   portal, where the reader is a third party following a link - a US host being handed their IP from
   a page they did not choose. Regenerate with `node scripts/gen-fonts.mjs`. */
/* ===== BEGIN GENERATED FONT FACES - scripts/gen-fonts.mjs, do not hand-edit ===== */
/* 22 subset files, 575 KB on disk. A browser fetches only the
   subsets whose unicode-range the page actually uses. IBM Plex is SIL OFL 1.1 - see fonts/OFL.txt. */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-400.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-greek-400.woff2') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext-400.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-500.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-greek-500.woff2') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext-500.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-500.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-600.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-greek-600.woff2') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext-600.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-700.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-greek-700.woff2') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext-700.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-700.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-ext-400.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-ext-500.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-500.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-ext-600.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===== END GENERATED FONT FACES ===== */

:root {
  /* Tell the UA which scheme to paint native controls + their popups in, so
     the browser's own calendar/time/select popups and scrollbars render in the
     right theme from the first frame. Without this the native date-picker popup
     flashes black for a frame before repainting (dark-mode "black blink" on
     open). Flipped to dark under [data-theme="dark"] below. */
  color-scheme: light;

  /* ===== CANONICAL BREAKPOINTS (mobile-first, min-width only) ===== */
  --nova-bp-sm: 480px;   /* large phone */
  --nova-bp-md: 768px;   /* small tablet - sidebar returns as 64px rail */
  --nova-bp-lg: 1024px;  /* small laptop - sidebar expands to 260px full */
  --nova-bp-xl: 1280px;  /* desktop */

  /* Palette - raw. Never reference directly from component CSS; use semantic tokens below. */
  --nova-blue-500: #146EF4;
  --nova-blue-600: #0a4bc7;
  --nova-blue-300: #8ab4f8;

  --nova-neutral-0:   #FFFFFF;
  --nova-neutral-50:  #F5F7FA;
  --nova-neutral-100: #FAFBFC;
  --nova-neutral-200: #EBF2FE;
  --nova-neutral-300: #E0E0E0;
  --nova-neutral-500: #555555;
  --nova-neutral-700: #2c2c30;
  --nova-neutral-800: #19191d;
  --nova-neutral-900: #0e0e11;

  --nova-sev-critical: #DC2626;
  --nova-sev-severe:   #ea580c;
  --nova-sev-warning:  #F59E0B;
  --nova-sev-success:  #16A34A;

  --nova-ink-900: #111111;

  /* Semantic - light mode */
  --nova-primary:          var(--nova-blue-500);
  /* Derived, not a second literal. --nova-primary is re-pointed per whitelabel tenant, and a
     hardcoded #0a4bc7 meant a red-branded tenant shipped red buttons that hovered BLUE. The mix
     darkens whatever the accent is, so the hover follows the brand by construction. */
  --nova-primary-hover:    color-mix(in oklab, var(--nova-primary) 82%, black);
  /* Long-standing bug: --nova-accent was declared ONLY in the dark block, so in light mode
     NovaGeoMap's accent route stroke, its marker fill, its focus outline and NovaStateLabel's
     accent dot all resolved to nothing and painted transparent. It is an alias of the accent. */
  --nova-accent:           var(--nova-primary);
  /* v2.6 · SUB-GAP-010 - ink to pair with --nova-primary (fg on blue chips,
     active tabs, filled buttons). Paired with severity families so that
     colored backgrounds never use raw #fff. */
  --nova-on-accent:            #FFFFFF;
  --nova-on-severity-critical: #FFFFFF;
  --nova-on-severity-severe:   #FFFFFF;
  --nova-on-severity-warning:  #FFFFFF;  /* GAP-182 - was #111111 on the raw amber. The fill tier is dark, so the whole tier is white-ink now, which also removes the one-off. */
  --nova-on-severity-success:  #FFFFFF;
  /* SUB-GAP-047 §16 / v2.13a §1 - three orphan tokens declared canonically.
     Components reference these; without declarations CSS falls back silently. */
  --nova-inverse-surface:  #111111;                   /* toasts / dark spinners on light canvas */
  --nova-on-accent-border: rgba(255, 255, 255, 0.32); /* 1px hairline inside filled --nova-primary */
  /* GAP-181 - measured at 2.77:1 (light) / 3.92:1 (dark) over --nova-primary. The description
     used to claim ~6.5:1 and clearing AA; it does not. Legitimate only as DISABLED-state ink,
     which WCAG 1.4.3 exempts, and no component currently uses it. Not ordinary second-tier ink. */
  --nova-on-accent-muted:  rgba(255, 255, 255, 0.64);
  /* The page field sits BELOW the panels that stand on it. Light mode used to paint both
     #FFFFFF, so a card was separated from its page by a 1px border and nothing else, and the
     whole surface read as a wireframe. Dark mode has always had the two tiers (#0e0e11 page /
     #19191d panel); this is light mode catching up, and it is the single edit that most changes
     how considered the system looks. Panels stay pure white so they still read as raised. */
  --nova-canvas:           #F6F7F9;
  --nova-canvas-panel:     #FFFFFF;
  --nova-canvas-panel-alt: var(--nova-neutral-100);
  --nova-canvas-border:    #E5E7EB;

  /* GAP-185 - the input surface tier. Thirteen sites across NovaSelect, NovaTextInput,
     NovaCheckChip, NovaDataGrid and the three date pickers were already written as
     `var(--nova-canvas-input, var(--nova-canvas-panel))`, i.e. against a tier that had never been
     declared. The fallback form hid it: the token gate only ever matched a var() with no fallback,
     so for months every one of those sites silently painted the panel colour while reading as
     though a field tier existed. Declared at the value they were already resolving to, so this
     changes no paint and the tier becomes re-pointable for a tenant or a future dense lane.
     The hover step is the one place they disagreed with each other: two sites resolved to
     panel-alt and one (NovaSelect option hover) to panel. panel-alt wins - a hover IS a step. */
  --nova-canvas-input:       var(--nova-canvas-panel);
  --nova-canvas-input-hover: var(--nova-canvas-panel-alt);
  --nova-border-subtle:    rgba(0,0,0,0.06);
  /* GAP-160 - the 1px INSET rim on a NovaFlag mark and on its code-only plate. Declared in both
     lanes because the failure is symmetric: a pale field (PL, DK, FI, GR, IS, NL, LV, CH) loses its
     edge against the white panel, and a black or navy field (DE, BE, EE, GB, RU, MK) loses its edge
     against the dark one. NOT --nova-border-subtle: that is a surface divider at 0.05-0.06 alpha and
     is too weak to bound a white field. Inset so it never changes the mark's advance width, which
     would break the alignment of a stacked column of lanes. */
  --nova-flag-rim:         rgba(0,0,0,0.12);

  /* GAP-038 NovaGeoMap - self-contained world basemap (light): near-white land on a pale
     sea, hairline coastline, faint graticule. Marker tones reuse --nova-severity-*. */
  --nova-geo-ocean:        #e9eff6;
  --nova-geo-land:         #f8fafc;
  --nova-geo-land-stroke:  #c2ccd8;
  --nova-geo-graticule:    rgba(0,0,0,0.05);
  --nova-geo-link:         var(--nova-accent);
  --nova-geo-place:        #6b7280;

  /* One step below the canvas, not level with it: with the canvas tinted, the old #F5F7FA left
     the sidebar and the page indistinguishable. Same cool-grey family, one interval down. */
  --nova-chrome-surface:   #EFF1F5;
  --nova-chrome-divider:   #EFF1F4;

  /* v2 - neutral interstitials. Sub-surfaces between panel and border.
     Use for: progress-bar rails, chart gridlines, sunken track fills. */
  --nova-canvas-rail:    #EEF1F5;
  --nova-canvas-sunken:  #F0F3F7;

  --nova-text-primary:   var(--nova-ink-900);
  /* SUB-GAP-033 reconciliation - text tier values are CANONICAL here.
     app.css MUST NOT redefine these. Light --nova-text-secondary tightens
     from #555555 → #424655 (4.95:1 → 7.96:1 contrast on canvas-panel-alt
     #FAFBFC). Light --nova-text-muted holds at #727786. */
  --nova-text-secondary: #424655;
  --nova-text-muted:     #696E7D;   /* was #727786: 4.17:1 on the tinted canvas, under AA. Now 4.75:1. */
  /* SUB-GAP-048 §3 - third text tier, sits between secondary (#424655) and
     muted (#727786) on the shipped ladder. Used by NovaTextInput /
     NovaSelect / NovaDatePicker chrome (icons, hover-borders, helper text,
     placeholders, counters). 5.7:1 on canvas-panel - passes AA for
     non-text UI; passes AA Large for body. PRE-v2.12.1 this token was
     referenced in 16 sites without a declaration - chrome rendered with
     inherited currentColor (theme-incorrect). */
  --nova-text-tertiary:  #6b7280;

  --nova-severity-critical: var(--nova-sev-critical);
  --nova-severity-severe:   var(--nova-sev-severe);
  --nova-severity-warning:  var(--nova-sev-warning);
  --nova-severity-success:  var(--nova-sev-success);
  --nova-severity-info:     var(--nova-primary);

  /* GAP-182 - the severity FILL tier. The DS already split the MARK (--nova-severity-*: dots,
     bars, left borders) from the INK (--nova-severity-*-text: anything that colours text),
     because one value cannot do both jobs. This is that lesson a third time: the value that is
     right as a mark is not dark enough to carry white ink. NovaButton --tone-severe shipped
     white on #ea580c at 3.56:1 and --tone-success white on #16A34A at 3.30:1, both under AA,
     on buttons that are in production.

     FILL is what filled chrome uses when it carries --nova-on-severity-* ink, and it is chosen
     for that one job. Darkening the marks instead would have moved roughly twenty surfaces that
     are correct as they are - and would have pushed the amber into orange, where it collides
     with severe. Every value below clears 4.5:1 against white. */
  --nova-severity-critical-fill: #DC2626;  /* white 4.83:1 - already passed, listed so the tier is complete */
  --nova-severity-severe-fill:   #c2410c;  /* white 5.18:1, was #ea580c at 3.56 */
  --nova-severity-warning-fill:  #a16207;  /* white 4.92:1, was #F59E0B at 2.15 with black ink */
  --nova-severity-success-fill:  #15803d;  /* white 5.02:1, was #16A34A at 3.30 */
  --nova-severity-info-fill:     var(--nova-primary);

  /* The severity INK tier. The raw --nova-severity-* values are fills and marks; as small text on
     a light surface they do not clear AA (warning 2.15:1, success 3.30:1, severe 3.56:1 at 10-12px).
     Anything that COLOURS TEXT reaches for these instead; dots, bars and fills keep the raw token.
     Severe was missing entirely, so NovaButton fell back to the raw orange (3.56:1). Info is
     derived because info IS the accent, and the accent is whitelabelled. */
  --nova-severity-critical-text: #b91c1c;
  --nova-severity-severe-text:   #9a3412;
  --nova-severity-warning-text:  #a16207;
  --nova-severity-success-text:  #15803d;
  --nova-severity-info-text:     color-mix(in oklab, var(--nova-severity-info) 72%, black);

  /* GAP-119 (light): categorical lifecycle status ramp for NovaIdentityBadge
     Variant=Status / StatusTone. Its OWN tokens (NOT --nova-severity-* / not
     --nova-primary) so eight shipment-lifecycle stages read consistently under
     any tenant hue and don't collide with severity re-tuning. `<stage>` = the
     dot fill (saturated); `<stage>-ink` = the text (darker, AA on canvas-panel).
     Vocabulary is generic (a domain maps its statuses -> nearest stage). */
  --nova-status-draft:         #64748b;  --nova-status-draft-ink:     #475569;  /* not started (slate) */
  --nova-status-pending:       #d97706;  --nova-status-pending-ink:   #b45309;  /* booked, awaiting action (amber) */
  --nova-status-approved:      #0d9488;  --nova-status-approved-ink:  #0f766e;  /* confirmed / go (teal) */
  --nova-status-transit:       #2563eb;  --nova-status-transit-ink:   #1d4ed8;  /* loaded / on the water (blue) */
  --nova-status-arrived:       #6366f1;  --nova-status-arrived-ink:   #4f46e5;  /* discharged at destination (indigo) */
  --nova-status-completed:     #16a34a;  --nova-status-completed-ink: #15803d;  /* received / delivered (green) */
  --nova-status-cancelled:     #dc2626;  --nova-status-cancelled-ink: #b91c1c;  /* cancelled (red) */
  --nova-status-archived:      #94a3b8;  --nova-status-archived-ink:  #64748b;  /* removed / soft-deleted (recessive slate) */

  /* GAP-013: NovaProgressRowList / NovaProgressBar over-capacity (>100%) treatment.
     Replaces the fill colour on a bar whose value exceeds 100% and feeds the hatched
     overflow tip past the 100% reference line, so an over-capacity bar reads as a
     distinct measurement regime without relying on the % text.
     Deeper than --nova-severity-critical on purpose: over-capacity is a distinct
     measurement fact (a hard limit crossed), not a generic critical state. */
  --nova-over-capacity:        #b42318;
  --nova-over-capacity-hatch:  #7a1c12;
  --nova-over-capacity-text:   #b42318;

  /* v2 - container tints. Only valid use: the body fill of a status banner
     that itself is NOT a row. Row-level severity is still 3px left border
     + colored text + dot - do not tint rows. These are for standalone
     inline notices (e.g. form submission banner, empty-state-with-cause). */
  --nova-container-success:  rgba(22, 163, 74, 0.08);
  --nova-container-warning:  rgba(245, 158, 11, 0.08);
  --nova-container-severe:   rgba(234, 88, 12, 0.08);
  --nova-container-critical: rgba(220, 38, 38, 0.08);
  --nova-container-info:     rgba(20, 110, 244, 0.08);

  /* v2 - tertiary accents. Use ONLY for secondary data series and chart
     categoricals. Never for brand, never for severity. */
  --nova-accent-teal:   #0EA5A3;
  --nova-accent-violet: #7C5CE0;

  /* v2 - chart-series palette. Qualitative 6-color set, ordered by
     scan-priority. Use in index order: first category = chart-1, etc.
     These are the ONLY allowed colors for bars/lines/donut segments.
     Dark-mode values re-point below. */
  --nova-chart-1: #146EF4;  /* brand blue */
  --nova-chart-2: #0F766E;  /* GAP-182 - was #0EA5A3 at 3.03:1, a hair over the non-text minimum. Deeper teal, 5.47:1. */
  --nova-chart-3: #A21CAF;  /* GAP-182 - was #7C5CE0 violet, which sat 0.034 from chart-1 in Oklab under deuteranopia: two of the first three series were the same colour to ~8% of men. Fuchsia clears 0.10 against every other series under all three deficiencies. */
  --nova-chart-4: #B45309;  /* GAP-182 - was #F59E0B at 2.15:1 on the panel, under the 3:1 non-text minimum, in a role where the colour ALONE identifies the series. 5.02:1. */
  --nova-chart-5: #9F1239;  /* GAP-182 - was #E1643F coral, which collided with the darkened chart-4. */
  --nova-chart-6: #334155;  /* GAP-182 - deeper slate, further from chart-1 under every deficiency. */

  --nova-signal-surface: var(--nova-ink-900);
  --nova-signal-ink:     #FFFFFF;

  /* §SUB-GAP-001 - muted text tiers for the always-opposite-of-canvas signal
     surface. These tokens invert with --nova-signal-surface (not with canvas),
     so consumers don't hand-roll rgba() on hex values. */
  --nova-signal-text-muted:  rgba(255, 255, 255, 0.72);
  --nova-signal-text-subtle: rgba(255, 255, 255, 0.56);

  /* v2.11 · SUB-GAP-021/022 - canvas-inverted focal surface family for
     NovaSignalCard + NovaPredictorCard. Separate from --nova-signal-surface
     because signal-surface inverts with canvas (dark ↔ light), whereas
     these go DEEPER than canvas in dark mode so the focal surface still
     contrasts against dark chrome. Flat - no gradient, no colored shadow. */
  --nova-signal-canvas:     #0e1016;
  --nova-signal-canvas-alt: #1a1d25;
  --nova-signal-hairline:   rgba(255, 255, 255, 0.08);

  /* The canvas is ALWAYS dark (both modes), so its text is always light. These
     are mode-invariant on purpose (no dark override) and exist because
     --nova-signal-ink / -text-muted / -text-subtle invert with
     --nova-signal-surface, which would collapse to dark-on-dark on the canvas in
     dark mode. NovaPredictorCard / NovaSignalCard use these. */
  --nova-signal-canvas-ink:         #FFFFFF;
  --nova-signal-canvas-text-muted:  rgba(255, 255, 255, 0.72);
  --nova-signal-canvas-text-subtle: rgba(255, 255, 255, 0.56);

  /* v2.11 · SUB-GAP-026 - 5th-tier aging severity. One step deeper than
     --nova-severity-critical for 5-band aging palettes (0-7d / 8-30d /
     31-60d / 61-90d / 90d+). Not a replacement for --nova-severity-critical. */
  --nova-severity-critical-deep: #991b1b;

  /* §SUB-GAP-005 - promoted tokens. Legacy names (--nova-occ-press,
     --nova-canvas-inset-shadow) live here under canonical semantic names. */
  --nova-state-pressed:        rgba(0, 0, 0, 0.06);
  --nova-canvas-inset-shadow:  inset 0 1px 2px rgba(0, 0, 0, 0.04);

  /* GAP-185 - the accent wash, and a live derive-the-accent violation until now.
     NovaRotationGrid asks for it twice, under a comment that says in so many words "primary glyph
     over a soft wash". The token was never declared, so both sites fell through to
     --nova-state-pressed and painted NEUTRAL GREY under a primary-coloured glyph. On a
     red-branded tenant the glyph is red and the wash it stands on is grey: the exact failure
     derive-the-accent was written for. Derived with color-mix like every other accent descendant,
     so it follows the tenant. This one DOES change paint, and that is the point. */
  --nova-primary-soft: color-mix(in oklab, var(--nova-primary) 10%, transparent);

  /* GAP-180 - the focus ring, as a token family. It was written out literally in 65 places
     (`outline: 2px solid var(--nova-primary)`) across 47 component stylesheets, so there was no
     single place to change it and no way for a whitelabel tenant to re-point the ring WITHOUT
     re-pointing the accent it is drawn from. Benchmarked against Carbon, which declares three
     (--cds-focus, --cds-focus-inset, --cds-focus-inverse); we need two.

     --nova-focus tracks --nova-primary, so this ships byte-identical and a tenant inherits the
     ring from their accent by default. It is a SEPARATE token precisely so a tenant whose accent
     is too pale to clear 3:1 against the canvas can override the ring alone.

     --nova-focus-inset is the ring on accent-FILLED chrome, where an accent ring would be
     invisible. NovaPipelineCard already needed this and hand-rolled `var(--nova-on-accent, #fff)`
     because there was nothing to reach for. Both are declared once and resolve per theme at use,
     the same way --nova-accent does. */
  --nova-focus:              var(--nova-primary);
  --nova-focus-inset:        var(--nova-on-accent);
  --nova-focus-width:        2px;

  /* Three offsets, three rules, replacing five ad-hoc values (2px / 1px / -1px / -2px / -3px)
     picked per site. Which one applies is a property of the SURFACE, not of the component:
       offset       - the control has room around it. The default.
       offset-tight - the control is packed against neighbours (calendar days, steppers, in-field
                      glyphs), where 2px collides with the next target.
       offset-inset - the surface has no gutter to bleed into (rows, cells, tabs, panels), so the
                      ring is drawn inside its own box. */
  --nova-focus-offset:       2px;
  --nova-focus-offset-tight: 1px;
  --nova-focus-offset-inset: -2px;

  /* Type */
  --nova-font-sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --nova-font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Spacing / radii / motion */
  /* GAP-184 - the spacing SCALE. Before this, 875 of 1170 spacing declarations were literal
     pixels across 32 distinct values, and only 125 used a token: the five-rung t-shirt ladder
     below was public API that the DS itself had almost stopped using. That is why there was no
     system-wide density - a compact lane had nothing to re-point.

     Two layers, the same shape as palette -> semantic elsewhere in this file:
       SCALE (here)  - literal, never re-pointed by anything. Replaces a literal px.
       ROLE (below)  - what a thing IS (a row's vertical padding, a card's inset). Points at a
                       scale rung, and is the ONLY layer density re-points.

     A 2px grid, not 4px. 4px is the industry default and it does not fit this corpus: only 52%
     of occurrences sit on a 4px grid against 90% on a 2px one, because 6px, 10px and 14px carry
     275 uses between them in the dense views. Forcing a 4px grid would have moved 46% of the
     system to make the ladder tidier, which is the tail wagging the dog. It widens to 4px above
     16px, where a 2px step stops being a distinction anyone can see. */
  --nova-space-1:  1px;
  --nova-space-2:  2px;
  --nova-space-3:  3px;
  --nova-space-4:  4px;
  --nova-space-6:  6px;
  --nova-space-8:  8px;
  --nova-space-10: 10px;
  --nova-space-12: 12px;
  --nova-space-14: 14px;
  --nova-space-16: 16px;
  --nova-space-18: 18px;
  --nova-space-20: 20px;
  --nova-space-24: 24px;
  --nova-space-28: 28px;
  --nova-space-32: 32px;
  --nova-space-48: 48px;

  /* The t-shirt names are consumer API (Nova and Atlas use them) and stay. They are now aliases
     onto the scale so there is one set of values, not two. */
  --nova-space-xs: var(--nova-space-4);
  --nova-space-sm: var(--nova-space-8);
  --nova-space-md: var(--nova-space-16);
  --nova-space-lg: var(--nova-space-24);
  --nova-space-xl: var(--nova-space-32);

  /* GAP-184 - spacing ROLES. A role says what the space is FOR, which is the only thing a
     density mode can reason about: "a table row's vertical padding" can be compacted, "the gap
     between a label and its error message" cannot. Components reach for a role where one fits and
     a scale rung otherwise. These are the ONLY tokens the compact lane re-points; the scale never
     moves, so a rung named 12 is always 12.

     Every value below is the value those surfaces already used, so adopting a role changed nothing
     on screen. Deliberately NOT named page / section / stack: those three are already taken by the
     fluid page-spacing clamps further down and mean something else. */
  --nova-space-row-y:     var(--nova-space-12);  /* table + data-grid body row, vertical */
  --nova-space-row-x:     var(--nova-space-12);  /* table + data-grid body row, horizontal */
  --nova-space-head-y:    var(--nova-space-10);  /* header row, vertical */
  --nova-space-cell-y:    var(--nova-space-6);   /* matrix / spreadsheet cell, tighter than a row */
  --nova-space-cell-x:    var(--nova-space-10);
  --nova-space-card:      var(--nova-space-20);  /* card + panel inset */
  --nova-space-card-gap:  var(--nova-space-12);  /* between blocks inside a card */
  --nova-space-control-x: var(--nova-space-12);  /* inside a button / input / chip */
  --nova-space-control-y: var(--nova-space-8);
  --nova-space-inline:    var(--nova-space-6);   /* between inline siblings: glyph + label */
  --nova-space-grid-x:    var(--nova-space-8);   /* spreadsheet-grid cell (NovaBulkGrid), tighter again */
  --nova-row-h:           32px;                  /* fixed-height grid row (NovaBulkGrid) */

  --nova-radius-sm: 4px;
  --nova-radius-md: 8px;
  --nova-radius-pill: 9999px;
  /* The phone bottom-sheet corner. Seven surfaces slide up from the bottom edge on a phone
     (dialog, modal, menu, select, autocomplete, filter popup, glass popup) and all round only
     their top corners. Larger than --nova-radius-md on purpose: at full bleed an 8px corner
     reads as a rendering artefact rather than as a sheet. Named so it stops looking like drift
     against the max-corner-radius ladder. */
  --nova-radius-sheet: 16px;

  --nova-shadow-sm: 0 1px 3px rgba(0,0,0,0.07);
  /* v2.13b · SUB-GAP-033 / roadmap B2 - migrated from app.css to canonical token source. */
  --nova-shadow-lg: 0 4px 16px rgba(0,0,0,0.08);
  --nova-shadow-xl: 0 24px 48px rgba(25,28,30,0.12), 0 4px 12px rgba(25,28,30,0.06);

  --nova-transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);

  /* GAP-185 - the bare DURATION, for the call sites that supply their own easing word.
     --nova-transition carries duration AND curve together, which is right when a rule wants the
     house curve, but 20 declarations across 12 stylesheets instead wrote
     `transition: background var(--nova-motion-fast, 150ms) ease` - a token that did not exist,
     hidden from the gate by its fallback. Every one of them has been running on the literal since
     it was written, which is also why three of them drifted to 120ms: nothing connected them.
     Declared here at 150ms so the whole set is re-pointable and reduced-motion can reach it.
     The easing those sites use (`ease`) still differs from the house cubic-bezier; unifying the
     CURVE is a visual decision and is deliberately NOT folded into this pass. */
  --nova-motion-fast: 150ms;

  /* Runtime-set inline by NovaRiskHighlightCard - declared with a default
     so the validator's token-completeness check passes and the CSS has a
     sensible fallback if a consumer omits the inline style. */
  --nova-risk-tone: var(--nova-severity-severe);

  /* GAP-185 - the other six runtime knobs, on the same rule as --nova-risk-tone above. Each is
     written inline from a component parameter (NovaCheckChip Accent, NovaCompositionCard
     BarHeight, NovaTallyCardRow FluidMinColumn and RowLockPx, NovaShareMatrix trade slot,
     NovaSelect MaxHeight) and each was referenced ONLY in the fallback form, so the gate never
     saw them and they were indistinguishable from the four phantom tokens found alongside them.
     Declared at exactly the default each site was already falling back to: no paint changes,
     and a reader of tokens.css can now see the full knob surface in one place. */
  --nova-chip-accent:      var(--nova-primary);
  --nova-comp-bar-height:  10px;
  --nova-tcr-fluid-min:    20rem;
  --nova-tcr-rowlock:      auto;
  --nova-sm-trade:         var(--nova-text-secondary);
  --nova-sm-max-height:    72vh;

  /* Runtime-set inline by NovaTable (declarative mode) - defaults so the
     CSS resolves without inline style and the validator passes. The
     declarative path always sets these inline; values here are fallback. */
  --nova-tablev2-grid: auto;
  --nova-tablev2-pad: 20px;
  --nova-tablev2-rowpad: 12px;
  --nova-tablev2-maxheight: none;
  /* GAP-204 - the severity ink a highlighted row hands to its own label. Set inline per row from
     the five judged NovaTone members, which is why the default here is `inherit`: an unhighlighted
     row, and a row highlighted with an unjudged swatch like Teal, must draw its label exactly as
     any other row does. */
  --nova-tablev2-highlight-ink: inherit;

  /* v2.13b · SUB-GAP-049 §3 - single source of truth for the icon-only sidebar
     rail width. Used by both `.nova-shell--sidebar-collapsed` (manual collapse)
     and the `@media (max-width: 900px)` auto-collapse so widths don't double-jump
     on resize. Layout token, not theme-driven (same value light + dark). */
  --nova-sidebar-collapsed-width: 64px;

  /* v2.11 · SUB-GAP-022 - LIVE indicator on NovaPredictorCard. Slower than
     spinner cadence (900ms) so it reads as breathing, not spinning. */
  --nova-motion-predictor-pulse-duration: 1800ms;

  --nova-content-max-width: 1400px;

  /* ===== SHELL TOKENS - three-tier responsive ===== */
  --nova-appbar-height: 56px;       /* phone */
  --nova-sidebar-width: 0;           /* phone - drawer is overlay, no inline rail */
  --nova-sidebar-collapsed-width: 64px; /* retained for the rail at md tier */

  /* v2.7 · SUB-GAP-015 - Phase B motion durations. Long-running
     indeterminate animations that don't fit --nova-transition (150ms).
     Same value in light and dark. */
  --nova-motion-spinner-duration: 900ms;
  --nova-motion-progress-indeterminate-duration: 1400ms;
  --nova-motion-skeleton-duration: 1600ms;

  /* v2.7 · SUB-GAP-015 - Scrim + z-scale. Scrim is the NovaOverlay
     fill; z-scale orders popover (70) → scrim (80) → dialog (90), with
     10-point gaps leaving room for toasts / drawer content without
     renumbering. */
  --nova-scrim:         rgba(0, 0, 0, 0.38);
  --nova-scrim-strong:  rgba(0, 0, 0, 0.60);
  --nova-z-popover:     70;
  --nova-z-scrim:       80;
  --nova-z-dialog:      90;

  /* ───────── v2.12 (25 Apr 2026) ───────── */

  /* SUB-GAP-039 - tone-keyed value text. The focal number in a stat card
     or hero metric inherits one of these instead of the generic text-primary,
     so callers can encode "open repairs is bad and rising" via Tone="Critical"
     without per-page CSS. NEVER use these for body text - value-class only.
     Pairs visually with --nova-severity-*-text but slightly darker so the
     focal number reads as gravity, not a chip. */
  --nova-value-tone-neutral:  var(--nova-text-primary);
  --nova-value-tone-good:     #15803d;
  --nova-value-tone-warning:  #92400e;
  --nova-value-tone-severe:   #c2410c;
  --nova-value-tone-critical: #991b1b;
  --nova-value-tone-info:     var(--nova-blue-600);

  /* SUB-GAP-035 - chart axes / gridlines / tooltip chrome. Quieter than
     canvas-border because they live BEHIND data. Tooltip is shared with
     NovaGlassPopup family so chart hover-cards feel native. */
  --nova-chart-axis:           rgba(0, 0, 0, 0.18);
  --nova-chart-grid:           rgba(0, 0, 0, 0.06);
  --nova-chart-grid-strong:    rgba(0, 0, 0, 0.10);
  --nova-chart-tooltip-bg:     rgba(255, 255, 255, 0.96);
  --nova-chart-tooltip-border: rgba(0, 0, 0, 0.08);
  --nova-chart-empty-stroke:   rgba(0, 0, 0, 0.10);

  /* SUB-GAP-027/030 - glass-family chrome shared by NovaGlassPopup
     (hover tooltip) and NovaFilterPopup (click-open filter panel). Both
     resolve to ONE source of truth so the surface family stays unified. */
  --nova-glass-surface:        rgba(255, 255, 255, 0.88);
  --nova-glass-surface-strong: rgba(255, 255, 255, 0.94);
  --nova-glass-border:         rgba(0, 0, 0, 0.08);
  --nova-glass-blur:           blur(12px) saturate(140%);

  /* SUB-GAP-037 - segmented control. Track is sunken.
     GAP-199: the active segment is the ACCENT, not a lifted panel. A segmented control
     answers "which one of these is on", and the NovaToggle standing beside it in every
     filter bar already answers that question in blue - so one strip stated the same state
     two different ways, and the quieter of the two lost. --nova-primary over
     --nova-on-accent is the pair NovaButton primary already ships: measured 4.64:1 for
     12px/600 ink (AA floor 4.5:1) and 4.3:1 for the thumb against the track (1.4.11 floor
     3:1 for a component edge). Both lanes follow --nova-primary, so neither is hardcoded.
     The ring goes transparent rather than away: a 1px border existed to detach a
     panel-coloured thumb from a panel-coloured neighbourhood, and a filled accent needs no
     help - but a consumer overriding this family should not meet a missing var. */
  --nova-segmented-track:      var(--nova-canvas-sunken);
  --nova-segmented-thumb:      var(--nova-primary);
  --nova-segmented-thumb-ink:  var(--nova-on-accent);
  --nova-segmented-thumb-ring: transparent;

  /* GAP-199: hover is not a dimmer version of selected. The stepper arrows used to hover
     onto --nova-segmented-thumb, which now MEANS selected, so hover takes its own rung -
     the panel tone the thumb just vacated, which is what hover already looked like. */
  --nova-segmented-hover:      var(--nova-canvas-panel);

  /* SUB-GAP-040 - radial-progress ring track (the un-filled portion of
     a NovaRadialProgress arc). Quieter than canvas-rail because rings sit
     INSIDE cards and need to recede further. */
  --nova-ring-track:           var(--nova-canvas-rail);

  /* SUB-GAP-036 - treemap ink-flip threshold. Tiles whose primary-tint
     opacity exceeds this value flip foreground from --nova-text to
     --nova-on-accent. Mode-invariant; consumers read this and branch. */
  --nova-treemap-ink-flip-at:  0.55;

  /* SUB-GAP-035 - chart series stroke widths + dash patterns. Tokenized
     so all chart primitives draw lines at the same weight without page-CSS. */
  --nova-chart-stroke:         2px;
  --nova-chart-stroke-strong:  2.5px;
  --nova-chart-dash:           6 3;

  /* ───────── v2.14 (GAP-003…006) ───────── */

  /* GAP-005: shared control heights. The input family (NovaTextInput,
     NovaNumberInput, NovaSelect) sizes off these so a row of controls aligns.
     Ported from Nova v2.9 (SUB-GAP-018); NovaTextInput already references them
     via var(--nova-control-h-md, 36px) fallbacks; now canonical. Mode-invariant. */
  --nova-control-h-sm: 28px;
  --nova-control-h-md: 36px;
  --nova-control-h-lg: 44px;

  /* GAP-005: fourth control radius. The checkbox/grid-cell tier: 2px, one
     step tighter than --nova-radius-sm (4px, inputs/buttons). INPUTS.md caps
     the checkbox box at "no rounded > 2px"; this is the canonical value so
     NovaCheckbox / NovaBulkGrid cells never hardcode it. Mode-invariant. */
  --nova-radius-xs: 2px;

  /* GAP-004: NovaLightbox immersive chrome. A photo viewer is always a
     dark, immersive surface REGARDLESS of app theme (the photo, not the UI,
     is the figure), so these are mode-invariant, no dark override. The
     backdrop is near-opaque, far heavier than --nova-scrim (0.38) which is
     for dialogs that keep page context visible underneath. */
  --nova-lightbox-scrim:         rgba(0, 0, 0, 0.92);
  --nova-lightbox-surface:       rgba(255, 255, 255, 0.10); /* nav buttons, caption bar */
  --nova-lightbox-surface-hover: rgba(255, 255, 255, 0.18);
  --nova-lightbox-ink:           #FFFFFF;                   /* captions, controls */
  --nova-lightbox-ink-muted:     rgba(255, 255, 255, 0.64); /* counter, EXIF meta */

  /* GAP-006: NovaBulkGrid cell selection. The range / multi-cell selection
     fill + its rectangle outline (also the fill-handle anchor). Theme-keyed
     (re-pointed in dark below) so the translucent brand wash reads on both
     canvases. The sticky header reuses --nova-canvas-rail via the alias. */
  --nova-grid-selection:        rgba(20, 110, 244, 0.12);
  --nova-grid-selection-border: rgba(20, 110, 244, 0.40);
  --nova-grid-header-bg:        var(--nova-canvas-rail);    /* alias: mode-invariant */

  /* GAP-009: NovaHorizonBoard resting voyage-bar fill. A horizon board is ONE
     calm field where the SELECTED voyage must pop, so every resting bar shares
     a single translucent brand wash and selection flips to solid --nova-primary
     + a lift (not six chart hues competing). Theme-keyed (re-pointed in dark so
     the wash registers on the dark lane); the bar radius reuses --nova-radius-sm. */
  --nova-horizon-bar:           rgba(20, 110, 244, 0.14);

  /* =====================================================================
     TYPE LADDER (GAP-177) - every font-size in the DS resolves here.
     =====================================================================
     Two things were wrong. 616 font-size declarations across ~90 component
     stylesheets were literal pixels, so none of them answered the reader: a
     px size does not grow when someone raises their browser text size or
     runs a scaled display, and browser zoom - which also enlarges chrome
     and images - was their only lever. And they landed on 28 different
     values, ten of them between 7px and 13px, so "the size below body" was
     whatever the last person typed.

     Every rung is rem, so the rendered size follows the reader, and every
     rung is named for what it renders at the 16px browser default, so the
     CSS still reads in the units the design is drawn in.

     SIXTEEN rungs, and every one of them earns its place. The twelve that
     were dropped (7, 8, 8.5, 9, 9.5, 10.5, 11.5, 12.5, 15, 17, 26, 28) were
     snapped onto their neighbours by what the element IS, the way GAP-175
     snapped the radius ladder - never by arithmetic rounding. Two calls
     worth knowing about: a `--large` control lands on 16 rather than 15
     because 16px is also the floor below which iOS zooms the page on focus,
     and the sort caret left the ladder entirely for `0.65em`, because two
     aria-hidden triangles are an icon and an icon should scale with the
     label it sits beside, not with the root.

     The text ladder, by weight of use. Six rungs carry 572 of the 613 sites:
       10 (146) micro label, uppercase tag, axis tick, badge
       11 (136) column head, chip, meta, grid label
       12 (107) secondary text, subtitle, help, hint sentence
       13 (101) dense body: table cell, input, listbox option
       14  (57) body, card title, calendar day
       16  (25) section head, dialog and modal title, large control
     Then display, where each rung is a different object rather than a
     different size: a unit suffix, a summary value, a stat, a KPI, a hero.
     ===================================================================== */
  --nova-fs-10:  0.625rem;    /* 10px */
  --nova-fs-11:  0.6875rem;   /* 11px */
  --nova-fs-12:  0.75rem;     /* 12px */
  --nova-fs-13:  0.8125rem;   /* 13px */
  --nova-fs-14:  0.875rem;    /* 14px */
  --nova-fs-16:  1rem;        /* 16px */
  --nova-fs-18:  1.125rem;    /* 18px  - panel title, picker nav */
  --nova-fs-20:  1.25rem;     /* 20px  - unit suffix, hotspot title */
  --nova-fs-22:  1.375rem;    /* 22px  - summary value */
  --nova-fs-24:  1.5rem;      /* 24px  - stat value */
  --nova-fs-30:  1.875rem;    /* 30px  - page title (--nova-text-page-title) */
  --nova-fs-32:  2rem;        /* 32px  - mini KPI */
  --nova-fs-36:  2.25rem;     /* 36px  - KPI value (--nova-text-kpi-value) */
  --nova-fs-48:  3rem;        /* 48px  - hero metric md */
  --nova-fs-64:  4rem;        /* 64px  - hero metric lg */
  --nova-fs-80:  5rem;        /* 80px  - hero metric xl */

  /* ===== FLUID TYPE RAMP (mobile-first via clamp) ===== */
  --nova-text-h1: clamp(1.5rem, 1rem + 2vw, 2.25rem);
  --nova-text-h2: clamp(1.25rem, 0.9rem + 1.5vw, 1.75rem);
  --nova-text-h3: clamp(1.125rem, 0.95rem + 0.75vw, 1.375rem);
  --nova-text-body: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
  --nova-text-caption: var(--nova-fs-13); /* legibility floor; fixed */

  /* ===== FLUID PAGE SPACING ===== */
  --nova-space-page: clamp(12px, 2vw + 4px, 32px);
  --nova-space-section: clamp(16px, 1vw + 12px, 24px);
  --nova-space-stack: clamp(8px, 0.5vw + 6px, 12px);

  /* ===== TOUCH TARGET FLOOR ===== */
  --nova-touch-min: 44px;
}


/* GAP-184 - the COMPACT lane. One attribute on the app root:
       <div data-nova-density="compact">   or   html[data-nova-density="compact"]
   and every dense surface tightens at once. This is the whole point of splitting scale from
   role: only the roles below move, so a rung named 12 is still 12 and nothing that measured
   itself against the scale drifts.

   Benchmarked against Cloudscape, which reduces its spacing scale in increments of 4 on the
   same base unit, and - the part that matters - deliberately does NOT apply compact to two
   kinds of surface:

     Readability   informational components. Alerts, notices, toasts, empty states, helper and
                   error text. People are READING there, and a consumer flipping one attribute
                   must not be able to make a validation message hard to read. Those surfaces
                   use the scale directly, never a role, so this lane cannot reach them.
     Interactivity anything already a small target. Controls keep their height because
                   --nova-control-h-* is deliberately NOT in this lane, which also keeps the
                   GAP-149 44px coarse-pointer floor intact for free. Only the padding INSIDE
                   a control tightens, and only horizontally.

   What it does reach is the full-page data-intensive views this was asked for: the schedule
   matrix, the bulk grid, the data grid, the rotation grid and the tables. A body row goes from
   12px of vertical padding to 6px and a fixed grid row from 32px to 24px, so roughly 40% more
   rows land on the same screen. */
[data-nova-density="compact"] {
  --nova-space-row-y:     var(--nova-space-6);
  --nova-space-row-x:     var(--nova-space-8);
  --nova-space-head-y:    var(--nova-space-6);
  --nova-space-cell-y:    var(--nova-space-2);
  --nova-space-cell-x:    var(--nova-space-6);
  --nova-space-card:      var(--nova-space-12);
  --nova-space-card-gap:  var(--nova-space-8);
  --nova-space-control-x: var(--nova-space-8);
  --nova-space-grid-x:    var(--nova-space-4);
  --nova-row-h:           24px;
  /* --nova-space-control-y and --nova-space-inline are NOT re-pointed: the first is what keeps
     a control's label off its border, the second is the gap between a glyph and its word. */
}

@media (min-width: 768px) {
  :root {
    --nova-appbar-height: 64px;
    --nova-sidebar-width: 64px;     /* md - compact rail */
  }
}

@media (min-width: 1024px) {
  :root {
    --nova-sidebar-width: 260px;    /* lg+ - full sidebar */
  }
}

[data-theme="dark"],
.mud-theme-dark {
  color-scheme: dark;   /* native controls + popups paint dark from frame one (no black blink) */
  --nova-primary:          var(--nova-blue-300);
  /* v2.6 · SUB-GAP-010 - on-accent families (dark theme). Dark-tint blue
     is pale enough that near-black ink reads better than white. Severity
     backgrounds stay black-ink on warning, white-ink elsewhere. */
  --nova-on-accent:            #0e0e11;
  --nova-on-severity-critical: #111111;
  --nova-on-severity-severe:   #111111;
  --nova-on-severity-warning:  #111111;
  --nova-on-severity-success:  #111111;
  /* SUB-GAP-047 §16 / v2.13a §1 - orphan tokens (dark). inverse-surface flips
     to white in dark mode; on-accent-* flip to dark hairline / dark muted. */
  --nova-inverse-surface:  #FFFFFF;
  --nova-on-accent-border: rgba(0, 0, 0, 0.18);
  --nova-on-accent-muted:  rgba(0, 0, 0, 0.56);
  --nova-canvas:           var(--nova-neutral-900);
  --nova-canvas-panel:     var(--nova-neutral-800);
  --nova-canvas-panel-alt: #131316;
  --nova-canvas-border:    rgba(255,255,255,0.07);
  --nova-border-subtle:    rgba(255,255,255,0.05);
  --nova-flag-rim:         rgba(255,255,255,0.16);   /* GAP-160 - see the light lane */

  /* GAP-038 NovaGeoMap - self-contained world basemap (dark): deep sea, raised land,
     low-contrast coastline + graticule so vessel markers carry the signal. */
  --nova-geo-ocean:        #0b1018;
  --nova-geo-land:         #232f44;
  --nova-geo-land-stroke:  #3c4a63;
  --nova-geo-graticule:    rgba(255,255,255,0.05);
  --nova-geo-link:         var(--nova-accent);
  --nova-geo-place:        #9ca3af;

  --nova-chrome-surface:   var(--nova-neutral-900);
  --nova-chrome-divider:   rgba(255,255,255,0.07);

  /* v2 - neutral interstitials (dark) */
  --nova-canvas-rail:    rgba(255,255,255,0.06);
  --nova-canvas-sunken:  rgba(255,255,255,0.03);

  --nova-text-primary:   #e8eaed;
  /* SUB-GAP-033 reconciliation - dark tier values CANONICAL here.
     --nova-text-secondary lifts from #9aa0a6 → #b3b8c0 (3.8:1 → 5.9:1 on
     canvas-panel #19191d, AA threshold). --nova-text-muted lifts from
     #6b7280 → #9ca3af (failed AA at 3.4:1 → passes at 5.0:1). */
  --nova-text-secondary: #b3b8c0;
  --nova-text-muted:     #9ca3af;
  /* SUB-GAP-048 §3 (dark) - third text tier. 5.0+ headroom on canvas-panel
     because consumer sites include NovaDatePicker helper text and
     NovaTextInput hover-border (body-text use, not just icon/border). */
  --nova-text-tertiary:  #8a909c;

  --nova-severity-critical: #f28b82;
  --nova-severity-severe:   #fb923c;
  --nova-severity-warning:  #fbbf24;
  --nova-severity-success:  #4ade80;
  --nova-severity-info:     var(--nova-blue-300);

  /* GAP-182 (dark) - the dark marks are already light enough to carry the near-black
     --nova-on-severity-* ink (7.9:1 to 11.3:1), so the fill tier points straight at them.
     The tier exists in both lanes so a component never has to branch on theme. */
  --nova-severity-critical-fill: #f28b82;
  --nova-severity-severe-fill:   #fb923c;
  --nova-severity-warning-fill:  #fbbf24;
  --nova-severity-success-fill:  #4ade80;
  --nova-severity-info-fill:     var(--nova-blue-300);

  /* Dark never declared a hover, so it inherited the LIGHT literal: a primary button on the dark
     canvas went from #8ab4f8 to #0a4bc7 under the cursor while its ink stayed --nova-on-accent
     (#0e0e11), i.e. near-black on navy at 2.61:1 - the label disappeared on hover. On a dark canvas
     the accent lifts toward white rather than sinking toward black. 10.1:1 against the ink. */
  --nova-primary-hover:     color-mix(in oklab, var(--nova-primary) 88%, white);

  /* The -text ink tier had NO dark declaration, so 31 component stylesheets - every inline
     validation message on NovaTextInput / NovaDatePicker / NovaDateTimePicker, every delta chip,
     every toned button - painted the LIGHT ink on the dark canvas: critical 2.71:1, success
     3.49:1, warning 3.56:1. On dark the raised severity value is already the correct ink
     (7.3:1 and up on the panel), so the tier collapses onto it rather than inventing a
     fourth set of reds. */
  --nova-severity-critical-text: var(--nova-severity-critical);
  --nova-severity-severe-text:   var(--nova-severity-severe);
  --nova-severity-warning-text:  var(--nova-severity-warning);
  --nova-severity-success-text:  var(--nova-severity-success);
  --nova-severity-info-text:     var(--nova-severity-info);

  /* GAP-119 (dark): categorical lifecycle status ramp. Dot lightens (lightness =
     intensity on dark), ink lighter still so it stays AA-legible on the badge's
     dark panel-alt surface (severity -text tokens have no dark override; this
     ramp defines both modes explicitly to avoid inheriting that gap). */
  --nova-status-draft:         #94a3b8;  --nova-status-draft-ink:     #cbd5e1;
  --nova-status-pending:       #fbbf24;  --nova-status-pending-ink:   #fcd34d;
  --nova-status-approved:      #2dd4bf;  --nova-status-approved-ink:  #5eead4;
  --nova-status-transit:       #60a5fa;  --nova-status-transit-ink:   #93c5fd;
  --nova-status-arrived:       #a5b4fc;  --nova-status-arrived-ink:   #c7d2fe;
  --nova-status-completed:     #4ade80;  --nova-status-completed-ink: #86efac;
  --nova-status-cancelled:     #f87171;  --nova-status-cancelled-ink: #fca5a5;
  --nova-status-archived:      #64748b;  --nova-status-archived-ink:  #94a3b8;

  /* GAP-013 (dark): over-capacity treatment. Fill + text lift to a hot pale red
     so they read on the dark lane; the hatch goes deep red so the diagonal stripe
     still registers over the light-red overflow tip. */
  --nova-over-capacity:        #ff8a80;
  --nova-over-capacity-hatch:  #b42318;
  --nova-over-capacity-text:   #ff8a80;

  /* v2 - container tints (dark). Higher alpha so they register against
     dark panels but never fight the colored text. */
  --nova-container-success:  rgba(74, 222, 128, 0.14);
  --nova-container-warning:  rgba(251, 191, 36, 0.14);
  --nova-container-severe:   rgba(251, 146, 60, 0.14);
  --nova-container-critical: rgba(242, 139, 130, 0.14);
  --nova-container-info:     rgba(138, 180, 248, 0.14);

  /* v2 - tertiary accents (dark - lightened for contrast on dark panel) */
  --nova-accent-teal:   #2DD4BF;
  --nova-accent-violet: #A78BFA;

  /* v2 - chart-series (dark). Hue-shifted lighter to clear dark surface. */
  --nova-chart-1: #8ab4f8;
  --nova-chart-2: #2DD4BF;
  --nova-chart-3: #d946ef;  /* GAP-182 - was #A78BFA, 0.069 from chart-1 under deuteranopia. */
  --nova-chart-4: #fbbf24;
  --nova-chart-5: #f472b6;  /* GAP-182 - separated from the amber chart-4. */
  --nova-chart-6: #cbd5e1;  /* GAP-182 - was #94A3B8, which sat 0.092 from chart-1 with NORMAL vision: a plain palette defect, not only an accessibility one. */

  --nova-signal-surface: #FFFFFF;
  --nova-signal-ink:     var(--nova-ink-900);

  /* §SUB-GAP-001 (dark) */
  --nova-signal-text-muted:  rgba(17, 17, 17, 0.72);
  --nova-signal-text-subtle: rgba(17, 17, 17, 0.58);  /* GAP-182 - 0.56 measured 4.31:1 on --nova-signal-surface, just under AA. 0.58 gives 4.61:1. */

  /* v2.11 · SUB-GAP-021/022 (dark) - goes DEEPER than default canvas, not
     lighter. Focal surface must still contrast against dark chrome. */
  --nova-signal-canvas:     #050608;
  --nova-signal-canvas-alt: #101217;
  /* --nova-signal-hairline stays mode-invariant. */

  /* v2.11 · SUB-GAP-026 (dark) - lighter than --nova-severity-critical
     (#f28b82) to preserve the "deepest" semantic. In dark mode the screen
     is already dark; lightness = intensity. */
  --nova-severity-critical-deep: #fca5a5;

  /* §SUB-GAP-005 (dark) */
  --nova-state-pressed:        rgba(255, 255, 255, 0.06);
  /* GAP-185 - the accent wash needs more of the accent on a dark panel to read at all: 10% of a
     mid-blue over #19191d is very close to invisible. Same derivation, heavier mix. */
  --nova-primary-soft: color-mix(in oklab, var(--nova-primary) 22%, transparent);
  --nova-canvas-inset-shadow:  inset 0 1px 2px rgba(0, 0, 0, 0.25);

  /* v2.13b · SUB-GAP-033 / roadmap B2 - dark shadow steps up to 0.4 alpha so
     the drop reads through the darkened canvas. Migrated from app.css. */
  --nova-shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);

  /* v2.7 · SUB-GAP-015 (dark) - scrim steps up to ~56% alpha so the
     darkened dark-mode scrim still registers visually against
     --nova-canvas (neutral-900). --nova-scrim-strong is identical in
     both themes - "strong" is a fixed 60% alpha regardless of mode. */
  --nova-scrim:  rgba(0, 0, 0, 0.56);

  /* ───────── v2.12 (dark) ───────── */

  /* SUB-GAP-039 (dark) - tone-keyed value text. Lighter so the focal
     number stays readable on dark canvas. Mirrors severity-*-text-dark
     intentions. */
  --nova-value-tone-good:     #4ade80;
  --nova-value-tone-warning:  #fbbf24;
  --nova-value-tone-severe:   #fb923c;
  --nova-value-tone-critical: #f28b82;
  --nova-value-tone-info:     var(--nova-blue-300);

  /* SUB-GAP-035 (dark) - chart axes / grids / tooltip. Inverted via
     white-alpha. Tooltip uses dark-glass surface. */
  --nova-chart-axis:           rgba(255, 255, 255, 0.20);
  --nova-chart-grid:           rgba(255, 255, 255, 0.06);
  --nova-chart-grid-strong:    rgba(255, 255, 255, 0.10);
  --nova-chart-tooltip-bg:     rgba(19, 19, 22, 0.92);
  --nova-chart-tooltip-border: rgba(255, 255, 255, 0.10);
  --nova-chart-empty-stroke:   rgba(255, 255, 255, 0.12);

  /* SUB-GAP-027/030 (dark) - glass-family chrome */
  --nova-glass-surface:        rgba(19, 19, 22, 0.82);
  --nova-glass-surface-strong: rgba(19, 19, 22, 0.92);
  --nova-glass-border:         rgba(255, 255, 255, 0.08);
  /* --nova-glass-blur stays mode-invariant. */

  /* SUB-GAP-037 (dark) - segmented control. GAP-199.
     The thumb and its ink ARE restated here, and the reason is a CSS rule that is easy to get
     wrong: a var() inside a custom property is substituted where the property is DECLARED, not
     where it is used. `--nova-segmented-thumb: var(--nova-primary)` at :root therefore freezes to
     the light-lane blue and inherits that frozen value into this subtree - redefining
     --nova-primary here does nothing for it. Measured headless before this line existed: a dark
     pane rendered #146EF4 with white ink instead of #8ab4f8 with near-black.
     --nova-segmented-thumb-ring stays light-only because `transparent` is a literal with nothing
     to resolve. */
  --nova-segmented-track:      rgba(255, 255, 255, 0.04);
  --nova-segmented-thumb:      var(--nova-primary);
  --nova-segmented-thumb-ink:  var(--nova-on-accent);
  --nova-segmented-hover:      var(--nova-neutral-700);

  /* SUB-GAP-040 (dark) - ring track quieter on dark canvas */
  --nova-ring-track:           rgba(255, 255, 255, 0.08);

  /* --nova-treemap-ink-flip-at, --nova-chart-stroke, --nova-chart-dash
     stay mode-invariant. */

  /* ───────── v2.14 (dark) ───────── */

  /* GAP-006 (dark): grid selection re-points to the pale-blue dark primary
     and steps the alpha up so the wash registers on the dark canvas.
     --nova-radius-xs and the --nova-lightbox-* family are mode-invariant,
     no dark override. --nova-grid-header-bg follows --nova-canvas-rail. */
  --nova-grid-selection:        rgba(138, 180, 248, 0.18);
  --nova-grid-selection-border: rgba(138, 180, 248, 0.45);
  /* GAP-009 (dark): resting voyage-bar wash re-points to the pale-blue dark
     primary and steps the alpha up so it registers on the dark lane. */
  --nova-horizon-bar:           rgba(138, 180, 248, 0.22);
}

/* Base */
body {
  font-family: var(--nova-font-sans);
  font-size: var(--nova-fs-14);
  line-height: 1.6;
  color: var(--nova-text-primary);
  background: var(--nova-canvas);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Utility - signature Nova labels. Use as a class on spans/divs. */
.nova-label {
  font-size: var(--nova-fs-10);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--nova-text-muted);
}

.nova-mono,
code {
  font-family: var(--nova-font-mono);
  font-variant-numeric: tabular-nums;
}

.tabular-nums { font-variant-numeric: tabular-nums; }

/* §SUB-GAP-005 - panel typography utilities, promoted from colors_and_type.css. */
.nova-panel-title {
  font-size: var(--nova-fs-16);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--nova-text-primary);
  margin: 0;
}
.nova-panel-subtitle {
  font-size: var(--nova-fs-13);
  font-weight: 400;
  color: var(--nova-text-secondary);
  margin: 2px 0 0;
}
.nova-panel-meta {
  font-family: var(--nova-font-mono);
  font-size: var(--nova-fs-11);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--nova-text-muted);
  font-variant-numeric: tabular-nums;
}

/* ============================================================================ */
/* GAP-065 - FORMS.md layout utilities. The one form layout primitive + the      */
/* action row + section rhythm. Layout only (no colour); tokens only; additive.  */
/* See specs/FORMS.md.                                                           */
/* ============================================================================ */

/* .nova-form-grid - responsive field grid. Mobile-first: 1-up → 2-up (--nova-bp-md)
   → 3-up (--nova-bp-lg). data-span widens a cell; put it on a <div> grid child. */
.nova-form-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--nova-space-md);
    align-items: start;
}
@media (min-width: 768px) {   /* --nova-bp-md → 2-up */
    .nova-form-grid { grid-template-columns: repeat(2, 1fr); }
    .nova-form-grid > [data-span="2"],
    .nova-form-grid > [data-span="3"] { grid-column: span 2; }
}
@media (min-width: 1024px) {  /* --nova-bp-lg → 3-up */
    .nova-form-grid { grid-template-columns: repeat(3, 1fr); }
    .nova-form-grid > [data-span="2"] { grid-column: span 2; }
    .nova-form-grid > [data-span="3"] { grid-column: span 3; }
}

/* .nova-form-actions - a form's button row. Wrapping, vertically centred,
   --nova-space-sm gap. Primary-first, left-aligned by default; --end right-aligns. */
.nova-form-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--nova-space-sm);
}
.nova-form-actions--end { justify-content: flex-end; }

/* .nova-form - section rhythm. Stacks a form's blocks (field grid, section title,
   error row, action row) with --nova-space-lg. One wrapper, not hand-rolled divs. */
.nova-form {
    display: flex;
    flex-direction: column;
    gap: var(--nova-space-lg);
}

/* ============================================================================ */
/* Consolidated from Nova app.css during DS migration (2026-06-11).             */
/* ============================================================================ */
:root {
    --nova-radius-lg: 8px;
    --nova-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);

    /* GAP-185 - renamed from --nova-chrome-surface-hover. It is the hover sibling of
       --nova-state-pressed and belongs in that family; under the old name the two halves of one
       interaction-state pair sat 700 lines and one vocabulary apart. Four call sites moved. */
    --nova-state-hover: rgba(0, 0, 0, 0.03);
}

/* ============================================================================
   GAP-185 - DEPRECATED. A second vocabulary for concepts the DS already names.

   This is the residue of the June 2026 app.css migration, and it had quietly become a shadow
   copy of the system: --nova-error (#ba1a1a) beside --nova-severity-critical (#DC2626);
   --nova-surface beside --nova-canvas-panel; --nova-border beside --nova-canvas-border;
   --nova-text beside --nova-text-primary; --nova-weight-* beside --nova-fw-*; --nova-track-*
   beside --nova-tracking-*; and --nova-spacing-* a byte-identical duplicate of --nova-space-*
   that survived the GAP-184 vocabulary freeze only because it is spelled differently and lives
   in a different block of this file. Two names for one idea, with values that sometimes differ
   just enough to see, is the reason a new surface can be built entirely from real tokens and
   still not match the surface beside it.

   Every one of these now has ZERO references in the 142 component stylesheets: 54 declarations
   were repointed onto the canonical names, and the other 34 names were already used by nothing
   at all. They are kept, not deleted, for one reason: they ship in a published package and Nova
   and Atlas may reference them from their own app CSS, which is not visible from this repo.
   Deleting them here would break a consumer silently.

   So: quarantined instead. Gate 15 fails the build if a component reaches for one again, and
   gen-design-md.mjs never puts them in the Stitch mirror, so no new design is drawn against
   them. Removal is a separate, announced breaking change, once Nova and Atlas are swept.
   ============================================================================ */
[data-theme="dark"],
.mud-theme-dark {
    --nova-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
    --nova-state-hover: rgba(255, 255, 255, 0.04);   /* GAP-185 - see the light lane */
    --nova-accent: var(--nova-blue-300);
}

/* GAP-185-DEPRECATED-BEGIN */
:root {
    --nova-border: #c2c6d7;
    --nova-border-light: rgba(0, 0, 0, 0.06);
    --nova-canvas-surface: var(--nova-neutral-0);
    --nova-chrome-accent: var(--nova-blue-500);
    --nova-chrome-elevation: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
    --nova-chrome-surface-hover: var(--nova-state-hover);
    --nova-chrome-text: var(--nova-ink-900);
    --nova-chrome-text-muted: var(--nova-neutral-500);
    --nova-error: #ba1a1a;
    --nova-error-container: #ffdad6;
    --nova-severity-critical-bg: rgba(239, 68, 68, 0.08);
    --nova-severity-info-bg: rgba(20, 110, 244, 0.06);
    --nova-severity-success-bg: rgba(34, 197, 94, 0.08);
    --nova-severity-warning-bg: rgba(245, 158, 11, 0.08);
    --nova-shadow-ambient: 0 4px 20px rgba(25, 28, 30, 0.06);
    --nova-shadow-primary: none;
    --nova-signal-cta-bg: #FFFFFF;
    --nova-signal-cta-ink: var(--nova-ink-900);
    --nova-spacing-2xl: var(--nova-space-48);
    --nova-spacing-lg: var(--nova-space-lg);
    --nova-spacing-md: var(--nova-space-md);
    --nova-spacing-sm: var(--nova-space-sm);
    --nova-spacing-xl: var(--nova-space-xl);
    --nova-spacing-xs: var(--nova-space-xs);
    --nova-surface: #f7f9fc;
    --nova-surface-card: #ffffff;
    --nova-text: var(--nova-text-primary);
    --nova-text-badge: var(--nova-fs-10);
    --nova-text-kpi-value: var(--nova-fs-36);
    --nova-text-kpi-value-lg: var(--nova-fs-48);
    --nova-text-label: var(--nova-fs-10);
    --nova-text-page-title: var(--nova-fs-30);
    --nova-text-section: var(--nova-fs-16);
    --nova-track-normal: 0;
    --nova-track-tight: -0.02em;
    --nova-track-wide: 0.02em;
    --nova-track-wider: 0.08em;
    --nova-track-widest: 0.15em;
    --nova-transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
    --nova-warning: #a23b00;
    --nova-weight-black: 700;
    --nova-weight-bold: 700;
    --nova-weight-medium: 500;
    --nova-weight-regular: 400;
    --nova-weight-semi: 600;
}

/* GAP-185 - the dark lane of the deprecated vocabulary above. Same terms: no component
   references any of these, they are kept only so a consumer's own CSS does not break, and
   gate 15 stops a component reaching for one again. */
[data-theme="dark"],
.mud-theme-dark {
    --nova-border: #48474b;
    --nova-border-light: rgba(255, 255, 255, 0.05);
    --nova-canvas-surface: var(--nova-neutral-900);
    --nova-chrome-accent: var(--nova-blue-300);
    --nova-chrome-elevation: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.25);
    --nova-chrome-surface-hover: var(--nova-state-hover);
    --nova-chrome-text: var(--nova-text-primary);
    --nova-chrome-text-muted: var(--nova-text-muted);
    --nova-error: #f28b82;
    --nova-error-container: rgba(242, 139, 130, 0.15);
    --nova-severity-critical-bg: rgba(255, 113, 108, 0.1);
    --nova-severity-info-bg: rgba(125, 175, 255, 0.1);
    --nova-severity-success-bg: rgba(74, 222, 128, 0.1);
    --nova-severity-warning-bg: rgba(251, 191, 36, 0.1);
    --nova-signal-cta-bg: var(--nova-ink-900);
    --nova-signal-cta-ink: #FFFFFF;
    --nova-surface: #131316;
    --nova-surface-card: #19191d;
    --nova-text: var(--nova-text-primary);
    --nova-warning: #fbbf24;
}
/* GAP-185-DEPRECATED-END */

/* =========================================================================
   GAP-149 - .nova-touch-target: lift a small affordance to the touch floor
   WITHOUT changing how it looks or how its row lays out.

   In-field affordances (a clear glyph, a compact text action) are 16-18px by
   design: they sit inside a 28-44px control and a larger visual box would eat
   the field. Growing the element itself would change the layout on every
   surface that composes it. So the hit area is expanded by a transparent
   pseudo-element instead - the standard technique - and only on a coarse
   pointer, so fine-pointer rendering is byte-identical.

   Ships GLOBAL, and is ONE declaration, on purpose: this defect was found in
   five places at once (NovaDatePicker, NovaDateTimePicker, NovaDateRangePicker,
   NovaAutocomplete, NovaFilterBar) because each had authored its own clear
   affordance. A per-component copy is how they drifted below the floor
   separately; a shared class is how they cannot drift again.

   Contract for the host element: it must be a positioning context. An
   absolutely-positioned affordance already is; anything else needs
   `position: relative` in its own stylesheet. The utility deliberately does
   NOT set position, because forcing `relative` would break the three pickers
   whose clear glyph is `position: absolute`.
   ========================================================================= */
@media (pointer: coarse) {
    .nova-touch-target::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: var(--nova-touch-min, 44px);
        height: var(--nova-touch-min, 44px);
        transform: translate(-50%, -50%);
    }
}

/* =========================================================================
   GAP-133 - NovaScheduleMatrix blank-cell reason tip.

   Ships GLOBAL, not with the component: the tip node is created by
   nova-schedule-matrix.js on document.body (a foreign child of a Blazor-managed
   element would be liable to be clobbered by the next diff), and Blazor's scoped
   CSS cannot reach a node outside the component's own DOM.

   No interactive content, ever. The moment a tip holds a link or a button this
   stops being an explanation.
   ========================================================================= */
.nova-sm__reason-tip {
    position: fixed;
    z-index: var(--nova-z-popover);
    max-width: 240px;
    padding: 8px 10px;
    background: var(--nova-canvas-panel);
    border: 1px solid var(--nova-canvas-border);
    border-radius: var(--nova-radius-md, 6px);
    box-shadow: var(--nova-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.18));
    color: var(--nova-text-secondary);
    font-family: var(--nova-font-sans);
    font-size: var(--nova-fs-12);
    line-height: 1.45;
    /* A reason is a consumer SENTENCE, not a chip: it must be allowed to wrap. */
    white-space: normal;
    text-align: left;
    pointer-events: none;
}

/* The eyebrow announces the tip's kind before the sentence, so it cannot be mistaken for a menu. */
.nova-sm__reason-tip b {
    display: block;
    margin-bottom: 2px;
    color: var(--nova-text-muted);
    font-family: var(--nova-font-mono);
    font-size: var(--nova-fs-10);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* =========================================================================
   Reduced motion - the system-wide honour of the OS setting.
   =========================================================================
   Every component that animates used to have to remember its own
   @media (prefers-reduced-motion) block. Twenty-one did; sixty-four did not,
   so most of the DS kept moving for a user who asked it to stop. This is the
   one place that decision is made, exactly like the token ladder above.

   !important is required, not sloppy: every component rule ships as scoped
   CSS with a [b-xxxxxxxxxx] attribute, which outranks a bare universal
   selector. Duration is collapsed rather than set to `none` so that
   animation-fill-mode: forwards still lands on the final frame, and any
   transitionend / animationend handler still fires.

   Loading indicators are exempt. WCAG 2.3.3 is about non-essential motion:
   an indeterminate progress bar and a skeleton are the only signal that work
   is in flight, and freezing them reads as a hung screen. They opt out with
   data-nova-motion="essential" - the only value this attribute takes.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *:not([data-nova-motion="essential"]),
  *:not([data-nova-motion="essential"])::before,
  *:not([data-nova-motion="essential"])::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }

  html { scroll-behavior: auto !important; }
}

/* =============================================================================================
   GAP-202 - THE BUTTON VOCABULARY IS GLOBAL, because a native <button> has to reach it.

   This block was `Components/NovaButton.razor.css` until 2.15.0. It is moved, not copied: the
   scoped file is deleted and this is the only definition. NovaButton emits exactly the same class
   string it always did and renders from these rules now.

   WHY. A consumer cannot always use NovaButton. Signing out and signing in are real HTTP POSTs
   (only a real request can write or clear the auth cookie), so those pages carry a plain
   <form> and a native <button type="submit">, with no Blazor circuit to host a component. Atlas
   wrote `class="nova-btn nova-btn--primary nova-btn--block"` on exactly such a button and the
   classes did nothing at all: Blazor rewrites a scoped selector to `.nova-btn[b-yz99y8anxf]` and
   the attribute only lands on markup the COMPONENT emits. Three Atlas buttons were rendering as
   raw browser buttons for that reason - sign-out, and both circuit-reconnect buttons. A class
   vocabulary that consumer markup is expected to use has to live where consumer markup can see it,
   and `logisoft.tokens.css` is the only sheet the package ships globally.

   THE SPECIFICITY CHANGE, deliberately accepted. Scoped, every selector here carried the
   isolation attribute and sat at (0,2,0). Unscoped it sits at (0,1,0), which is the correct weight
   for a design-system class; the extra rung was an artefact of the build, not a decision. Swept
   before the move: nothing in the DS, in MudBlazor 9.1.0, in Nova's app.css or embed.css, or in
   Atlas outranks a bare class on a button. The one interaction that exists is recorded below.

   KNOWN INTERACTION. Three slot rules select `::deep a` at (0,1,1) and so now outrank `.nova-btn`
   for a NovaButton rendered as an <a> (Href set) INSIDE one of those slots:
   `.nova-auth-shell__footer`, `.nova-journal-layout__back-nav`, `.nova-tri-layout__back-nav`. No
   consumer does this today. If one starts, the slot rule is what to change - not this block.
   ============================================================================================= */
.nova-btn {
    /* GAP-202: the tone pair DEFAULTS here, and this is the one change the move actually required.
       `.nova-btn--primary` paints `background: var(--_accent)`, and --_accent was only ever set by a
       `.nova-btn--tone-*` class. NovaButton always emits one, so nothing in the system had ever
       rendered without it - but hand-written consumer markup does not, and Atlas's
       `class="nova-btn nova-btn--primary nova-btn--block"` is exactly that. With both custom
       properties undefined, `background` and `color` are invalid at computed-value time and the
       submit button renders as bare black text on the card. Caught in a browser on the Atlas sign-in
       page; no test in either repo would have seen it, and the markup looks completely correct.
       The tone classes below sit later in source at equal specificity, so they still win. */
    --_accent: var(--nova-primary);
    --_on: var(--nova-on-accent);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--nova-space-6);
    border: 0;
    padding: 0 var(--nova-space-18);
    height: var(--nova-control-h-md, 36px);
    font-family: var(--nova-font-sans);
    /* Sentence case, not caps. The system stacks a deliberate 10px/700/0.15em micro-label tier
       (card eyebrows, table headers) and that tier is the ops signature worth keeping; running the
       SAME shout through 12px buttons and 13px card titles as well left nothing quiet to read
       against, which is what made the whole surface read as a template. Carbon, Fluent, Atlassian
       and Polaris all set button labels in sentence case at 13-14px semibold; this matches.
       Consumer note: labels are no longer upper-cased for you, so a call site that passed "save"
       now renders "save". Atlas and Nova need one read-through of their button labels. */
    font-size: var(--nova-fs-13);
    font-weight: 600;
    letter-spacing: 0;
    border-radius: var(--nova-radius-sm, 4px);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--nova-transition, 120ms ease),
                border-color var(--nova-transition, 120ms ease),
                color var(--nova-transition, 120ms ease),
                opacity var(--nova-transition, 120ms ease);
    /* Trust rule: no gradients, no colored shadows */
    box-shadow: none;
    box-sizing: border-box;
}

.nova-btn__label { display: inline-block; }

.nova-btn:disabled,
.nova-btn[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
}

.nova-btn:focus-visible {
    outline: var(--nova-focus-width) solid var(--nova-focus);
    outline-offset: var(--nova-focus-offset);
}

.nova-btn--block { width: 100%; }

.nova-btn--icon-only {
    width: var(--nova-control-h-md, 36px);
    padding: 0;
}

.nova-btn--loading { pointer-events: none; }

/* ---------- Kind × Tone tokenization ---------- */
/* Each kind sets --_bg, --_bd, --_fg based on tone; :hover layers add
   state-pressed overlays. Tone tokens fall back to --nova-primary if the
   severity variant isn't defined (defensive for --warning-text ink tier). */

.nova-btn--tone-primary  { --_accent: var(--nova-primary);             --_on: var(--nova-on-accent); }
.nova-btn--tone-critical { --_accent: var(--nova-severity-critical-fill);   --_on: var(--nova-on-severity-critical); --_text: var(--nova-severity-critical-text); }
.nova-btn--tone-severe   { --_accent: var(--nova-severity-severe-fill);     --_on: var(--nova-on-severity-severe);   --_text: var(--nova-severity-severe-text); }
.nova-btn--tone-warning  { --_accent: var(--nova-severity-warning-fill);    --_on: var(--nova-on-severity-warning);  --_text: var(--nova-severity-warning-text); }
.nova-btn--tone-success  { --_accent: var(--nova-severity-success-fill);    --_on: var(--nova-on-severity-success);  --_text: var(--nova-severity-success-text); }
/* GAP-182: the four severity tones take --_accent from the FILL tier, not the mark. --_accent is
   used in exactly one place, the filled `.nova-btn--primary` background, and the mark values were
   never chosen to carry white ink: --tone-severe shipped at 3.56:1 and --tone-success at 3.30:1,
   both under AA, on production buttons. --_text still comes from the ink tier and the marks are
   untouched everywhere else. */

/* GAP-153: the shared Neutral tier. Deliberately carries NO --_accent, because the whole point
   is a tertiary action that recedes: a Text/Ghost button in the tenant accent sitting under a
   filled button in that SAME accent reads as a second call to action, and on a red-branded
   tenant it reads as a destructive one. Filled Neutral falls back to the panel surface so the
   kind still renders sanely if anyone asks for it. */
.nova-btn--tone-neutral  { --_accent: var(--nova-canvas-panel-alt);    --_on: var(--nova-text-primary);         --_text: var(--nova-text-muted); }

/* Primary kind - filled accent */
.nova-btn--primary {
    background: var(--_accent);
    color: var(--_on);
}
.nova-btn--primary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--nova-primary-hover);
}
.nova-btn--primary.nova-btn--tone-critical:hover:not(:disabled) { filter: brightness(0.92); }
.nova-btn--primary.nova-btn--tone-severe:hover:not(:disabled)   { filter: brightness(0.92); }
.nova-btn--primary.nova-btn--tone-warning:hover:not(:disabled)  { filter: brightness(0.92); }
.nova-btn--primary.nova-btn--tone-success:hover:not(:disabled)  { filter: brightness(0.92); }

/* Secondary kind - outlined on panel */
.nova-btn--secondary {
    background: var(--nova-canvas-panel);
    border: 1px solid var(--nova-canvas-border);
    color: var(--nova-text-primary);
}
.nova-btn--secondary.nova-btn--tone-critical { color: var(--_text); border-color: var(--_text); }
.nova-btn--secondary.nova-btn--tone-severe   { color: var(--_text); border-color: var(--_text); }
.nova-btn--secondary.nova-btn--tone-warning  { color: var(--_text); border-color: var(--_text); }
.nova-btn--secondary.nova-btn--tone-success  { color: var(--_text); border-color: var(--_text); }
.nova-btn--secondary.nova-btn--tone-primary  { color: var(--nova-primary); border-color: var(--nova-primary); }
.nova-btn--secondary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--nova-canvas-panel-alt);
}

/* Ghost kind - transparent, hover fill */
.nova-btn--ghost {
    background: transparent;
    color: var(--_text, var(--nova-primary));
}
.nova-btn--ghost.nova-btn--tone-primary { color: var(--nova-primary); }
.nova-btn--ghost.nova-btn--tone-neutral { color: var(--nova-text-muted); }
.nova-btn--ghost:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--nova-canvas-panel-alt);
}

/* Text kind - inline link-style, no padding-box hover */
.nova-btn--text {
    background: transparent;
    color: var(--_text, var(--nova-primary));
    padding: 0 var(--nova-space-4);
    height: auto;
    text-transform: none;
    letter-spacing: normal;
    font-weight: 600;
}
.nova-btn--text:hover:not(:disabled):not([aria-disabled="true"]) {
    text-decoration: underline;
    background: transparent;
}
.nova-btn--text:focus-visible { outline-offset: var(--nova-focus-offset-tight); }

/* ── Phone (base): touch floor on default/large; md+ restores compact heights ── */
.nova-btn:not(.nova-btn--small):not(.nova-btn--text):not(.nova-btn--icon-only) {
    min-height: var(--nova-touch-min);
}

.nova-btn--icon-only:not(.nova-btn--small) {
    min-height: var(--nova-touch-min);
    width: var(--nova-touch-min);
}

@media (min-width: 768px) {
    .nova-btn:not(.nova-btn--small):not(.nova-btn--text):not(.nova-btn--icon-only) {
        min-height: unset;
    }

    .nova-btn--icon-only:not(.nova-btn--small) {
        min-height: unset;
        width: var(--nova-control-h-md, 36px);
    }
}

/* =============================================================================================
   GAP-202 - `.nova-form-native`: the native lane of the input vocabulary.

   The other half of the problem the button block above solves. A page that must POST without a
   circuit cannot use NovaTextInput or NovaSelect either, so its <input>, <select> and <textarea>
   are native elements and every one of them was the consumer's problem. Atlas wrote its own field
   CSS, and it drifted the way un-owned CSS drifts: until 2026-09-06 the sign-in form named six
   custom properties this file does not define (--nova-accent-soft, --nova-accent-strong,
   --nova-accent-contrast, --nova-tone-critical, --nova-font-size-sm, --nova-font-size-md) while
   its own header claimed "DS TOKENS only", so it rendered entirely off hardcoded hex fallbacks and
   did not track the theme. Nothing about reading the file said so.

   OPT-IN, ONE CLASS ON THE <form>, and NOT free inside the auth card. GAP-189 put the form's
   RHYTHM on the container (`.nova-auth-shell__card ::deep form` is a flex column) and the obvious
   next step was to put the CONTROLS there too. It is the wrong step, for two reasons found by
   looking:

     1. It restyles markup nobody asked about. Nova.Portal's contact form carries an offscreen
        honeypot: a class-less <label> + <input type="text"> that exists to be invisible and to
        catch bots. A rule keyed on "any input under an auth card" reaches it, and reaches every
        future class-less control in any consumer's auth form. `.nova-form-native` reaches exactly
        what a consumer pointed it at.
     2. It would hard-code `__card`, a NovaAuthShell BEM internal, into the global sheet, where
        renaming the component would silently unstyle every auth form in the system.

   `nova-input` IS NOT THE CLASS, and cannot be. Atlas's markup carried `class="nova-input"` on its
   native inputs, and that looks like the contract to honour until you check what the name already
   means: `.nova-input` is the WRAPPER div on NovaTextInput (display:flex; flex-direction:column)
   and NovaAutocomplete reuses it through ::deep. The native field inside it is
   `.nova-input__native`. A global `.nova-input` carrying a height and a border would put a 36px
   bordered box around every text field and every autocomplete in Nova and Atlas both. The contract
   is therefore explicitly REPLACED rather than honoured, and Atlas drops the class.

   TYPE EXCLUSIONS, not a type allowlist. The list below names every non-text-like input type in
   HTML: it is closed, and it is the smaller list. An allowlist would silently miss `date`,
   `month`, `week` and `datetime-local`, which all want the box. Without the exclusions a checkbox
   or a radio in a native form becomes a 36px full-width bordered box, and GAP-189's own note
   records a checkbox sitting as the last child of one auth form.

   The `:not([class*="nova-"])` guard is a second line, not the first: it keeps a DS component's
   own native element out of this block if one is ever dropped inside a `.nova-form-native`. Both
   guards ride `:where()` so they add no specificity and a consumer can still override one field
   without an arms race.
   ============================================================================================= */

/* One field = one label + its control. Mirrors NovaField, which is what a form WITH a circuit
   would be using; the native lane should not read as a second design. */
.nova-form-native__field {
    display: flex;
    flex-direction: column;
    gap: var(--nova-space-stack);
    font-family: var(--nova-font-sans);
}

.nova-form-native__label {
    display: inline-flex;
    align-items: center;
    gap: var(--nova-space-6);
    font-size: var(--nova-fs-11);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--nova-text-muted);
}

.nova-form-native__required { color: var(--nova-severity-critical-text); font-weight: 700; }

/* margin:0 on both, which NovaField's equivalents do not need and this lane does. NovaField renders
   its hint and error as spans; a native form reaches for a <p>, and a <p> arrives with a UA margin
   that the container's own gap has no say over. GAP-189 put every vertical step in an auth form on
   the container precisely so a page cannot set one by accident, and a browser default is exactly
   that accident. */
.nova-form-native__hint {
    margin: 0;
    font-size: var(--nova-text-caption);
    color: var(--nova-text-muted);
}

.nova-form-native__error {
    margin: 0;
    display: flex;
    align-items: center;
    gap: var(--nova-space-4);
    font-size: var(--nova-text-caption);
    font-weight: 600;
    color: var(--nova-severity-critical-text);
}

/* The control itself. Geometry and chrome are NovaTextInput's `.nova-input__native`, to the
   declaration: same height rung, same canvas, same hairline, same radius, same GAP-201 focus
   geometry. If one of them changes, both change. */
.nova-form-native :is(input, select, textarea):where(
    :not([type="button"]):not([type="checkbox"]):not([type="color"]):not([type="file"]):not([type="hidden"]):not([type="image"]):not([type="radio"]):not([type="range"]):not([type="reset"]):not([type="submit"]):not([class*="nova-"])
) {
    width: 100%;
    box-sizing: border-box;
    height: var(--nova-control-h-md);
    padding: 0 var(--nova-space-12);
    background: var(--nova-canvas-input, var(--nova-canvas-panel));
    border: 1px solid var(--nova-canvas-border);
    border-radius: var(--nova-radius-sm);
    color: var(--nova-text-primary);
    font-family: var(--nova-font-sans);
    font-weight: 500;
    box-shadow: none;
    transition: border-color var(--nova-transition, 120ms ease),
                background var(--nova-transition, 120ms ease);
    /* Phone base: the touch floor, and 16px so iOS does not zoom the page on focus. md+ restores
       the compact rung below, exactly as NovaTextInput does. */
    min-height: var(--nova-touch-min);
    font-size: var(--nova-fs-16);
}

.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"]))::placeholder {
    color: var(--nova-text-tertiary);
    opacity: 1;
}

.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])):hover:not(:disabled):not([readonly]) {
    border-color: var(--nova-text-tertiary);
}

.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])):focus-visible {
    outline: var(--nova-focus-width) solid var(--nova-focus);
    outline-offset: var(--nova-focus-offset-tight);
    border-color: var(--nova-primary);
}

/* :user-invalid, never :invalid. A `required` field is invalid from the moment the page paints, so
   `:invalid` would open the Atlas sign-in form with both boxes already outlined in red, before the
   reader has typed a character. NovaTextInput has the same rule expressed as a class the consumer
   sets (`.nova-input--invalid`); :user-invalid is the native equivalent, and it waits for the
   field to have been edited or the form to have been submitted. */
.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])):user-invalid {
    border-color: var(--nova-severity-critical);
}

.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])):user-invalid:focus-visible {
    outline-color: var(--nova-severity-critical);
    border-color: var(--nova-severity-critical);
}

.nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])):disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: var(--nova-canvas-panel-alt);
}

.nova-form-native :is(input, textarea):where(:not([class*="nova-"]))[readonly] {
    background: var(--nova-canvas-panel-alt);
    cursor: default;
}

/* A textarea is not a 36px box. The same override NovaTextInput carries for its multiline mode:
   the fixed control height and the touch floor both have to come off, or `rows` is ignored and
   every textarea collapses to one line. */
.nova-form-native textarea:where(:not([class*="nova-"])) {
    height: auto;
    min-height: unset;
    padding: var(--nova-space-10) var(--nova-space-12);
    line-height: 1.5;
    resize: vertical;
}

@media (min-width: 768px) {
    .nova-form-native :is(input, select, textarea):where(:not([class*="nova-"])) {
        min-height: unset;
        font-size: var(--nova-fs-14);
    }

    .nova-form-native textarea:where(:not([class*="nova-"])) {
        min-height: unset;
    }

    .nova-form-native__field { gap: var(--nova-space-6); }

    .nova-form-native__hint,
    .nova-form-native__error { font-size: var(--nova-fs-11); }
}

/* =============================================================================================
   GAP-183 - Windows High Contrast (forced-colors).

   In forced-colors mode Windows replaces author background-color, color, border-color and
   box-shadow with a small set of system colours. Our severity grammar, which the no-tinted-row-bg
   invariant fixes as "3px left border + coloured text + dot", loses all three carriers at once:
   the border, the text and the dot all resolve to CanvasText. An operator in High Contrast could
   not tell a critical row from a healthy one, and our users are Windows users.

   The call, made deliberately: where colour IS the datum, preserve it. A severity mark or a
   stacked bar that collapses into one system colour conveys nothing at all, and "nothing" is
   strictly worse than "the author's colours" for the person trying to read the board. Where the
   colour is chrome rather than data - surfaces, text, borders, focus - the system colours win, as
   they should, and this file says nothing about them.

   Two things already work and are deliberately left alone: focus rings, because GAP-180 made
   every one a real `outline`, which forced-colors recolours rather than removes; and disabled
   states, because `opacity` is not overridden.

   This lives here, in the package's global sheet, rather than in 30 component stylesheets. Same
   argument as the shared .nova-touch-target utility in GAP-149: one place, one rule, greppable.
   validate.mjs fails any component that declares forced-color-adjust locally.
   ============================================================================================= */
@media (forced-colors: active) {

  /* 1. Data visualisation. A chart is a picture, and its colours are its content. */
  .nova-bar-chart, .nova-trend-chart, .nova-treemap, .nova-heatmap, .nova-geo-map,
  .nova-waterfall, .nova-pyramid, .nova-stacked-share-bar, .nova-radial-progress,
  .nova-ring, .nova-pulse-grid, .nova-horizon, .nova-leg-profile, .nova-dist-card,
  .nova-comp-card, [class^="nova-pipeline-funnel"] {
    forced-color-adjust: none;
  }

  /* 2. Severity marks and measurement fills, wherever they appear. These are the BEM suffixes
     the DS uses for "this element's colour is its meaning" - dots, bars, fills, rails, routes,
     map glyphs and the over-capacity hatch. */
  [class*="__dot"], [class*="__bar"], [class*="__fill"], [class*="__rail-fill"],
  [class*="__route"], [class*="__marker-glyph"], [class*="__overflow"], [class*="__cap"],
  [class*="__pulse"], [class*="row-rail"] {
    forced-color-adjust: none;
  }

  /* 3. The severity left border, which is the row grammar itself. */
  [class*="--severity-"], [class*="--critical"], [class*="--severe"], [class*="--warning"],
  [class*="--success"], [class*="--info"], [data-tone], [data-tier], [data-gap-tone] {
    forced-color-adjust: none;
  }

  /* 4. Selection is the one case where the system pair is BETTER than ours: Highlight is what
     the user chose for "this is selected", and it is guaranteed to contrast with HighlightText. */
  .nova-bg__td--sel, .nova-rg__cell--staged, .nova-sm__cell--selected,
  .nova-dg__tr--selected, .nova-tablev2__row--selected {
    background: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }

  /* 5. A surface separated from the page only by its own background (the surface-steps-below-panel
     invariant) has nothing left once backgrounds collapse, and box-shadow is dropped entirely.
     Give every floating surface a real border so it still reads as a separate thing. */
  .nova-card, .nova-dialog, .nova-modal, .nova-slide-over__panel, .nova-menu, .nova-tooltip,
  .nova-toast, .nova-glass-popup, .nova-filter-popup, .nova-paged-popup, .nova-hotspot-panel {
    border: 1px solid CanvasText;
  }
}
