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

  /* 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; thumb is canvas-panel
     so the active segment reads as "lifted out" of the track. */
  --nova-segmented-track:      var(--nova-canvas-sunken);
  --nova-segmented-thumb:      var(--nova-canvas-panel);
  --nova-segmented-thumb-ring: var(--nova-canvas-border);

  /* 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 */
  --nova-segmented-track:      rgba(255, 255, 255, 0.04);
  --nova-segmented-thumb:      var(--nova-neutral-700);
  --nova-segmented-thumb-ring: rgba(255, 255, 255, 0.10);

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