/* ============================================================================
   Cordis Suite — design tokens v2.0
   One source for CRM (React), MachineControlServer (Blazor), Studio/Modeler
   and every new application.

   Rules:
   1. Application code uses ONLY the semantic tokens (--cordis-*).
      The primitives (--cordis-blue-500 etc.) exist solely to feed them.
   2. Never a hex in application code.
   3. Dark mode = redefining the semantic layer only.
   4. Two axes and no more: profile (Shell / Studio) × theme (light / dark).
      The density axis was retired by decision D11.
   5. A theme can be scoped: `data-theme="light"` or `"dark"` on any element (an HMI panel that
      is always dark in a light shell) re-declares the tokens there. `[data-theme]` joins `:root`
      on both light blocks because a `var()` inside a custom property is resolved where it is
      declared: declared on :root only, every derived token would keep the shell's value inside
      the scoped subtree. The dark blocks come later at the same specificity, so they still win.
   ========================================================================= */

:root, [data-theme] {
  /* ---------- Primitives · blue (sampled from the Cordis logo) ---------- */
  --cordis-blue-50:  #EBF7FD;
  --cordis-blue-100: #D2ECFA;
  --cordis-blue-200: #A9DCF5;
  --cordis-blue-300: #8FD3F5;  /* logo highlight */
  --cordis-blue-400: #45B1E4;
  --cordis-blue-500: #1F9CD8;  /* logo base — the Cordis blue */
  --cordis-blue-600: #1487C8;  /* logo shadow */
  --cordis-blue-700: #0F6A97;
  --cordis-blue-800: #0B4F71;
  --cordis-blue-900: #08364E;
  --cordis-blue-950: #06222F;  /* navigation rail */

  /* ---------- Primitives · green (logo) ---------- */
  --cordis-green-50:  #EDF8EC;
  --cordis-green-100: #D7EFD5;
  --cordis-green-500: #3BAA35;  /* logo base */
  --cordis-green-600: #2E8A2A;
  --cordis-green-700: #1F6B20;  /* logo deep */

  /* ---------- Primitives · red (logo) ---------- */
  --cordis-red-50:  #FDECEE;
  --cordis-red-100: #FAD3D6;
  --cordis-red-500: #E20717;  /* logo base */
  --cordis-red-600: #C00612;
  --cordis-red-700: #8F0510;

  /* ---------- Primitives · amber (derived, not in the logo) ---------- */
  --cordis-amber-50:  #FEF6E7;
  --cordis-amber-100: #FBE7C2;
  --cordis-amber-200: #FAD08B;
  --cordis-amber-300: #F7BA55;
  --cordis-amber-400: #F5A524;  /* = --cordis-chart-amber; the Cordis Server's live-state orange */
  --cordis-amber-500: #C97A0E;
  --cordis-amber-700: #8A5208;

  /* ---------- Primitives · neutral (cool, blue-tinted) ---------- */
  --cordis-n-0:   #FFFFFF;
  --cordis-n-25:  #F7F9FB;
  --cordis-n-50:  #F1F4F8;
  --cordis-n-100: #E5EAF0;
  --cordis-n-200: #D2DAE3;
  --cordis-n-300: #B2BECD;
  --cordis-n-400: #8494A8;
  --cordis-n-500: #5F7085;
  --cordis-n-600: #465667;
  --cordis-n-700: #33404E;
  --cordis-n-800: #212B36;
  --cordis-n-900: #131A21;

  /* ---------- Semantic · surface ---------- */
  --cordis-bg:             var(--cordis-n-50);
  --cordis-surface:        var(--cordis-n-0);
  --cordis-surface-sunken: var(--cordis-n-25);
  --cordis-surface-raised: var(--cordis-n-0);
  /* The workbench CHROME surface: the explorer, the inspector, the bottom and terminal pane -- one step
     greyer than the white editor between them (cordis-studio #1889). Promoted out of the compat shim's
     --cordis-sidebar-bg, which had been its only declaration, so every reader of it read a legacy name.
     A literal rather than a ramp alias on purpose: the nearest rung, --cordis-n-50 #F1F4F8, is close but
     not equal, and the chrome is a surface users have looked at for years. In dark it is the panel
     surface itself. */
  --cordis-surface-chrome: #F2F3F6;
  --cordis-border:         var(--cordis-n-100);
  --cordis-border-strong:  var(--cordis-n-200);
  --cordis-overlay:        rgba(19, 26, 33, 0.45);

  /* ---------- Semantic · text ---------- */
  /* THREE STEPS, AND ALL THREE ARE REAL. Until 2026-08 `muted` and `faint` were both n-500 --
     the same value -- so a caller reaching for the fainter of the two got no visual result and no
     warning, and a three-level hierarchy silently collapsed to two. The old comment here defended
     the duplication by rejecting the wrong alternative: n-400 (#8494A8) measures 3.10:1 on
     --cordis-surface and is indeed a border grey rather than a text colour, so lightening `faint`
     was never the answer.
     The answer was the rung that was missing: `muted` moves DOWN to n-600 and `faint` keeps the
     pixels it already had. Nothing that reads `faint` changes appearance; `muted` gets darker,
     which no contrast rule can object to. Measured on --cordis-surface (#FFFFFF):
         text 14.4:1   muted 7.5:1   faint 5.1:1
     which is also, and this is the argument for these particular values, the ladder dark has had
     all along on its own --cordis-surface (#282826): 12.6 / 7.0 / 5.3. The themes now agree on
     what three levels of text mean instead of only one of them having three.
     Reported by a consumer, 2026-08 (item 4). Pinned by TextLadderTests -- both themes, both
     directions: ordered, every step visible, every rung >= 4.5:1. */
  --cordis-text:         var(--cordis-n-800);
  --cordis-text-strong:  var(--cordis-n-900);
  --cordis-text-muted:   var(--cordis-n-600);
  --cordis-text-faint:   var(--cordis-n-500);
  --cordis-text-on-fill: #FFFFFF;
  /* Text on a filled WARNING ground (an amber command button, a badge). White on amber-500 is
     ~3:1 and on the dark theme's lighter amber worse, so amber takes near-black ink in both
     themes; the fill is light in both, which is why this one is declared once and not redeclared
     under [data-theme="dark"]. Replaces the Bootstrap #212529 the Server's command button carried. */
  --cordis-text-on-warning: var(--cordis-n-900);
  /* Text on a model-check SEVERITY fill: the count on CordisBadge's error / warning / info tones
     (Severity="…", the tree's and member rows' diagnostic badge). The fills are the primitives
     red-500 / amber-500 / blue-500, which do not step between themes, so the ink must not step
     either: declared ONCE, here, and deliberately not redeclared under [data-theme="dark"].
     It was a literal #fff in CordisDiagnosticBadge.razor.css while that sheet was its only reader
     ("one call site is not a token"); decision D16 folded that badge into CordisBadge, a shared
     primitive, so the value became part of the contract and got a name. Not --cordis-text-on-fill:
     that ink belongs to the accent fills and is free to follow them; this one belongs to fills that
     cannot. Measured by DiagnosticBadgeInkTests on all three fills in both themes: error 4.91:1
     (held to 4.5), amber 3.35:1 and blue 3.09:1 (held to 3.0 -- a 10px count on a 14px chip,
     the recorded white-on-blue-500 trade-off). */
  --cordis-text-on-severity: #FFFFFF;

  /* ---------- Semantic · action ----------
     THE BRAND BLUE IS THE PRIMARY. Blue 500 #1F9CD8 is the colour sampled from
     the logo and the one the specification document (reference/index.html)
     shows, and a filled Cordis button is now that colour rather than a darker
     relative of it. Decided 2026-08.

     What this costs, stated plainly rather than left to be discovered: white on
     #1F9CD8 measures 3.1:1, so a filled primary button does not reach the 4.5:1
     the rest of this system holds itself to. That was weighed and the brand won,
     on the same grounds as the Studio status bar (D4) — a filled button is a
     large, high-salience target with a label in semibold, not body copy. The
     rule the suite still keeps is the one about TEXT: --cordis-primary-text stays
     blue-700 so blue-on-white prose passes, and the hover and active states step
     down the ramp, so the two states a user is actually reading during an
     interaction both pass.

     Hover/active step to 600/700 rather than 700/800: the resting state is no
     longer the darkest rung, so the whole interaction ladder shifts up one. */
  --cordis-primary:         var(--cordis-blue-500);
  --cordis-primary-hover:   var(--cordis-blue-600);
  --cordis-primary-active:  var(--cordis-blue-700);
  --cordis-accent:          var(--cordis-primary);
  --cordis-primary-soft:    var(--cordis-blue-50);
  --cordis-primary-soft-bd: var(--cordis-blue-200);
  --cordis-primary-text:    var(--cordis-blue-700); /* blue text on white — AA */
  /* ONE ACCENT SURFACE. The focus ring is the same blue as everything else
     accent-coloured, and it says `var(--cordis-primary)` rather than naming a
     rung so it cannot drift from one again.

     It used to be blue-700, and the reason it was is worth keeping because it
     stopped being true. The ring was once blue-500 at 30% ALPHA, which resolves
     to about #C8E7F6 on white and measures ~1.4:1 — a ring the system claimed
     was visible and was not. Stepping the colour down the ramp fixed the
     symptom. But the ring is now a full-opacity 2px box-shadow, and at full
     opacity blue-500 on white measures 3.1:1, which is what WCAG 1.4.11 asks of
     a non-text UI component (3:1) — the 4.5:1 bar the old comment cited is the
     TEXT rule, applied here to a border. Holding a border to the text threshold
     is what put a second blue in the system, and a second accent blue is a
     bigger defect than 3.1:1 on a 2px outline: Cordis Studio has exactly one
     accent (its app.css declares `--cordis-blue: #1f9cd8` and everything reads
     it), and a suite whose focus ring is a different blue from its status bar
     does not look like one product.

     A box-shadow ring already follows the focused element's OWN border-radius,
     which is why the universal :focus-visible rule in cordis.css does not force
     a radius onto it — see the comment there. */
  --cordis-focus-color:     var(--cordis-primary);
  --cordis-focus-ring:      0 0 0 2px var(--cordis-focus-color);

  /* ---------- Semantic · entry canvas ----------
     The brand gradient behind login, licence and activation screens.
     Taken from MachineControlServer, but rebuilt out of the ramp. */
  --cordis-canvas-brand: linear-gradient(215deg,
      var(--cordis-n-100) 0%,
      var(--cordis-blue-200) 42%,
      var(--cordis-blue-700) 100%);
  --cordis-canvas-brand-text: var(--cordis-blue-900);

  /* ---------- Semantic · navigation ---------- */
  --cordis-nav-bg:        var(--cordis-blue-950);
  --cordis-nav-text:      #B7D5E8;
  --cordis-nav-text-on:   #FFFFFF;
  --cordis-nav-active:    var(--cordis-blue-700);
  --cordis-nav-hover:     rgba(255, 255, 255, 0.07);
  --cordis-nav-divider:   rgba(255, 255, 255, 0.09);
  /* Text and status ON a navigation surface. The ordinary --cordis-text-* tokens
     are for light surfaces and become unreadable on nav-bg. */
  --cordis-nav-text-muted: #8FB3CC;
  --cordis-nav-accent:     #8FD3F5;
  --cordis-nav-warning:    #F0C983;
  --cordis-nav-success:    #9BE096;
  --cordis-nav-danger:     #FF9EA3;

  /* ---------- Semantic · status bar ----------
     Decision D4, decided 2026-08, and the answer is per PROFILE because the two
     bars are not the same object. In SHELL the value below applies: dark
     blue-950 on brand blue, 5.3:1, because a Shell status bar carries readable
     information a user is expected to parse — a record count, a save state, a
     validation summary. In STUDIO it is white (cordis-studio.css overrides it):
     Studio's bar is window chrome in the manner of an IDE, glanced at rather
     than read, and white on brand blue is what a Cordis workbench has always
     looked like. 3.1:1, accepted deliberately there and nowhere else.

     The bar is brand-coloured in both themes, so this pair is declared in the
     dark layer too. */
  --cordis-statusbar-bg:   var(--cordis-blue-500);
  --cordis-statusbar-text: var(--cordis-blue-950);

  /* ---------- Semantic · inverse surface ----------
     A surface that deliberately contrasts with the page instead of belonging to
     it: the Shell toast pill, and anything else that has to read as laid OVER
     the document. It needs its own pair because the obvious shortcut does not
     work — `background: var(--cordis-text-strong)` over
     `color: var(--cordis-n-0)` is an inversion that only holds while the text
     colour is dark, and under [data-theme="dark"] --cordis-text-strong IS
     #FFFFFF, so the surface measures 1:1 against its own text and disappears.
     That was a real, shipped defect in the retired `.cds-toast`, found only
     because the family merge put the two toasts side by side.

     Redeclared in the dark layer below, which is the whole point: an inverse
     surface is the one thing that must flip when the theme does. */
  --cordis-inverse-bg:   var(--cordis-n-900);
  --cordis-inverse-text: var(--cordis-n-25);

  /* ---------- Semantic · tooltip chip ----------
     Deliberately dark in BOTH themes, which is why the five values are literal
     and are not redeclared in the dark layer below. A tooltip is a transient
     chip laid over the page rather than a surface belonging to it, and every
     system the suite is measured against draws it dark on light and dark on
     dark. The alternative — a chip that inverts with the theme — makes the
     hint indistinguishable from the panel it covers in dark mode, which is the
     exact defect the retired `.cds-tooltip` had (it was built out of the TEXT
     ramp: --cordis-text-strong ground over --cordis-n-0 text, and in dark mode
     both resolve to #FFFFFF, so the chip measured 1:1 and was invisible).

     Promoted out of compat/cordis-legacy.css by D10 family merge #6. */
  --cordis-tooltip-bg:     #2A2E38;
  --cordis-tooltip-fg:     #EEF1F6;
  --cordis-tooltip-border: rgba(255, 255, 255, 0.10);
  --cordis-tooltip-kbd:    #9AA2AF;
  /* The keycap outline a combo's keys wear inside the chip (.cordis-kbd): the chip's own
     hairline is 0.10, too faint to draw an 18px key on #2A2E38. */
  --cordis-tooltip-kbd-border: rgba(255, 255, 255, 0.22);

  /* ---------- Semantic · status ---------- */
  --cordis-success:      var(--cordis-green-500);
  --cordis-success-soft: var(--cordis-green-50);
  --cordis-success-text: var(--cordis-green-700);
  --cordis-warning:      var(--cordis-amber-500);
  --cordis-warning-soft: var(--cordis-amber-50);
  --cordis-warning-text: var(--cordis-amber-700);
  --cordis-danger:       var(--cordis-red-500);
  --cordis-danger-soft:  var(--cordis-red-50);
  --cordis-danger-text:  var(--cordis-red-700);
  --cordis-info:         var(--cordis-blue-500);
  --cordis-info-soft:    var(--cordis-blue-50);
  --cordis-info-soft-bd: var(--cordis-blue-200);  /* .cordis-notice draws a real border; the severity
                                               variants deliberately blend theirs into the fill */
  --cordis-info-text:    var(--cordis-blue-700);
  --cordis-neutral-soft: var(--cordis-n-100);
  --cordis-neutral-text: var(--cordis-n-600);

  /* ---------- Semantic · live machine state ----------
     Where a RUNNING state machine is, drawn on a diagram: the state it is in now, and the trail of
     states behind it, one step fainter each. Shared because two applications draw the same machine and
     a commissioning engineer switches between them — the Cordis Server's live dashboard and Studio's
     debug canvas must light the same state in the same colour. Orange because the Server has always
     drawn it orange (#FFA500 / #FFCF75 / #FFEBC6), and these are that ladder on the amber ramp.
     THE COLOUR IS ON THE FRAME, NOT THE BODY. A state box carries code: syntax colours and live-value
     chips are drawn for a near-neutral ground and lose their contrast on a full amber fill, and a live
     composite painted solid swallows every substate drawn inside it. So the header band and the border
     carry the orange, and the body only takes a faint tint.
       live          the body of the current state (a tint)     live-band   its header band (the orange)
       live-bd       its border                                 live-trail-1/2/3   the header band one, two,
                                                                three steps back (their bodies stay as they are)
     A list of the same history (a table row, no code in it) takes live-band for the current row and the
     trail rungs behind it. Fills, not text: ink on them is --cordis-text, which reads on every rung. */
  --cordis-live:         var(--cordis-amber-50);
  --cordis-live-band:    var(--cordis-amber-300);
  --cordis-live-bd:      var(--cordis-amber-500);
  --cordis-live-trail-1: var(--cordis-amber-200);
  --cordis-live-trail-2: var(--cordis-amber-100);
  --cordis-live-trail-3: var(--cordis-amber-50);

  /* ---------- Category badges (kind, not state) ----------
     Taken from the CRM. A fixed assignment per value list; never for
     status — that has five meanings and a shape of its own (pill with a dot). */
  --cordis-cat-blue:   #0F6A97;  --cordis-cat-blue-soft:   rgba(31,156,216,0.13);
  --cordis-cat-violet: #4A2FB8;  --cordis-cat-violet-soft: rgba(124,92,255,0.12);
  --cordis-cat-amber:  #8A5208;  --cordis-cat-amber-soft:  rgba(245,165,36,0.13);
  --cordis-cat-orange: #A44205;  --cordis-cat-orange-soft: rgba(249,115,22,0.12);
  --cordis-cat-green:  #1F6B20;  --cordis-cat-green-soft:  rgba(18,161,80,0.12);
  --cordis-cat-grey:   #4A5560;  --cordis-cat-grey-soft:   rgba(105,121,131,0.13);
  --cordis-cat-red:    #A11220;  --cordis-cat-red-soft:    rgba(229,72,77,0.12);
  /* Four more since 2026-09, for the state timeline's known state names (CordisChartInks.KnownStates):
     eleven fixed names, each on an ink of its own, where seven put three of them on one green. */
  --cordis-cat-teal:   #0B6B63;  --cordis-cat-teal-soft:   rgba(20,184,166,0.13);
  --cordis-cat-pink:   #A3195B;  --cordis-cat-pink-soft:   rgba(236,72,153,0.12);
  --cordis-cat-lime:   #4A6800;  --cordis-cat-lime-soft:   rgba(132,204,22,0.14);
  --cordis-cat-brown:  #7A4B2A;  --cordis-cat-brown-soft:  rgba(160,110,70,0.14);

  /* ---------- Studio · data types ----------
     The only hue outside the four brand colours. For user-defined data types
     (enumerations, complex types) in Studio only. */
  --cordis-type-violet: #8B5CF6;
  --cordis-type-violet-dark: #B79BFF;  /* on dark surfaces; 8B5CF6 does not reach 4.5:1 there */

  /* ---------- Data visualisation (one blue ramp; status = meaning) -------- */
  --cordis-chart-1: var(--cordis-blue-700);
  --cordis-chart-2: var(--cordis-blue-500);
  --cordis-chart-3: var(--cordis-blue-400);
  --cordis-chart-4: var(--cordis-blue-300);
  --cordis-chart-5: var(--cordis-n-300);
  /* The dashed gridline, aliased to --cordis-border rather than naming a grey of its
     own. The two answer the same question -- what is the quietest line still visible
     on a surface -- and the dark half of this token answered it wrongly for a year:
     it was #1B262E, which is --cordis-neutral-soft's value, a soft BADGE FILL picked
     against the dark bg (#1B1F26) and therefore DARKER than the #242932 card a chart
     is drawn on. 1.05:1. The gridlines were not there. That is precisely the defect
     the --cordis-border comment in the dark block below records, one token over, so
     it gets the same answer rather than a second measurement. */
  --cordis-chart-track: var(--cordis-border);
  /* The chart's category inks: a state-timeline band nobody coloured, a many-series palette. Solid FILLS, so
     they are the Cordis hues at full strength (the logo blue and green, the -soft washes' own hues), not the
     --cordis-cat-* badge inks, which are darkened for TEXT on a pale wash and drew bands in slate and brown.
     The label inside a band picks its own ink from the fill's luminance (inkOn in js/cordis-charts.js). Grey
     and slate are the two quiet ones, for the states that mean "not running" (Inactive, Stopped). */
  --cordis-chart-blue:   var(--cordis-blue-500);
  --cordis-chart-violet: #7C5CFF;
  --cordis-chart-amber:  #F5A524;
  --cordis-chart-green:  var(--cordis-green-500);
  --cordis-chart-orange: #F97316;
  --cordis-chart-red:    #E5484D;
  --cordis-chart-grey:   #B4BEC6;
  --cordis-chart-teal:   #14B8A6;
  --cordis-chart-pink:   #EC4899;
  --cordis-chart-lime:   #84CC16;
  --cordis-chart-slate:  #6B7C8C;
  /* Cordis yellow, the logo's fourth colour: a panel's fourth line (PANEL in js/cordis-charts.js). */
  --cordis-chart-yellow: #FFC600;

  /* ---------- Typography ----------
     Both families are SHIPPED as of decision D6 (2026-08): cordis.css declares
     the @font-face for each against fonts/IBMPlexSans-latin.woff2 and
     fonts/IBMPlexMono-latin.woff2. The system-ui tail is a genuine fallback for
     the frame before the face arrives, not the value every application quietly
     rendered in for the first year of this file's life. */
  --cordis-font:      'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --cordis-font-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', monospace;
  --cordis-fs-display: 30px; --cordis-lh-display: 38px;
  --cordis-fs-h1:  24px;     --cordis-lh-h1:  32px;
  --cordis-fs-h2:  18px;     --cordis-lh-h2:  26px;
  --cordis-fs-h3:  15px;     --cordis-lh-h3:  22px;
  --cordis-fs-body:14px;     --cordis-lh-body:21px;
  --cordis-fs-sm:  13px;     --cordis-lh-sm:  20px;
  --cordis-fs-xs:  12px;     --cordis-lh-xs:  18px;
  /* The caption: 11px, semibold, uppercase, tracked by --cordis-ls-caption. The size and tracking are tokens and
     the uppercase is the .cordis-caption class (cordis.css); a sheet that sets a caption on its own element
     writes those three and takes the tracking from here, never as a literal of its own. 0.08em is the Shell
     value. Studio redeclares the tracking at 0.04em on .cordis-studio (cordis-studio.css), because an 11px
     caption inside a 26px row is the one place a wide tracking costs the row its width. */
  --cordis-fs-caption: 11px; --cordis-ls-caption: 0.08em;
  --cordis-fw-regular: 400; --cordis-fw-medium: 500; --cordis-fw-semibold: 600; --cordis-fw-bold: 700;

  /* ---------- Space (4px grid) ---------- */
  --cordis-space-1: 4px;  --cordis-space-2: 8px;  --cordis-space-3: 12px;
  --cordis-space-4: 16px; --cordis-space-5: 24px; --cordis-space-6: 32px;
  --cordis-space-7: 48px; --cordis-space-8: 64px;
  /* The half-steps (issue #42), for the gaps a whole step cannot fit: a glyph beside a label, a chip's inner
     pad, the padding of a 26px row. N-h is (N + 0.5) x 4px -- 6, 10, 14 -- and 0h (2px) is Studio's, in
     cordis-studio.css. They are ABSOLUTE, in both profiles, and no profile restates them: the profiles compress
     the rungs from 3 up because they are a page's RHYTHM, and a half-step is never rhythm, it is a fit between two
     things inside one control. Use a rung for the gap between regions and a half-step for the gap inside one. */
  --cordis-space-1h: 6px; --cordis-space-2h: 10px; --cordis-space-3h: 14px;
  /* The two 1px spacings (cordis-studio#1777). Both are absolute and both are named for what they hold apart, because
     1px is not a step of any scale: a token is the promise that every site of one meaning moves together.
     gap-hairline: between items set edge to edge with no rhythm between them -- the rows of a navigation list, the
       buttons of an action strip, a toolbar's buttons, crumbs -- so two hover backgrounds never merge into one.
     chip-pad-y: the vertical pad of an inline chip set in caption or xs text -- a code chip, a key cap, a tag, a
       count -- whose height comes from its text; the pad only keeps the fill off the glyphs. */
  --cordis-gap-hairline: 1px; --cordis-chip-pad-y: 1px;

  /* ---------- Radius ----------
     Decision D9 (2026-08): the profiles diverge in CHARACTER, not only in size,
     and radius is one of the three places that happens. This is the Shell scale
     and it takes the web end — controls 6, panels 10, modals 16, the CRM's own
     values.

     Studio keeps this 6px small step, unchanged, and takes 8px for both md and lg
     (cordis-studio.css): a workbench panel is a region of a window, and a 16px
     corner on a splitter pane looks like a card that has come loose. Those two
     numbers are MEASURED off Cordis Studio's own stylesheet rather than chosen,
     and the measurement is a mild embarrassment to D9 — the pre-D9 compromise
     scale was 6/8/12, so on radius specifically the compromise had Studio very
     nearly right and it is Shell that moved. Radius is the weakest of D9's three
     divergences; shadow and the spacing rhythm are where the profiles really do
     part company. */
  --cordis-radius-sm: 6px; --cordis-radius-md: 10px; --cordis-radius-lg: 16px;
  --cordis-radius-pill: 999px;

  /* ---------- Elevation ----------
     The second half of D9. A Shell page is cards and sheets on a background, so
     the ladder is soft and generous. Studio nulls --cordis-shadow-sm outright and
     tightens the other two (cordis-studio.css): a workbench separates surfaces
     with 1px borders, and a resting panel that floats is a panel that looks
     detachable when it is not. Overlays keep a shadow in both profiles, because
     an overlay has to read as being above the thing it covers. */
  --cordis-shadow-sm: 0 1px 2px rgba(19, 26, 33, 0.06);
  --cordis-shadow-md: 0 4px 12px rgba(19, 26, 33, 0.08);
  --cordis-shadow-lg: 0 12px 32px rgba(19, 26, 33, 0.14);

  /* ---------- Layering (decision D16) ----------
     The three rungs a modal surface sits on, one backdrop (.cordis-scrim) for all of
     them. --cordis-z-scrim is a page's own modal or drawer: above the off-canvas rail
     (40). --cordis-z-dialog is CordisDialogService's dialog, and --cordis-z-prompt a
     prompt / confirm, which must stack ABOVE an open dialog — a dialog asks "Discard
     changes?" over itself. Menus (2000) sit between the two: a menu opened inside a
     dialog shows over it, and a prompt covers both. */
  --cordis-z-scrim: 50; --cordis-z-dialog: 1100; --cordis-z-prompt: 2100;

  /* ---------- Motion ----------
     Durations from this document, curves from the CRM. --cordis-ease for anything
     moving from one state to another, --cordis-ease-out for anything arriving. */
  --cordis-ease:     cubic-bezier(0.4, 0, 0.2, 1);
  --cordis-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --cordis-dur-fast: 120ms; --cordis-dur: 180ms; --cordis-dur-slow: 260ms;

  /* ---------- Metrics · the Shell scale ----------
     These seven were "density · comfortable" until D11 (2026-08) retired the
     density axis. There is no [data-density="compact"] any more: it moved five of
     these to roughly Studio's values, which is precisely the coincidence that
     made Studio look like "compact Shell" and hid the fact that the profiles are
     two kinds of application. Profile is the axis; there is no third one.

     --cordis-fs-row is decision D7, and it is declared HERE, among the metrics,
     rather than up in the type scale — that placement is the answer. Row text
     scales with the row it sits in: 14px in a 44px Shell row, 12.5px in a 26px
     Studio row (cordis-studio.css). Before this, a tree row's text size was
     pinned to a global step while the row height came from the profile, so
     making Studio's rows taller left the text where it was. A 12.5px step is
     therefore NOT a new rung on the suite's type scale — it is a metric that
     belongs to a profile, which is why it never fitted between --cordis-fs-xs and
     --cordis-fs-sm. */
  --cordis-control-h:    36px;
  --cordis-control-px:   16px;
  --cordis-control-fs:   14px;
  --cordis-row-h:        44px;
  /* A tree row IS a list row until Studio tightens it, so the Shell value is the
     general row height rather than a number of its own. cordis-studio.css declares
     the 26px workbench value. */
  --cordis-tree-row-h:   var(--cordis-row-h);
  --cordis-fs-row:       var(--cordis-fs-body);
  /* The height of a status pill / category tag, and it is a ROW metric rather than a
     step on some tag scale — same reasoning as --cordis-fs-row above (decision D7).
     A tag's usual home is a table cell, and both shapes used to set a literal 4px of
     block padding, so their height was decided by the font and came out at ~23px in
     BOTH profiles: comfortable in a 44px Shell row and flush against the borders of a
     26px Studio one, which read as a broken cell rather than a tag. Deriving it from
     the row keeps the same air on either side in both profiles. */
  --cordis-tag-h:        24px;
  /* The controls of an editor toolbar band (CordisEditorToolbar): Studio's small diagram
     tool buttons, a step under the profile's control height, so a band of a dozen tools
     stays one slim row above the surface it edits. Studio's value is its own 26px. */
  --cordis-toolbar-control-h: 28px;
  /* The dense scale: a list that is scanned rather than read (a dashboard panel's table or tree, a
     message list, a picker's tree), in either profile. Studio's own rows are 26px, so the dense row
     is the one metric where a Shell surface ends up a step taller than Studio's default, and that is
     deliberate: dense is a mode a Shell page chooses for one surface, not a third profile (D11), so
     it stays one set of values. The control is what sits IN such a row (a run button, an inline
     editor, a panel header's small tool button); the tag is a status pill or chip in that row.
     Consumers set the profile metrics FROM these (`--cordis-row-h: var(--cordis-dense-row-h)`) or
     use the classes that do (`.cordis-table--dense`, `.cordis-dense-tools`); they never restate
     the pixels. Cordis Server #4025. */
  --cordis-dense-row-h:     28px;
  --cordis-dense-control-h: 24px;
  --cordis-dense-tag-h:     20px;
  --cordis-card-p:       20px;
  --cordis-section-gap:  24px;

  /* ---------- The page ----------
     One scaffold for a full-pane page — CordisPageView. Every surface that fills a
     pane with a heading and some sections reads these, so a new page cannot invent a
     fourth gutter (which is what three of them had done).

     The horizontal gutter is a clamp rather than a number because a page in a
     workbench lives in a PANE, not in the viewport: a media query would ask the wrong
     element how wide it is. A percentage resolves against the containing block, so
     the gutter narrows with the pane it is in and stops at 16px, which is where a
     40-character form field starts losing more than the gutter is worth.

     The two measures are reading widths, not layout widths. Prose is the ~72-character
     column an explanatory paragraph wants; Form is wider because a labelled field grid
     is scanned in columns rather than read in lines, and capping it at the prose
     measure forces two-column rows into one. A page with neither — a card grid, a
     table — asks for no cap at all and is not listed here.

     These are single values, not a profile pair: page-level air is the one metric
     Studio does NOT tighten. Shrinking a workbench's controls buys back rows in a list,
     which is the whole point of the profile; shrinking its page gutter buys back
     nothing, because the space it takes is space nothing else wanted. */
  --cordis-page-px:      32px;
  --cordis-page-pt:      28px;
  --cordis-page-pb:      40px;
  --cordis-page-gap:     22px;
  --cordis-page-measure-prose: 720px;
  --cordis-page-measure-form:  880px;
  --cordis-nav-item-h:   36px;
  /* D10 family merge #2. Both halves of the switch family were right about different things — the
     component's DOM (a <label> wrapping a hidden native input, so keyboard and screen-reader
     behaviour stay free) and the asset layer's SIZE — and the reason the component looked like an
     afterthought beside a 36px control is that it hardcoded 24x14, which is Studio's switch, and
     rendered it in Shell too. cordis-studio.css already overrode the plain half to exactly those
     numbers, so the profile scale existed and the component could not see it. Now both read these.

     Only three values are needed: the two halves compute their own thumb inset and travel from
     them, because one has a 1px border and the other does not. */
  --cordis-switch-w:     38px;
  --cordis-switch-h:     22px;
  --cordis-switch-thumb: 16px;

  /* The switch and slider palette (Cordis Server, Mantis #4025). Both switch spellings, the slider and
     the Server's HMI setting controls read these, so an off track, an unfilled rail and a knob are the
     same grey and the same knob wherever they are drawn. The Server's HMI had kept its own copy
     (a translucent grey track, a white knob) beside the design system's; the copy is gone.
       switch-off / -on    the track, off and on.
       switch-knob         the thumb. Light: the surface (white). Dark: a light ink, because the dark
                           surface on the dark off track measured 1.4:1 — a knob nobody could see.
       slider-h / -thumb   the bar's thickness and the thumb's diameter: the colour picker's bars
                           (12px, a 14px thumb), so a value slider and a hue bar are one control.
       slider-rail / -fill the unfilled bar and the filled span (origin to value).
       slider-thumb-ring   the white ring around the thumb, in both themes (the colour picker's). */
  --cordis-switch-off:   var(--cordis-border-strong);
  --cordis-switch-on:    var(--cordis-primary);
  --cordis-switch-knob:  var(--cordis-surface);
  --cordis-slider-h:     12px;
  --cordis-slider-thumb: 14px;
  --cordis-slider-rail:  var(--cordis-border-strong);
  --cordis-slider-fill:  var(--cordis-primary);
  --cordis-slider-thumb-ring: var(--cordis-n-0);
  --cordis-slider-thumb-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.30);

  /* Diagram canvas (--cdg-*). Promoted from compat/cordis-legacy.css in 2026-09 when the canvas look
     became shared: Studio's class and state-machine diagrams and the Cordis Server's HMI editor draw the
     same node chrome, selection ring and grid, so the values belong to the contract rather than to a
     shim that is meant to disappear. The prefix is kept, not renamed to --cordis-*: Studio's diagram
     sheets read these names, and a rename would be a second name for one value.
       node-bg / node-border  a box on the canvas; the dark block lifts both clear of the canvas.
       emph-box               the ONE emphasis for a selected / connect / nest target box.
       emph-glow-svg          the same emphasis as an SVG filter, for markers and selected lines.
       grid-size / grid-dot   the 20px dot lattice every editable canvas snaps to. */
  --cdg-node-bg: var(--cordis-surface);
  --cdg-node-border: var(--cordis-border);
  --cdg-emph-box: 0 0 0 2px var(--cordis-primary),
                  0 0 7px 1px color-mix(in srgb, var(--cordis-primary) 45%, transparent),
                  0 1px 3px rgba(0, 0, 0, 0.08);
  --cdg-emph-glow-svg: drop-shadow(0 0 4px color-mix(in srgb, var(--cordis-primary) 60%, transparent));
  --cdg-grid-size: 20px;
  --cdg-grid-dot: var(--cordis-border-strong);

  /* CordisCanvas host (#50). lattice / lattice-major are the colours the host paints its screen-space lattice in
     (a canvas element cannot read a stylesheet, so the painter reads these off the root); settle is how long the
     live layer fades when a gesture settles (css/components/canvas.css). */
  --cordis-canvas-lattice: color-mix(in srgb, var(--cdg-grid-dot) 70%, transparent);
  --cordis-canvas-lattice-major: var(--cdg-grid-dot);
  --cordis-canvas-settle: var(--cordis-dur);
}

/* Dark — neutral greys with a faint warm cast, and NO blue tint.
   The comment here always promised that, and until 2026-09 the values did not keep it: every rung
   had its blue channel 8-15 above its red (#1B1F26 / #242932 / #30404F), so the whole shell read
   navy next to a neutral dark app such as the Claude desktop client. The ladder now keeps red >= green
   >= blue by one to four steps, i.e. grey that leans a hair warm, and the BRAND is the only blue left
   on screen — which is what makes the accent read as an accent. The mean-channel steps between rungs
   are unchanged in size (DarkSurfaceLadderTests), so only the hue moved, not the depth.
   Body text is a warm off-white rather than #FFFFFF: pure white on a neutral grey glares, and
   --cordis-text-strong keeps #FFFFFF for the one level that should.
   Only the semantic layer changes. The brand stays blue. */
[data-theme="dark"] {
  --cordis-bg:             #1F1F1E;  /* editor / canvas */
  --cordis-surface:        #282826;  /* panels, cards */
  --cordis-surface-sunken: #171716;  /* activity rail, deepest surface */
  --cordis-surface-raised: #3A3936;  /* selected blocks, popovers */
  --cordis-surface-chrome: var(--cordis-surface);  /* workbench chrome: the panel surface */
  /* Lifted from #2B2F35 in 2026-08. At #2B2F35 a border measured 1.11:1 against the
     #282826 panel it divides, which is not a line anyone can see -- the same class of
     defect as the 1.4:1 focus ring fixed above, and found the same way: Cordis Studio
     holds five bordered panels at once and its row dividers, toolbar rules and splitter
     edges had disappeared on dark. Studio's own token block had raised this value on
     purpose before the extraction, with a comment saying so; that reasoning is not
     Studio-specific, so it belongs to the canonical palette rather than to a profile
     override. 1.36:1 -- still a hairline, deliberately, but a visible one.
     MIRRORED in css/compat/cordis-legacy.css; change both. */
  --cordis-border:         #3E3D3A;
  --cordis-border-strong:  #4A4945;
  --cordis-overlay:        rgba(0, 0, 0, 0.60);

  --cordis-text:        #EEEDEA;
  --cordis-text-strong: #FFFFFF;
  --cordis-text-muted:  #B4B2AC;
  --cordis-text-faint:  #9D9B95;  /* stays readable on raised surfaces */

  /* THE ACCENT SURFACE DOES NOT CHANGE HUE ON DARK. Blue-500 here as in light, so
     the status bar, a filled button, a selected row, a checked switch, the
     segmented toggle and the focus ring are all one colour in all four
     profile x theme combinations. It said blue-400 for a while, on the reasoning
     that "on dark the accent has to be lighter to reach AA" — which is true of
     accent TEXT and false of an accent SURFACE, and conflating the two is what
     put two blues on the same screen. The tell was visible in the showcase:
     --cordis-statusbar-bg is brand blue in both themes (D4) while --cordis-primary
     stepped to blue-400, so in dark the status bar and every other accent
     disagreed by one rung.

     Accent TEXT still steps, and that is the whole distinction: --cordis-primary-text
     is blue-300 here and blue-700 in light, because blue-on-a-ground being READ
     has to pass 4.5:1 and a filled surface does not. This is exactly what Cordis
     Studio's own app.css does — one --cordis-blue for every accent surface in
     both themes, with --cordis-selected-fg stepping per theme for the text on it.

     Hover and active step LIGHTER from blue-500 rather than darker, because the
     ground is dark: 500 → 400 → 300. */
  --cordis-primary:         var(--cordis-blue-500);
  --cordis-primary-hover:   var(--cordis-blue-400);
  --cordis-primary-active:  var(--cordis-blue-300);
  --cordis-accent:          var(--cordis-primary);
  --cordis-primary-soft:    #0A2E42;
  --cordis-primary-soft-bd: #135372;
  --cordis-primary-text:    var(--cordis-blue-300);
  --cordis-focus-color:     var(--cordis-primary);
  --cordis-focus-ring:      0 0 0 2px var(--cordis-focus-color);
  /* WHITE, the same as light — this token no longer steps between themes, and that
     is the point of it. It was #06222F here, and the argument for that was sound in
     isolation: on the brand blue, #06222F measures 5.32:1 against white's 3.09:1.
     What it missed is that the fill it lands on does NOT step. --cordis-primary is
     var(--cordis-blue-500) in both themes, so a stepped ink put two different inks on
     one identical fill, and every control carrying it changed appearance when the
     theme changed for a reason nothing on screen could show. CordisSegmentedToggle is
     where a reader meets that head-on: its thumb is the constant blue, so the active
     segment's glyph went from white to near-black on a ground that did not move.

     The contrast cost is real and is accepted deliberately, not overlooked: white on
     blue-500 is ~3.09:1, which clears the 3:1 floor for icons and other non-text UI
     and is BELOW the 4.5:1 text floor. That is not a new trade-off — it is the one
     the --cordis-primary comment in the light layer already records system-wide, and
     dark was the outlier that had quietly opted out of it. If the text floor is to be
     met on a filled accent, the fill has to darken (blue-700 puts white at 5.95:1);
     re-inking one theme only cannot do it without splitting the control in two. */
  --cordis-text-on-fill:    #FFFFFF;

  --cordis-nav-bg:      #171716;
  --cordis-nav-text:    #9D9B95;
  --cordis-nav-divider: rgba(255, 255, 255, 0.08);
  --cordis-nav-text-muted: #B4B2AC;
  /* Studio sets the status bar in the brand blue with dark text. */
  --cordis-statusbar-bg:   var(--cordis-blue-500);
  --cordis-statusbar-text: var(--cordis-blue-950);

  /* The inverse surface flips: a pale chip over a dark page. See the light layer
     for why this pair exists rather than being read off the text ramp. */
  --cordis-inverse-bg:   #ECEBE7;
  --cordis-inverse-text: #1A1A19;

  --cordis-canvas-brand: linear-gradient(215deg, #16232C 0%, #0B4F71 55%, #06222F 100%);
  --cordis-canvas-brand-text: var(--cordis-blue-300);

  --cordis-success:      #5BC454;
  --cordis-success-soft: #12290F;
  --cordis-success-text: #9BE096;
  --cordis-warning:      #D8941F;
  --cordis-warning-soft: #3A3630;
  --cordis-warning-text: #F0C983;
  --cordis-danger:       #F4565C;
  --cordis-danger-soft:  #2E1113;
  --cordis-danger-text:  #FF9EA3;
  --cordis-info:         var(--cordis-blue-400);
  --cordis-info-soft:    #0A2E42;
  --cordis-info-soft-bd: #135372;
  --cordis-info-text:    var(--cordis-blue-300);
  --cordis-neutral-soft: #2A2A28;
  --cordis-neutral-text: #A5A39D;

  /* The live ladder on dark: the same orange mixed down over the diagram's box surface, so the light
     text a dark box carries stays readable on every rung (the band at 42% keeps it near 5:1). A pale
     amber-200 band on a dark canvas would be the loudest thing on screen and would need dark ink, unlike
     every box beside it. The body tint stays faint enough that dimmed code keeps its contrast. */
  --cordis-live:         color-mix(in srgb, var(--cordis-amber-400) 10%, var(--cdg-node-bg));
  --cordis-live-band:    color-mix(in srgb, var(--cordis-amber-400) 42%, var(--cdg-node-bg));
  --cordis-live-bd:      var(--cordis-amber-400);
  --cordis-live-trail-1: color-mix(in srgb, var(--cordis-amber-400) 30%, var(--cdg-node-bg));
  --cordis-live-trail-2: color-mix(in srgb, var(--cordis-amber-400) 18%, var(--cdg-node-bg));
  --cordis-live-trail-3: color-mix(in srgb, var(--cordis-amber-400) 9%, var(--cdg-node-bg));

  /* THE CATEGORY INK STEPS ONE LIGHTER ON DARK, like every other hue (rule 4).
     It did not until 2026-08: the seven --cordis-cat-* inks were declared once, on :root, and this
     block did not touch them — so a .cordis-cat chip on dark drew a light-theme ink chosen for a
     WHITE ground on a #282826 panel. Measured against --cordis-surface, the light values land at
     blue 2.45  violet 1.66  amber 2.29  orange 2.34  green 2.21  grey 1.92  red 1.82 — all of them
     under the 3:1 floor for any legible mark, and cat-violet at 1.66:1 is not low contrast, it is an
     invisible chip. Reported by a consumer measuring all seven (2026-08, item 2).
     Only the INK moves. The -soft companions are rgba() washes of the bright hue, so they composite
     against whatever surface they land on and already adapted — that is why the fills looked right
     and only the text did not, which is exactly what made this survive a dark-theme review.
     Each value keeps its hue and saturation and raises lightness to clear 4.5:1 on the LIGHTEST dark
     surface (--cordis-surface-raised #3A3936, i.e. a chip inside a popover). On a panel that leaves
     6.2:1, and in the real chip case — ink on its own soft wash over a panel — 4.9 to 5.5:1.
     Not mirrored into css/compat/cordis-legacy.css's dark block, and deliberately: that mirror exists
     for the families Studio's class-based dark mechanism needs, and .cordis-cat--* is Shell-only
     furniture (css/cordis.css:474-480 are the only readers in the suite). Pinned by
     CategoryInkContrastTests. */
  --cordis-cat-blue:   #40B3EC;
  --cordis-cat-violet: #AD9FE6;
  --cordis-cat-amber:  #F19315;
  --cordis-cat-orange: #F98C49;
  --cordis-cat-green:  #38C23A;
  --cordis-cat-grey:   #9FAAB5;
  --cordis-cat-red:    #F28B95;
  --cordis-cat-teal:   #3CC7B8;
  --cordis-cat-pink:   #F28DC0;
  --cordis-cat-lime:   #9CCB2E;
  --cordis-cat-brown:  #D0A07A;

  --cordis-chart-1: var(--cordis-blue-300);
  --cordis-chart-2: var(--cordis-blue-400);
  --cordis-chart-3: var(--cordis-blue-500);
  --cordis-chart-4: var(--cordis-blue-700);
  --cordis-chart-5: #4A4945;
  --cordis-chart-track: var(--cordis-border);   /* #3E3D3A -- see the :root block */
  /* One step lighter on dark, like the category inks; grey steps DOWN, so "not running" stays the quiet band. */
  --cordis-chart-blue:   #40B3EC;
  --cordis-chart-violet: #AD9FE6;
  --cordis-chart-amber:  #F19315;
  --cordis-chart-green:  #38C23A;
  --cordis-chart-orange: #F98C49;
  --cordis-chart-red:    #F28B95;
  --cordis-chart-grey:   #6E7A85;
  --cordis-chart-teal:   #3CC7B8;
  --cordis-chart-pink:   #F28DC0;
  --cordis-chart-lime:   #9CCB2E;
  --cordis-chart-slate:  #8FA0B0;
  --cordis-chart-yellow: #FFD24D;

  --cordis-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
  --cordis-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
  --cordis-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);

  /* Diagrams sit on the darkest rung, where the surface tokens are too close in value to read as
     boxes: lift the node surfaces clear of the canvas (DarkSurfaceLadderTests). The grid dot is
     redeclared so it resolves against THIS block's border-strong wherever the theme attribute sits. */
  --cdg-node-bg: #363532;
  --cdg-node-border: #6B6A64;
  --cdg-grid-dot: var(--cordis-border-strong);
  --cordis-canvas-lattice: color-mix(in srgb, var(--cdg-grid-dot) 70%, transparent);
  --cordis-canvas-lattice-major: var(--cdg-grid-dot);

  /* The switch and slider palette (see the :root block), redeclared for the same reason as the grid dot;
     the knob steps to a light ink, since the dark surface disappears on the dark off track. */
  --cordis-switch-off:  var(--cordis-border-strong);
  --cordis-switch-on:   var(--cordis-primary);
  --cordis-switch-knob: var(--cordis-text);
  --cordis-slider-rail: var(--cordis-border-strong);
  --cordis-slider-fill: var(--cordis-primary);
}

/* ============================================================================
   Bootstrap bridge — paste this AFTER bootstrap.min.css in MachineControlServer.
   Replaces the Bootswatch Sandstone tokens with the Cordis tokens.
   ========================================================================= */
/* HOW FAR A BRIDGE OF VARIABLES CAN GET YOU, because this was reported as a gap and it is half a gap.
   It carried 17 names; Bootstrap 5.3 reads roughly fifty at the DOCUMENT level, and a page migrated
   about 80% of the way before a .form-control, a striped row or an .alert-success came back in the
   consuming application's own Bootswatch oatmeal (2026-08, feedback item 1). The rest of that surface
   is now here.
   Two things it still cannot reach, and knowing which is which saves an afternoon:
     * COMPONENT-SCOPED variables. Most of Bootstrap 5.3's variables are declared INSIDE a component's
       own rule (`.btn { --bs-btn-bg: … }`, `.alert { --bs-alert-bg: … }`), so a :root declaration of
       one does nothing at all. Reaching those means a block per component, and the variant classes
       below are the reason it is rarely needed: 5.3's `.alert-info` resolves its own
       `--bs-alert-bg` from `--bs-info-bg-subtle`, which IS a root name, so mapping the subtle and
       emphasis families fixes every alert, badge and list-group variant in one go.
     * LITERALS IN A THEME'S OWN RULES. No variable value can beat `.alert-info { background-color:
       #29abe0 }` — a rule at a specificity a token does not compete with. That needs rules, and they
       live in css/compat/bootswatch-sandstone.css, which is opt-in beside this file.
   The `-rgb` triples stay hand-written because CSS cannot decompose a hex into channels; each one
   duplicates a token and has to be edited with it. Keep the set as small as the components force. */
:root, [data-theme] {
  /* ---- theme colours ---- */
  --bs-primary:      var(--cordis-primary);
  --bs-primary-rgb:  31, 156, 216;
  --bs-secondary:    var(--cordis-n-500);
  --bs-secondary-rgb: 95, 112, 133;
  --bs-success:      var(--cordis-success);
  --bs-success-rgb:  46, 158, 58;
  --bs-danger:       var(--cordis-danger);
  --bs-danger-rgb:   205, 23, 32;
  --bs-warning:      var(--cordis-warning);
  --bs-warning-rgb:  201, 122, 14;
  --bs-info:         var(--cordis-info);
  --bs-info-rgb:     31, 156, 216;
  --bs-light:        var(--cordis-n-50);
  --bs-light-rgb:    241, 244, 248;
  --bs-dark:         var(--cordis-n-800);
  --bs-dark-rgb:     33, 43, 54;

  /* ---- the subtle / emphasis families (5.3) ----
     These are what every alert, badge, list-group and table variant resolves ITS own component
     variables from, so they are the highest-value half of this whole block. */
  --bs-primary-text-emphasis:   var(--cordis-primary-text);
  --bs-secondary-text-emphasis: var(--cordis-text-muted);
  --bs-success-text-emphasis:   var(--cordis-success-text);
  --bs-info-text-emphasis:      var(--cordis-info-text);
  --bs-warning-text-emphasis:   var(--cordis-warning-text);
  --bs-danger-text-emphasis:    var(--cordis-danger-text);
  --bs-light-text-emphasis:     var(--cordis-text);
  --bs-dark-text-emphasis:      var(--cordis-text-strong);

  --bs-primary-bg-subtle:   var(--cordis-primary-soft);
  --bs-secondary-bg-subtle: var(--cordis-neutral-soft);
  --bs-success-bg-subtle:   var(--cordis-success-soft);
  --bs-info-bg-subtle:      var(--cordis-info-soft);
  --bs-warning-bg-subtle:   var(--cordis-warning-soft);
  --bs-danger-bg-subtle:    var(--cordis-danger-soft);
  --bs-light-bg-subtle:     var(--cordis-surface-sunken);
  --bs-dark-bg-subtle:      var(--cordis-n-200);

  --bs-primary-border-subtle:   var(--cordis-primary-soft-bd);
  --bs-secondary-border-subtle: var(--cordis-border-strong);
  --bs-success-border-subtle:   var(--cordis-success-soft);
  --bs-info-border-subtle:      var(--cordis-info-soft-bd);
  --bs-warning-border-subtle:   var(--cordis-warning-soft);
  --bs-danger-border-subtle:    var(--cordis-danger-soft);
  --bs-light-border-subtle:     var(--cordis-border);
  --bs-dark-border-subtle:      var(--cordis-border-strong);

  /* ---- body, and the three text levels 5.3 exposes ---- */
  --bs-body-bg:          var(--cordis-bg);
  --bs-body-bg-rgb:      241, 244, 248;
  --bs-body-color:       var(--cordis-text);
  --bs-body-color-rgb:   33, 43, 54;
  --bs-emphasis-color:   var(--cordis-text-strong);
  --bs-secondary-color:  var(--cordis-text-muted);
  --bs-secondary-bg:     var(--cordis-surface-sunken);
  --bs-tertiary-color:   var(--cordis-text-faint);
  --bs-tertiary-bg:      var(--cordis-surface-sunken);
  --bs-heading-color:    var(--cordis-text-strong);

  /* ---- type ---- */
  --bs-body-font-family:  var(--cordis-font);
  --bs-body-font-size:    var(--cordis-fs-body);
  --bs-body-font-weight:  var(--cordis-fw-regular);
  --bs-body-line-height:  var(--cordis-lh-body);
  --bs-font-monospace:    var(--cordis-font-mono);
  --bs-code-color:        var(--cordis-primary-text);
  --bs-highlight-bg:      var(--cordis-warning-soft);

  /* ---- borders, radii, elevation ---- */
  --bs-border-color:             var(--cordis-border);
  --bs-border-color-translucent: var(--cordis-border);
  --bs-border-width:             1px;
  --bs-border-style:             solid;
  --bs-border-radius:      var(--cordis-radius-sm);
  --bs-border-radius-sm:   var(--cordis-radius-sm);
  --bs-border-radius-lg:   var(--cordis-radius-md);
  --bs-border-radius-xl:   var(--cordis-radius-lg);
  --bs-border-radius-xxl:  var(--cordis-radius-lg);
  --bs-border-radius-pill: var(--cordis-radius-pill);
  --bs-box-shadow-sm:      var(--cordis-shadow-sm);
  --bs-box-shadow:         var(--cordis-shadow-md);
  --bs-box-shadow-lg:      var(--cordis-shadow-lg);

  /* ---- links, focus, form validation ---- */
  --bs-link-color:           var(--cordis-primary-text);
  --bs-link-color-rgb:       15, 106, 151;
  --bs-link-hover-color:     var(--cordis-primary-active);
  --bs-focus-ring-color:     var(--cordis-focus-color);
  --bs-form-valid-color:            var(--cordis-success-text);
  --bs-form-valid-border-color:     var(--cordis-success);
  --bs-form-invalid-color:          var(--cordis-danger-text);
  --bs-form-invalid-border-color:   var(--cordis-danger);
}
/* The dark theme's channels for the triples that follow the THEME rather than a fixed hue. Everything else
   in the bridge is a var() and flips with the tokens on its own; a triple cannot, so without this block
   `.text-body`, `.bg-body` and `.link-*` (which read `rgba(var(--bs-…-rgb), …)`) kept the light values
   under data-theme="dark" — near-black text on the dark canvas. Reported by the Cordis Server (M4025), on
   a controller card's "Connected" label. Each value duplicates a dark token (named beside it) and has to be
   edited with it; BootstrapBridgeTests pins the two that the body text and background depend on. */
[data-theme="dark"] {
  --bs-body-bg-rgb:    31, 31, 30;     /* --cordis-bg          #1F1F1E */
  --bs-body-color-rgb: 238, 237, 234;  /* --cordis-text        #EEEDEA */
  --bs-link-color-rgb: 143, 211, 245;  /* --cordis-primary-text = --cordis-blue-300 #8FD3F5 */
}
/* Bootswatch sets buttons and tabs in capitals; Cordis does not. */
.btn, .nav-link, .dropdown-item, .page-link {
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--cordis-fw-semibold);
}
