/* INOVIU shared theme — THREE LAYERS. Change a value in layer 1 or 2, every
   grid/panel/tree restyles.

   Why three and not one: this file used to be 90 literal hexes and 5 references,
   so "change the scheme" meant editing 90 values by hand and hoping they stayed
   in step. Now:

     LAYER 1  --p-*         the palette. Raw values. The ONLY place a hex lives.
     LAYER 2  --surface-*   what a colour MEANS here (a card, body text, a border).
              --text-*      Light mode, then the same names again for dark mode.
              --border-* …  This is the layer a theme swap edits.
     LAYER 3  --th-* --tg-* the names every component already calls. Each one is
                            now a var() into layer 2. No component changed.

   A component still uses ONLY layer 3 (RULES.md rule 2/5: theme tokens, never a
   raw hex). Layers 1 and 2 are this file's business.

   DARK MODE is layer 2 declared a second time under [data-theme="dark"], so it
   is a ~40-line block rather than a second copy of the file. It is opt-in: set
   data-theme="dark" on <html>. It deliberately does NOT follow the OS setting —
   flipping that on is one @media block, below, when someone asks for it.
*/

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 1 — PALETTE. Raw values. Nothing outside layer 2 may reference these.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Neutral ramp — cool grey, chroma ≤ 4.6 (effectively achromatic; the INOVIU
     template's own palette is already near-neutral — its bar is C* 1.6 and its
     "neutral" C* 0.4 — so this ramp is a truer reading of the brand than the
     pale cyan was). Every chrome surface, border and text tone comes from here.
     Steps are spaced by LIGHTNESS, so they stay distinguishable without hue —
     which is the property that matters here (Alex is colour-blind). */
  --p-n-000: #ffffff;   /* L* 100 — card / grid body */
  --p-n-025: #fafbfc;   /* L*  99 — card title band */
  --p-n-050: #f4f6f7;   /* L*  97 — page field */
  --p-n-100: #eff2f3;   /* L*  95 — row rules */
  --p-n-150: #e7eaec;   /* L*  93 — dividers, sunken rows */
  --p-n-200: #dadee2;   /* L*  88 — the top bar */
  --p-n-300: #c3cace;   /* L*  81 — emphasis fill (primary button, active tab) */
  --p-n-400: #a9b2b8;   /* L*  73 — splitter grab, strong border */
  --p-n-500: #8a9399;   /* L*  61 — faint text */
  --p-n-600: #6e777c;   /* L*  50 — muted text */
  --p-n-700: #525b60;   /* L*  38 — secondary text */
  --p-n-750: #3a4248;   /* L*  27 — body text (the old #374151's weight, on-ramp) */
  --p-n-800: #2e3438;   /* L*  21 — emphasis text (was "navy") */
  --p-n-900: #15181a;   /* L*   8 — body ink */

  /* Dark-mode neutrals — a separate ramp, not the light one inverted: an
     inverted ramp puts pure black behind text and reads as a hole.
     Listed light → dark, the same direction as the light ramp above, and the
     number RISES as L* falls. That ordering is not decoration: the reason six
     roles ended up sharing --p-d-600, and the reason a locked row became
     invisible (THM-2), is that this ramp was written as a scatter of values
     nobody could read as a ladder. L* is quoted so the next person can see at a
     glance where a new step belongs — and whether one is missing. */
  --p-d-100: #e6edf1;   /* L* 93.3 — primary ink */
  --p-d-125: #bccdd6;   /* L* 81.4 — emphasis text (THM-3) */
  --p-d-150: #b4c5cf;   /* L* 78.5 — body text */
  --p-d-175: #9fb0bd;   /* L* 70.9 — secondary / read-only text (THM-3) */
  --p-d-200: #8397a3;   /* L* 61.3 — muted text */
  --p-d-225: #6d7f89;   /* L* 52.1 — disabled text */
  --p-d-250: #63757f;   /* L* 48.1 — faint text */
  --p-d-275: #46545d;   /* L* 34.9 — strong border, splitter grab */
  --p-d-300: #39454d;   /* L* 28.5 — emphasis fill */
  --p-d-350: #303b45;   /* L* 24.3 — a row owned elsewhere (THM-2) */
  --p-d-400: #2a353c;   /* L* 21.4 — borders */
  --p-d-500: #242e35;   /* L* 18.3 — row rules */
  --p-d-600: #1d262c;   /* L* 14.6 — card title band, sunken */
  --p-d-650: #1c252b;   /* L* 14.1 — hover */
  --p-d-700: #181f24;   /* L* 11.3 — card / grid body */
  --p-d-800: #141a1e;   /* L*  8.8 — side panel */
  --p-d-900: #0f1417;   /* L*  6.0 — page field */

  /* STATUS HUES stay hues. The chrome above went achromatic on purpose; status
     did not, because status is information, not decoration. RULES.md rule 3
     still holds and is what makes this safe: the icon + the word carry the
     meaning, the colour only makes a state findable when scanning. */
  --p-amber-050: #fff3cd;
  --p-amber-500: #e6a800;
  --p-amber-600: #d97706;
  --p-amber-900: #664d03;
  --p-red-050:   #fdeaea;
  --p-red-500:   #e63946;
  --p-red-600:   #dc2626;
  --p-green-600: #059669;
  --p-blue-700:  #1f4e79;
  --p-violet-600: #7c3aed;

  /* MICROSOFT SIGN-IN BUTTON — brand constants, NOT ours to retheme.
     Microsoft's identity-branding guidelines fix the four-square mark's colours
     and the button's two permitted treatments. The mark is identical in both
     themes; only the button ground switches (light #FFFFFF / dark #2F2F2F).
     These live in layer 1 so src/web can reference them as tokens — the style
     guard forbids raw hex there, and these are exactly the kind of value that
     must not drift. Check against Microsoft's current asset pack before a
     production release; this is the published spec as of writing. */
  --p-ms-red:           #f25022;
  --p-ms-green:         #7fba00;
  --p-ms-blue:          #00a4ef;
  --p-ms-yellow:        #ffb900;
  --p-ms-ground-light:  #ffffff;
  --p-ms-ground-dark:   #2f2f2f;
  --p-ms-border:        #8c8c8c;
  --p-ms-label-light:   #5e5e5e;
  --p-ms-label-dark:    #ffffff;

  /* Status hues again, lifted for dark grounds (the light ones drop under
     4.5:1 on --p-d-700). */
  --p-amber-400: #f0a13a;
  --p-red-400:   #ff6b6b;
  --p-green-400: #4ed08c;
  --p-blue-400:  #7fb3e0;
  --p-violet-400: #b794f6;

  /* Tinted SURFACES for dark mode — a row that is saying something about itself
     (selected, failed, warned). They are hues, not neutrals, so they belong with
     the status colours rather than on the --p-d-* ladder; and they are dark
     grounds, hence the 950s. --p-amber-200 is the text that sits on the warn one. */
  --p-blue-950:  #1e3138;
  --p-red-950:   #3a1f22;
  --p-amber-950: #3a2f14;
  --p-amber-200: #f2d489;

  /* Alpha veils — a tint OF whatever is under them, which is why they are alpha
     and not a flat colour: the status glyph has to blend into a white row, a
     selected row and a locked row without three values. Black over light,
     white over dark. */
  --p-alpha-black-06: rgba(0, 0, 0, 0.06);
  --p-alpha-black-12: rgba(0, 0, 0, 0.12);
  --p-alpha-white-08: rgba(255, 255, 255, 0.08);
  --p-alpha-white-16: rgba(255, 255, 255, 0.16);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 2 — SEMANTIC, LIGHT. What each colour means. A theme swap edits HERE.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Surfaces */
  --surface-page:      var(--p-n-050);
  --surface-card:      var(--p-n-000);
  --surface-panel:     var(--p-n-025);
  --surface-sunken:    var(--p-n-100);
  --surface-band:      var(--p-n-025);
  --surface-hover:     var(--p-n-050);
  --surface-selected:  var(--p-n-200);
  --surface-chip:      var(--p-n-150);
  --surface-disabled:  var(--p-n-050);
  --surface-highlight: var(--p-n-050);
  --surface-locked:    var(--p-n-150);
  --surface-error:     var(--p-red-050);
  --surface-viewer:    var(--p-n-100);

  /* Text */
  --text-primary:   var(--p-n-900);
  --text-emphasis:  var(--p-n-800);
  --text-body:      var(--p-n-750);
  --text-secondary: var(--p-n-700);
  --text-muted:     var(--p-n-600);
  --text-hint:      var(--p-n-600);
  --text-faint:     var(--p-n-400);
  --text-disabled:  var(--p-n-500);
  --text-readonly:  var(--p-n-700);
  --text-inverse:   var(--p-n-000);

  /* Borders */
  --border-strong:  var(--p-n-400);
  --border-default: var(--p-n-200);
  --border-subtle:  var(--p-n-150);
  --border-light:   var(--p-n-150);
  --border-divider: var(--p-n-100);
  --border-emphasis: var(--p-n-800);

  /* Emphasis fill — the primary button and the active tab. Deliberately a LIGHT
     grey carrying DARK ink (Alex, 2026-09-17: "change the few dark button to
     same as the top or something not as dark"). One step below the top bar, so
     it reads as filled without a near-black block anywhere on screen. */
  --fill-emphasis:        var(--p-n-300);
  --fill-emphasis-text:   var(--p-n-900);
  /* THE EDGE IS WHAT MAKES IT PRIMARY, not the fill (Alex, 2026-09-18, option C).
     The fill alone was 7 L* from --surface-selected, the row these buttons
     actually live on while you are editing it, so Save and Cancel read as the
     same button. Darkening the fill was the other candidate; a defined edge keeps
     the light fill Alex asked for and still holds on white AND on a selected row. */
  --fill-emphasis-border: var(--p-n-700);
  --fill-neutral:         var(--p-n-000);
  --fill-danger:          var(--p-red-500);

  /* Chrome — the app bar and the drag splitters */
  --chrome-bar:        var(--p-n-200);
  --chrome-bar-text:   var(--p-n-900);
  --chrome-bar-muted:  var(--p-n-700);
  --chrome-bar-border: var(--p-n-300);
  /* The INOVIU mark ships as ONE asset, white-on-transparent (inoviu-mark.js).
     A white mark vanishes on a light bar, so instead of a second asset that can
     drift from the first, the light theme inverts the one we have — it is a flat
     silhouette, so invert() gives a clean dark mark. Dark mode leaves it alone. */
  --brand-mark-filter: invert(1);
  /* The splitter is transparent at rest and takes this on hover, so it has to
     be visible against the page field the moment you reach for it — one ramp
     step darker than the bar (Alex, 2026-09-17: "update the color of the
     resizing vertical bar"). */
  --splitter-active:   var(--p-n-400);

  /* Status */
  --status-draft:      var(--p-n-600);
  --status-inprogress: var(--p-amber-600);
  --status-validated:  var(--p-blue-700);
  --status-synced:     var(--p-green-600);
  --status-deprecated: var(--p-red-600);
  --accent-product:    var(--p-violet-600);

  /* Warn / destructive */
  --warn-bg:     var(--p-amber-050);
  --warn-border: var(--p-amber-500);
  --warn-text:   var(--p-amber-900);
  --warn-accent: var(--p-amber-600);

  /* Reference chips */
  --chip-bg:        var(--p-n-150);
  --chip-bg-hover:  var(--p-n-200);
  --chip-border:    var(--p-n-300);
  --chip-text:      var(--p-n-800);
  --chip-badge-bg:  var(--p-n-200);
  --chip-badge-text: var(--p-n-700);

  /* Status glyph ground — a tint OF the surface, not a fixed black wash, so it
     survives the dark block instead of disappearing into it. */
  --glyph-bg:     var(--p-alpha-black-06);
  --glyph-border: var(--p-alpha-black-12);

  /* Microsoft sign-in button — the LIGHT treatment. */
  --ms-btn-bg:     var(--p-ms-ground-light);
  --ms-btn-text:   var(--p-ms-label-light);
  --ms-btn-border: var(--p-ms-border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 2 — SEMANTIC, DARK. Same names, dark values. Opt in with
   <html data-theme="dark">. Nothing else in the app changes.

   To make it follow the OS instead, add:
       @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { … } }
   with this same block. Left off on purpose — turning it on silently restyles
   the app for anyone whose OS is in dark mode.
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-theme="dark"] {
  color-scheme: dark;

  --surface-page:      var(--p-d-900);
  --surface-card:      var(--p-d-700);
  --surface-panel:     var(--p-d-800);
  --surface-sunken:    var(--p-d-600);
  --surface-band:      var(--p-d-600);
  --surface-hover:     var(--p-d-650);
  --surface-selected:  var(--p-blue-950);
  --surface-chip:      var(--p-d-600);
  --surface-disabled:  var(--p-d-600);
  --surface-highlight: var(--p-d-650);
  /* THM-2 — a locked row was invisible. This pointed at --p-d-600, the same step
     as --surface-sunken and one unit per channel from --surface-hover (#1c252b vs
     #1d262c, 1.013:1), so a row owned by Onshape sync and not editable here looked
     exactly like one you were merely hovering. Light separates them 1.115:1.
     #2a343c gives 1.227:1 against hover — deliberately MORE than light's own gap,
     because the same ratio reads weaker on a dark ground. Also clears sunken
     (1.345) and error (1.313), and stays close to selected (1.184) exactly as
     light does, so a locked row that is also selected still reads as selected.
     ⚠ The first value I shipped for this (#2a343c) was wrong in a way the row
     states alone could not show: it sat 1.011:1 from --border-default, so the
     locked row's own gridlines vanished into it. Light keeps those 1.119 apart.
     #303b45 restores that (1.098) — the constraint is not just "tell it from the
     rows", it is "tell it from the rows AND still see its own borders". */
  --surface-locked:    var(--p-d-350);
  --surface-error:     var(--p-red-950);
  --surface-viewer:    var(--p-d-800);

  /* THM-3 — the dark text hierarchy had collapsed. THREE pairs that are distinct
     in light resolved to ONE value here: primary/emphasis both --p-d-100, and
     body/secondary/readonly all --p-d-150. A copy-paste when this block was
     written, and it flattened the whole ladder: a mono code, a label and a value
     were the same brightness. Light's shape is restored — primary well clear of
     emphasis (1.382 here, 1.414 in light), emphasis sitting JUST above body
     (1.086 vs 1.043), body clear of secondary. Every step still clears 4.5:1 on
     a card: 14.08 / 10.19 / 9.39 / 7.47 / 5.49. */
  --text-primary:   var(--p-d-100);
  --text-emphasis:  var(--p-d-125);
  --text-body:      var(--p-d-150);
  --text-secondary: var(--p-d-175);
  --text-muted:     var(--p-d-200);
  --text-hint:      var(--p-d-200);
  --text-faint:     var(--p-d-250);
  --text-disabled:  var(--p-d-225);
  /* Same collapse, third instance: readonly equals SECONDARY in light (both
     #525b60), not body — so it follows secondary here, not --p-d-150. */
  --text-readonly:  var(--p-d-175);
  --text-inverse:   var(--p-d-900);

  --border-strong:  var(--p-d-275);
  --border-default: var(--p-d-400);
  --border-subtle:  var(--p-d-500);
  --border-light:   var(--p-d-500);
  --border-divider: var(--p-d-500);
  --border-emphasis: var(--p-d-100);

  --fill-emphasis:        var(--p-d-300);
  --fill-emphasis-text:   var(--p-d-100);
  /* Same job as the light edge, inverted: on dark the edge has to be LIGHTER
     than the fill to define the button. */
  --fill-emphasis-border: var(--p-d-200);
  --fill-neutral:         var(--p-d-700);
  --fill-danger:          var(--p-red-400);

  --chrome-bar:        var(--p-d-600);
  --chrome-bar-text:   var(--p-d-100);
  --chrome-bar-muted:  var(--p-d-200);
  --chrome-bar-border: var(--p-d-400);
  --brand-mark-filter: none;
  --splitter-active:   var(--p-d-275);

  --status-draft:      var(--p-d-200);
  --status-inprogress: var(--p-amber-400);
  --status-validated:  var(--p-blue-400);
  --status-synced:     var(--p-green-400);
  --status-deprecated: var(--p-red-400);
  --accent-product:    var(--p-violet-400);

  --warn-bg:     var(--p-amber-950);
  --warn-border: var(--p-amber-400);
  --warn-text:   var(--p-amber-200);
  --warn-accent: var(--p-amber-400);

  --chip-bg:        var(--p-d-600);
  --chip-bg-hover:  var(--p-d-500);
  --chip-border:    var(--p-d-400);
  --chip-text:      var(--p-d-100);
  --chip-badge-bg:  var(--p-d-500);
  --chip-badge-text: var(--p-d-150);

  --glyph-bg:     var(--p-alpha-white-08);
  --glyph-border: var(--p-alpha-white-16);

  /* Microsoft sign-in button — the DARK treatment. The mark's four colours do
     NOT change; only the ground and label do. */
  --ms-btn-bg:     var(--p-ms-ground-dark);
  --ms-btn-text:   var(--p-ms-label-dark);
  --ms-btn-border: var(--p-ms-border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYER 3 — THE TOKEN API components already call. Every name and every
   comment below is unchanged; only the VALUES moved to layer 2.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --th-bg:                  var(--surface-page);
  --th-accent:              var(--border-emphasis);
  /* Was the INOVIU navy #1F4E79. It is used ~93 times, and mostly as TEXT (mono
     codes, formula syntax, tree codes, breadcrumbs) — so it stays a DARK tone.
     The primary BUTTON that also used it is now --tg-save-bg below, which took
     the light emphasis fill instead. That split is the whole of "no dark
     buttons"; the name is kept so no component had to change. */
  --th-navy:                var(--text-emphasis);

  /* THE IDENTITY BAR — App.jsx's shell header, OnshapePanel's embedded header,
     OnshapeView's detail bar, AuthGate's card header. All four used
     `background: NAVY, color: --th-text-white`, which is why they went dark the
     moment --th-navy became a text tone. They are chrome, not text, so they get
     their own pair. --th-mark-filter goes on the <img> inside them. */
  --th-bar-bg:              var(--chrome-bar);
  --th-bar-text:            var(--chrome-bar-text);
  --th-bar-border:          var(--chrome-bar-border);
  --th-mark-filter:         var(--brand-mark-filter);

  --th-border:              var(--border-default);
  --th-border-light:        var(--border-light);

  --th-header-bg:           var(--surface-sunken);
  --th-row-even:            var(--surface-card);
  --th-row-odd:             var(--surface-panel);
  --th-row-hover:           var(--surface-hover);
  --th-row-selected:        var(--surface-selected);
  --th-row-selected-border: var(--border-emphasis);

  --th-font-size:           13px;
  /* PADDING INSIDE THE CELL, NOT AROUND IT (Alex, 2026-08-03: "my original intent
     was not to have less padding over all, but to have the cells not being
     smaller -- like if the padding was in the cell but not around").
     Yesterday's 0 conflated two things. The gap he wanted gone was the one AROUND
     an in-cell control -- an input at width:100% only fills the td's CONTENT box,
     so cell padding pushed its border inward and it floated inside its own cell.
     Killing the padding cured that but also put read-mode text on the gridline,
     which he never asked for. So they are separated now:
       --th-cell-pad     text inset for an ordinary read cell (restored to 4px 8px)
       --th-cell-inset   the SAME inset carried INSIDE a control, so its text lines
                         up with read mode and the box still spans the full cell
     A control that fills its cell zeroes the cell padding -- see the rule below. */
  /* Bumped 4px→8px vertical (2026-08-22, Alex: "the [Components] rows are kind of
     twice the size. I would like to try to have all the rows of all the grid
     twice the size" — Components' own ~39-40px row height, per --th-row-min-h
     above, was never a token difference from other grids; it comes from that
     grid's own multi-line cells. This is the lever that actually raises every
     OTHER grid's single-line rows to sit close to that height too. */
  --th-cell-pad:            8px 8px;
  --th-header-pad:          5px 8px;
  /* TEST VALUE (Alex, 2026-08-03: "can we do a test with the same 4 & 8 instead
     of 3 & 7 to see the differences"). Flip this one line to compare.
       4px 8px  same NUMBER as the cell -- but a control also draws a 1px border
                the cell does not, so its box ends up 2px taller and its text 1px
                lower than the read cell next to it.
       3px 7px  --th-cell-pad MINUS that border: box and text match to the pixel.
     Not an arbitrary preference either way -- 3/7 is 4/8 minus the border. */
  --th-cell-inset:          8px 8px;
  /* A <select> at 13px gets a 17px intrinsic line box from Chrome where a <span>
     gets 15px, so a dropdown sat 2px proud of the text cells around it (Alex,
     2026-08-03: "the drop down are slightly taller than the cell"). Pinning the
     controls to the text's own ratio removes that, independently of the inset. */
  --th-cell-line-h:         1.15;
  /* The height a body row settles at once it carries its icon buttons (measured
     2026-08-01: 39-40px). The EMPTY add row had none of that content and came out
     22px, reading as a thin gap rather than a row -- Alex: "when not selected, that
     hole looks like the other one but empty, which means a little bit wider". */
  --th-row-min-h:           38px;

  /* Type scale */
  --fs-title: 16px;
  --fs-body:  13px;
  --fs-mono:  13px;
  --fs-small: 11px;
  --fs-micro: 10px;
  --ff-mono:  ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --ff-body:  Arial, sans-serif;   /* same as body in index.html */

  /* Edit-control font. A grid cell sets these from its column's text kind
     (cellStyles.js cellFont), so an input in Edit mode reads exactly like the
     cell's read text (Alex, 2026-10-15: "in Edit mode, the font is generally
     smaller"). These root values are what a control gets OUTSIDE a grid cell:
     the size every edit control had before (fs-small). */
  --tg-cell-fs: var(--fs-small);
  --tg-cell-ff: var(--ff-body);
  --tg-cell-fw: 400;

  /* Tree / Grid tokens (--tg-*) — tree+grid styling lives here, not in component JSX.
     The tree's SHAPE is untouched (Alex, 2026-09-17: "keep the tree as it is
     beside the colors") — indents, paddings and border widths below are the same
     numbers as before; only the colours moved to layer 2. */
  --tg-node-active-bg:      var(--surface-selected);
  --tg-node-l0-bg:          var(--surface-hover);
  --tg-node-active-border:  3px solid var(--th-navy);
  --tg-node-inactive-border: 3px solid transparent;
  --tg-node-l0-border-btm:  1px solid var(--border-divider);
  --tg-node-l0-pad-y:       8px;
  --tg-node-l1-pad-y:       6px;
  --tg-node-l2-pad-y:       5px;
  --tg-node-pad-x:          8px;
  --tg-node-indent:         14px;
  --tg-code-color-l0:       var(--th-navy);
  --tg-code-color-other:    var(--text-secondary);
  --tg-label-color:         var(--text-body);
  --tg-filter-border:       1px solid var(--border-subtle);
  --tg-filter-pad:          6px 8px;

  /* Workspace splitter (HierarchyWorkspace nav-resize handle hover) */
  --tg-splitter-hover:      var(--splitter-active);

  /* Status glyph — shape carries meaning; muted so it blends like a code chip */
  --tg-glyph-bg:            var(--glyph-bg);
  --tg-glyph-color:         var(--text-secondary);
  --tg-glyph-border:        1px solid var(--glyph-border);
  --tg-glyph-radius:        3px;
  --tg-glyph-pad:           1px 5px;

  /* DataGrid edit-mode action buttons. --tg-save-bg is THE primary button fill
     (theme-tokens.js BTN_PRIMARY) — it used to be --th-navy and is now the light
     emphasis fill with dark ink on it. */
  --tg-save-bg:             var(--fill-emphasis);
  --tg-save-color:          var(--fill-emphasis-text);
  --tg-save-border:         1px solid var(--fill-emphasis-border);
  --tg-cancel-bg:           var(--fill-neutral);
  --tg-cancel-color:        var(--text-secondary);
  --tg-cancel-border:       1px solid var(--border-subtle);
  --tg-editbtn-pad:         2px 10px;
  --tg-editbtn-radius:      3px;

  /* Delete/warn row colours */
  --tg-warn-bg:             var(--warn-bg);
  --tg-warn-border-left:    3px solid var(--warn-border);
  --tg-warn-color:          var(--warn-text);

  /* Destructive action (delete confirm button) */
  --tg-delete-bg:           var(--fill-danger);

  /* Grid action icons (⧉ ✕ ⠿ ✎) + # columns — darker than --th-border so they
     read clearly against row backgrounds (F2, Alex 2026-07-03). Reuses the
     existing secondary-text tone rather than minting a new value. */
  --tg-icon-color:          var(--th-text-secondary);

  /* Semantic text colors (for tokenizing straggler components) */
  --th-text-body:           var(--text-body);        /* dark body text */
  --th-text-secondary:      var(--text-secondary);   /* secondary / muted labels */
  --th-text-muted:          var(--text-muted);       /* breadcrumb separators, light muted */
  --th-text-hint:           var(--text-hint);        /* hint / label row prefixes */
  --th-text-faint:          var(--text-faint);       /* very faint / placeholder dashes */
  --th-text-disabled:       var(--text-disabled);    /* disabled / placeholder text */
  --th-warn-accent:         var(--warn-accent);      /* amber warning (off-typical params) */
  --th-panel-bg:            var(--surface-panel);    /* side panel background */

  /* Status badge colors (StatusCell — icon+word, color-blind safe) */
  --th-status-draft:        var(--status-draft);
  --th-status-inprogress:   var(--status-inprogress);
  --th-status-validated:    var(--status-validated);
  --th-status-synced:       var(--status-synced);
  --th-status-deprecated:   var(--status-deprecated);

  /* Reference chip colors (ExplainerSubLine) */
  --tg-chip-bg:             var(--chip-bg);
  --tg-chip-bg-hover:       var(--chip-bg-hover);
  --tg-chip-border:         var(--chip-border);
  --tg-chip-color:          var(--chip-text);
  --tg-chip-badge-bg:       var(--chip-badge-bg);
  --tg-chip-badge-color:    var(--chip-badge-text);

  /* Subtle borders and surfaces (ParameterPanel, misc) */
  --th-border-subtle:       var(--border-subtle);     /* light border (panels, inputs) */
  --th-surface-disabled:    var(--surface-disabled);  /* disabled/inactive button bg */
  --th-surface-chip:        var(--surface-chip);      /* inline kind/tag chip background */
  --th-text-white:          var(--text-inverse);      /* white text on dark backgrounds */

  /* ROW STATE BACKGROUNDS (2026-08-04) — a whole row saying something about
     itself. Both are DELIBERATELY heavier than --th-surface-disabled: against a
     white row that reads as the zebra banding the grid removed on purpose, not
     as a state. Neither carries meaning ALONE — Alex is colour-blind, so the row
     always also carries an icon or the word: these only make a state findable
     when scanning, they never encode it. */
  --th-row-locked:          var(--surface-locked);  /* row owned elsewhere (Onshape sync) — not editable here */
  --th-row-error:           var(--surface-error);   /* row whose value failed to resolve/validate */

  /* NIGHT_SHIFT item 9 (2026-07-20) — raw hex closed. Each token below maps
     1:1 to a value that was previously hardcoded inline; no visual change. */
  --th-navy-dark:           var(--chrome-bar-border); /* header border-bottom — the bar's own edge */
  --th-navy-text-muted:     var(--chrome-bar-muted); /* label/nav text on the app header bar */
  --th-product-accent:      var(--accent-product);   /* "Product" source kind accent (vs --th-navy for Catalog) */
  --th-readonly-text:       var(--text-readonly);    /* read-only field text (component ref display) */
  --th-highlight-bg:        var(--surface-highlight);/* editing/add-param sub-row panel background */
  --th-highlight-border:    var(--border-subtle);    /* editing/add-param sub-row panel border */
  --th-card-bg:             var(--surface-panel);    /* create-form card background */
  --th-card-border:         var(--border-subtle);    /* create-form card border */
  --th-divider-light:       var(--border-divider);   /* search-result row divider */
  --th-chip-alt-bg:         var(--surface-chip);     /* category chip background (paired with navy text) */
  --th-viewer-bg:           var(--surface-viewer);   /* 3D model-viewer placeholder background */

  /* MICROSOFT SIGN-IN BUTTON (components/MicrosoftSignInButton.jsx).
     Separate from every other button token on purpose: this one is Microsoft's
     design, not ours, so it must NOT follow --fill-emphasis or the type scale
     when we retheme. --fs-ms-btn is 15px because the guidelines say 15px, which
     is not a size anywhere else in this app uses. */
  --th-ms-btn-bg:           var(--ms-btn-bg);
  --th-ms-btn-text:         var(--ms-btn-text);
  --th-ms-btn-border:       var(--ms-btn-border);
  --th-ms-red:              var(--p-ms-red);
  --th-ms-green:            var(--p-ms-green);
  --th-ms-blue:             var(--p-ms-blue);
  --th-ms-yellow:           var(--p-ms-yellow);
  --fs-ms-btn:              15px;
  --ff-ms-btn:              'Segoe UI', system-ui, -apple-system, sans-serif;
}
