/* =============================================================================
 * omx.css - GENERATED. Do not edit.
 *
 * Built by ci/build-assets.py from the co-located component sources. Edit the
 * file named in the section banner below, then rebuild:
 *
 *     python ci/build-assets.py --root Omnixa.Web.Shared
 *
 * Order is load-bearing and is defined by LAYERS in that script.
 * ========================================================================== */

/* --- Foundation/tokens.css ----------------------------------------- */
/* =============================================================================
 * L0 · Foundation · tokens.css
 *
 * THE ONLY FILE IN THIS PACKAGE THAT DEFINES A --omx-* CUSTOM PROPERTY.
 * A --omx-* declaration anywhere else fails `check-omx` C1, with one exception:
 * a component may declare a LOCAL variable scoped to its own block, derived from
 * a token here (see "Component-local variables" at the foot of this file).
 *
 * Derived with the imported `design-tokens` skill. Philosophy: **Dieter Rams /
 * functionalist** — restrained palette, colour as information, mathematical
 * spacing, borders over shadows, motion only where it carries meaning. That is
 * the honest match for a dense multi-tenant ERP; see docs/03_TOKEN_SPEC.md for
 * the three places the skill was deliberately not followed.
 * ========================================================================== */

/* -----------------------------------------------------------------------------
 * CASCADE LAYER ORDER — declared once, here, because tokens.css is the first
 * stylesheet every layout loads. Later wins.
 *
 *   omx.reset      normalize + box-sizing
 *   omx.base       these tokens, and bare-element defaults inside .omx-ui
 *   omx.layout     L2 — page, region, grid, split, stack, cluster
 *   omx.component  L1 + L3..L9 — every component's own CSS
 *   omx.utility    token-driven utilities; a utility must beat a component
 *   omx.module     EMPTY ON PURPOSE. The supported seat for a consuming module
 *                  that must override us: put the rule in this layer and it wins
 *                  by LAYER, not by out-specifying the design system.
 *
 * 🔴 Anything a module writes UNLAYERED still beats all of the above. That is
 *    intended — a module must be able to win — but it means "it works in my
 *    module" is not evidence the component is right.
 * -------------------------------------------------------------------------- */
@layer omx.reset, omx.base, omx.layout, omx.component, omx.utility, omx.module;

/* Self-hosted brand face. Used ONLY by the wordmark, never by UI text.
 * SIL OFL, latin subset. ⛔ No CDN: this package must render identically on an
 * air-gapped on-prem install. */
@font-face {
  font-family: "Omnixa Display";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/Poppins/poppins-600.woff2") format("woff2");
}
@font-face {
  font-family: "Omnixa Display";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/Poppins/poppins-700.woff2") format("woff2");
}

@layer omx.base {

  /* ===========================================================================
   * SCOPE — `.omx-ui`, never `:root`.
   * A host page that embeds our UI must not be repainted by us, and our UI must
   * not be repainted by a host that happens to define `--color-accent`. The
   * `--omx-` prefix and this scope are the same defence applied twice.
   * ======================================================================== */
  .omx-ui {
    color-scheme: light dark;

    /* -- BRAND ---------------------------------------------------------------
     * Identity, not theme. These five do NOT change between light and dark. */
    --omx-brand-blue:      #1e63e8;
    --omx-brand-cyan:      #29c3e0;
    --omx-brand-violet:    #8b5cf6;
    /* ⭐⭐ THE INSTRUMENT TONE. One value for every technical mark on the
     * brand plate: the mark's own ring, the node-graph datum, the divider and
     * the dashed baseline.
     * 🔴 FLAT, not light-dark(), and that is the point — it was chosen by
     * measuring BOTH grounds and picking the value with near-equal presence:
     * 2.89 on the light rail, 2.55 on the dark one. A light-dark pair would
     * be two values to keep in agreement for a mark that does not need it.
     * ⚠️ The two mark SVGs bake this same hex, because an asset cannot read
     * a token. This names the value they already use; change both together. */
    --omx-brand-instrument: #4a72be;
    --omx-brand-accent-a:  #4f83ff;
    --omx-brand-accent-b:  #a78bfa;

    /* -- TYPEFACE ------------------------------------------------------------
     * ⚠️ DEVIATION from the `frontend-design` skill, which says to avoid Inter
     * and system stacks and to load a distinctive face from a CDN. Rejected:
     * (a) on-prem installs have no outbound network, (b) a data grid at 13px
     * needs a face with real hinting and tabular figures, not personality,
     * (c) a webfont round-trip on every module's first paint is a cost the
     * design does not repay. Distinctiveness lives in the brand face and the
     * palette instead. */
    --omx-font-sans:    "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
    --omx-font-mono:    "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
    --omx-font-display: "Omnixa Display", var(--omx-font-sans);

    --omx-font-weight-regular:  400;
    --omx-font-weight-medium:   500;
    --omx-font-weight-semibold: 600;
    --omx-font-weight-bold:     700;

    /* -- TYPE RAMP -----------------------------------------------------------
     * Rams: "one size scale, used strictly." Seven steps, no in-between values.
     *
     * ✅ RULED 2026-09-13 — THE BODY SIZE IS 14px, AND THE FLOOR IS 12px.
     *
     * The question was open because 13px is what the FIRST RCL actually renders
     * 2–18 times per page, while its token named `body` claimed 16px and was
     * used almost nowhere. 13px was therefore the measured truth of the old
     * package — but "what the old one does" is not a design decision, and the
     * brief for this package was explicitly to be BETTER than the old one.
     *
     * Why 14px and not 13:
     *   · It is the modal size across comparable ERPs (SAP Fiori, Dynamics 365,
     *     Xero all sit at 14). 13 is the dense end; 14 is dense AND readable.
     *   · This is software people look at for eight hours. One pixel of body
     *     text is the cheapest readability the package will ever buy.
     *   · It let the whole ramp move OFF 11px. An 11px caption carries hints,
     *     placeholders, timestamps and validation text — the small print is
     *     exactly the text a person most needs to be able to read.
     *
     * What did NOT move: `display` (28) and `metric` (32). Page titles and KPI
     * figures were already right, so the top of the ramp is untouched and no
     * page header reflows.
     *
     * ⭐ AND THE DENSITY ANSWER IS NOT "PICK ONE". Anyone who wants the old
     * 13px density sets `data-text-scale="compact"` on the root; anyone who
     * needs more sets `"large"`. Every size below is multiplied by
     * `--omx-text-scale`, so the ramp keeps its proportions at every setting.
     *
     * 🔴 FIXED 2026-09-19 — that last sentence was FALSE for `display` and
     * `metric`, the only two sizes that were not multiplied. The comment said
     * they were, and nothing checked. At `data-text-scale="board"` (×2.4) the
     * caption reached 28.8px while the page title stayed at 28px, so the
     * display board rendered EVERY TITLE SMALLER THAN ITS OWN CAPTIONS — on the
     * one screen in the package designed to be read from across a room.
     * A single global choice was always going to be wrong for somebody.
     *
     * 🔴 iOS ZOOM: a form control below 16px makes mobile Safari zoom on focus.
     *    `--omx-font-size-control` is therefore 16px under 768px — see the
     *    breakpoint block below. ⛔ That override is ABSOLUTE and is NOT scaled:
     *    a "compact" preference must not reintroduce the zoom it prevents.
     *    Do not "simplify" it away. */
    --omx-text-scale: 1;

    --omx-font-size-display:  calc(1.75rem * var(--omx-text-scale)); /* 28px  page title   */
    --omx-font-size-metric:   calc(2rem     * var(--omx-text-scale)); /* 32px  KPI figure   */
    /* ⭐ OWNER RULING 2026-09-19 — "yes add a 22px step between 18 and 28".
     * 28 -> 18 was a 1.556 jump with nothing in it, the only gap above 1.15 in
     * the whole ramp, while the BOTTOM holds four steps inside 6px. Crowded
     * where it did not need to be, empty exactly where page headers live.
     * With 22: 28/22 = 1.273 and 22/18 = 1.222 — both comfortably perceptible.
     * ⛔ THE calc() IS NOT OPTIONAL. `display` and `metric` were added without it
     * and inverted the display board at data-text-scale="board" — the page title
     * rendered SMALLER than its own captions (item 1.8). A new size without the
     * multiplier is that same bug, and nothing catches it until someone opens a
     * board. */
    --omx-font-size-title-lg: calc(1.375rem * var(--omx-text-scale)); /* 22px  a heading with presence */
    --omx-font-size-title:    calc(1.125rem * var(--omx-text-scale)); /* 18px  section title   */
    --omx-font-size-lead:     calc(1rem     * var(--omx-text-scale)); /* 16px  intro           */
    --omx-font-size-body:     calc(0.875rem * var(--omx-text-scale)); /* 14px  THE body size   */
    --omx-font-size-label:    calc(0.8125rem * var(--omx-text-scale));/* 13px  labels, th      */
    --omx-font-size-caption:  calc(0.75rem  * var(--omx-text-scale)); /* 12px  captions, meta  */
    --omx-font-size-control:  var(--omx-font-size-body);

    --omx-line-height-tight:   1.2;
    --omx-line-height-body:    1.5;
    --omx-line-height-relaxed: 1.7;

    --omx-letter-spacing-tight:  -0.01em;
    --omx-letter-spacing-normal: 0;
    --omx-letter-spacing-wide:   0.06em;  /* all-caps labels only */

    --omx-measure: 68ch;                  /* max comfortable line length */
  
    /* -- SPACE ---------------------------------------------------------------
     * 4px base. The first RCL started at 8px with no 4px step, which is where
     * its stray 2/3/5/6px paddings came from — there was nothing legal to use. */
    --omx-space-0:  0;
    --omx-space-1:  2px;
    --omx-space-2:  4px;
    --omx-space-3:  6px;
    --omx-space-4:  8px;
    --omx-space-5:  12px;
    --omx-space-6:  16px;
    --omx-space-7:  20px;
    --omx-space-8:  24px;
    --omx-space-9:  32px;
    --omx-space-10: 40px;
    --omx-space-11: 48px;
    --omx-space-12: 64px;

    /* -- SIZE ----------------------------------------------------------------
     * Control heights are density-driven; see the [data-density] block. */
    --omx-size-icon-inline: 16px;   /* an icon sitting in a line of text */
    --omx-size-icon-action: 18px;   /* an icon acting as a button        */
    --omx-size-icon-feature: 24px;
    --omx-size-avatar-sm:   24px;
    --omx-size-avatar-md:   32px;
    --omx-size-touch:       44px;   /* WCAG 2.5.8 / iOS minimum target   */
    /* ⚠️ The OTHER floor. WCAG 2.5.8 AA is 24x24; 44 is the AAA/iOS
     * comfort size this package defaults to. 24 is for the few places
     * where 44 would push a whole region down a line - the rail's module
     * row is the first. ⛔ Not a licence to shrink targets generally. */
    --omx-size-touch-min:   24px;   /* WCAG 2.5.8 AA absolute minimum    */

    /* ⭐⭐ THE BRAND PLATE'S HEIGHT. It is NOT the topbar's height any
     * more, and that is a deliberate, costed decision rather than drift.
     * The rail head used to be exactly `--omx-topbar-height` so the chrome read
     * as one L. The mark is now the full dimensioned cut, which needs real size
     * to carry its ring, its crossing and its lattice ⚠️ measured on a
     * rendered ladder: at 28px it is a smudge, at 52 it reads.
     * ⛔ A 52px mark cannot live in a 52px row. So the head becomes its own
     * PLATE, framed with corner ticks, which is what stops the taller box
     * reading as a misalignment instead of a decision. */
    --omx-sidebar-plate-height: 80px;

    /* Shell chrome. 🔧 Moved here from Shell.css 2026-09-12: Topbar.css consumed
     * --omx-topbar-height while Shell.css declared it, so one component depended
     * on a variable another one owned. `check-omx` C1 named it. */
    --omx-topbar-height:    52px;
    --omx-sidebar-width:    248px;
    --omx-sidebar-rail:     56px;

    /* The brand navigation palette. Literals lived in Shell.css until C6 found
     * them; brand colour is identity and belongs with the other brand tokens. */
    --omx-brand-nav-1:      #132f59;
    --omx-brand-nav-2:      #10284c;
    --omx-brand-nav-3:      #0e2343;
    --omx-brand-nav-fg:     #edf4ff;
    --omx-brand-nav-fg-dim: #bfd0eb;
    --omx-brand-nav-rule:   rgb(162 181 215 / 30%);

    /* -- RADIUS --------------------------------------------------------------
     * Rams: sparing and consistent. Five steps; 3px and 5px are not values. */
    --omx-radius-xs:   2px;
    --omx-radius-sm:   4px;
    --omx-radius-md:   6px;
    --omx-radius-lg:   10px;
    --omx-radius-xl:   16px;
    --omx-radius-pill: 9999px;

    /* -- BORDER --------------------------------------------------------------
     * Rams prefers a rule to a shadow. These carry most of our separation. */
    --omx-border-width:        1px;
    --omx-border-width-strong: 2px;

    /* -- COLOUR · surfaces ---------------------------------------------------
     * Colour is information. Five neutral planes and nothing decorative.
     *
     * 🔴 CHANGED 2026-09-19 — canvas→surface MEASURED 1.047:1. Nothing in the
     * market goes below ~1.08 (Ant 1.122 · Carbon 1.100 · Atlassian 1.091 ·
     * Fluent 1.090 · Fiori 1.082), and under 1.05 a panel does not read as a
     * panel — it reads as a slightly different white, which is what made this
     * package look undecided rather than restrained. canvas is now 1.132 below
     * surface.
     *
     * ⭐ `surface` AND `surface-raised` ARE BOTH #ffffff, AND THAT IS THE POINT.
     * The market separates two near-white planes with a SHADOW, not a third
     * colour — there is not enough luminance left above a white panel to spend
     * on one. Raised is `--omx-shadow-md`; see Stage 1.5.
     *
     * ⚠️ HOW DARK THE RECESSED PLANES CAN GO IS FIXED BY THE TEXT TOKENS, not
     * by taste. `--omx-color-text-subtle` (#616e80, itself a 09-12 contrast
     * fix) must clear 4.5:1 on EVERY plane, and `--omx-color-border-control`
     * must clear 3.0:1. That puts the floor at #edf0f5 = 1.142 below white.
     * ⇒ THREE recessed planes each 1.08 apart need 1.166 and DO NOT FIT. So
     * `sunken` is not a third step below canvas; it is the recessed plane seen
     * from inside a white panel (1.144 below surface). canvas and sunken never
     * touch each other, so they do not need to separate from each other.
     * ⛔ Going darker means darkening `text-subtle` toward `text-muted`, which
     * would undo 09-12 and flatten a three-step text ramp into two.
     *
     * 🔴 `sunken` PASSES BY +0.033. MEASURED: `text-subtle` #616e80 on #ecf0f6 is
     * **4.5333** against a 4.5 floor, and `border-control` is 3.0602 against 3.0.
     * ⛔ DO NOT NUDGE THIS TOKEN DARKER "a shade" — one step down the hex and it
     * is a WCAG 1.4.3 failure on every caption, hint, placeholder and timestamp
     * sitting in a well. The spec that handed this over proposed #e6eaf2, which
     * measures 4.30 and fails. There is no room here; the CT check is the only
     * thing that will tell you. */
    --omx-color-canvas:          light-dark(#eef1f6, #0f1622);  /* behind everything             */
    --omx-color-surface:         light-dark(#ffffff, #161f2e);  /* a panel, a card               */
    --omx-color-surface-raised:  light-dark(#ffffff, #1d283a);  /* a thing above a panel         */
    --omx-color-surface-sunken:  light-dark(#ecf0f6, #111a26);  /* a well, an input track        */
    --omx-color-surface-overlay: light-dark(rgba(16, 25, 40, 0.44), rgba(4, 8, 14, 0.62));

    /* -- COLOUR · the chrome plane ⭐ THE TENANT-THEMEABLE SURFACE -------------
     * The sidebar and the topbar, as ONE plane. Added 2026-09-20 for per-tenant
     * theming (owner ruling: A+B split, one brand colour, per-tenant only).
     *
     * ⭐⭐ WHY ONE TOKEN AND NOT TWO. The R1 ruling put the rail and the bar on
     * the SAME surface deliberately -- "the chrome reads as a single L around a
     * tinted content canvas rather than as two rectangles that happen to touch"
     * (Sidebar.css). Two independent tenant colours would let that be broken on
     * the first afternoon. ⇒ one input, both surfaces follow, and the L
     * survives whatever colour is chosen.
     *
     * ⚠️ THESE DEFAULTS ARE TODAY'S VALUES EXACTLY -- `surface`, `text`,
     * `text-muted`, `border-subtle`. Introducing them is a REFACTOR with no
     * visual change, and the snapshot is what proves that.
     *
     * 🔴 WHY A GLOBAL AND NOT THE COMPONENT-LOCAL `--omx-sidebar-bg`. A runtime
     * tenant override lands on `.omx-ui`; `.omx-sidebar` REDECLARES
     * `--omx-sidebar-bg` in its own block, which is more specific AND later, so
     * the tenant value would never reach it. The local now resolves to this
     * global instead: the local stays private, and the global is the seam.
     *
     * ⛔ A tenant sets these four names and NOTHING ELSE touches the chrome.
     * That is what `check-omx` C25 enforces on the emitter's allow-list. */
    --omx-color-chrome:        var(--omx-color-surface);
    --omx-color-chrome-text:   var(--omx-color-text);
    --omx-color-chrome-muted:  var(--omx-color-text-muted);
    --omx-color-chrome-rule:   var(--omx-color-border-subtle);

    /* -- COLOUR · text ------------------------------------------------------- */
    --omx-color-text:          light-dark(#1b2434, #e3e9f2);
    /* 🔴 FIXED 2026-09-12 — `subtle` was #7a8798 and MEASURED 3.40:1 on canvas:
     * a WCAG 1.4.3 failure, and it is the colour every caption, hint and
     * placeholder in the system uses, so it failed in hundreds of places at
     * once. #616e80 measures 4.83 canvas / 5.05 surface / 4.65 sunken — it
     * clears on all three planes, not just the one it was tested against.
     * `muted` was darkened with it: at 4.89 it would otherwise have been
     * indistinguishable from the repaired `subtle`, and a three-step ramp whose
     * last two steps look identical is a two-step ramp with extra tokens. */
    --omx-color-text-muted:    light-dark(#505f74, #9aa8bd);
    --omx-color-text-subtle:   light-dark(#616e80, #8693a8);
    --omx-color-text-inverse:  light-dark(#eef3fb, #101827);
    --omx-color-text-link:     light-dark(#2059cf, #82aaff);

    /* -- COLOUR · borders ---------------------------------------------------- */
    --omx-color-border-subtle:  light-dark(#e2e9f2, #1f2a3b);
    --omx-color-border:         light-dark(#d7e0ec, #2a374b);
    --omx-color-border-strong:  light-dark(#adbbcd, #3d4c64);
    --omx-color-border-control: light-dark(#7c8aa0, #637591);   /* an input must read as an input */

    /* -- COLOUR · accent ----------------------------------------------------- */
    --omx-color-accent:            light-dark(#2660d8, #5b8cf0);
    --omx-color-accent-hover:      light-dark(#2059cf, #6f9bf5);
    --omx-color-accent-active:     light-dark(#1b4cb4, #86abf8);
    /* 🔴 CHANGED 2026-09-19 — SELECTED AND HOVER WERE ONE COLOUR. This was
     * #e5eeff against `--omx-color-control-hover` #e7eef9: ΔE2000 2.40 and a
     * luminance ratio of 1.001. A selected row and a row under the pointer were
     * the same pixel value to within rounding, so the table could not say which
     * row you had chosen — only which one your mouse was over.
     * ⭐ ΔE, NOT CONTRAST RATIO. Contrast ratio is a luminance relationship and
     * reads 1.001 as "identical" even when two colours differ in hue; ΔE is the
     * metric for two fills. #d8e5fc measures ΔE 5.01 and ratio 1.088 — over the
     * ~2.3 JND with margin, and over the 1.08 plane floor as well, so it
     * survives a cheap panel and a colour-blind reader both. */
    --omx-color-accent-subtle:     light-dark(#d8e5fc, #243a5c);
    --omx-color-accent-text:       light-dark(#f2f7ff, #0b1424);  /* text ON accent */
    --omx-color-accent-ring:       light-dark(rgba(47, 111, 235, 0.3), rgba(91, 140, 240, 0.38));

    /* -- COLOUR · the brand mark ⛔ CHROME ONLY ------------------------------
     * ⭐ OWNER RULING 2026-09-19 — "professional and exclusive not traditional".
     *
     * 🔴 THE PACKAGE SHIPPED FIVE BRAND COLOURS AND USED NONE OF THEM. Measured
     * before this change: `brand-violet`, `brand-accent-a` and `brand-accent-b`
     * appeared ZERO times outside their own declaration; `brand-blue` and
     * `brand-cyan` once each. Every accent a person actually saw was
     * `--omx-color-accent`, a safe blue — which is precisely the default-SaaS
     * look the ruling rejects. The brand was declared, not worn.
     *
     * ⭐⭐ AND THE SHIPPED ARTWORK ALREADY CARRIES THIS. `Foundation/Brand` says
     * the lockup has "the gradient x and violet tagline". So the logo has been
     * sweeping blue into violet the whole time and the CSS never echoed it once.
     * This token is not a new idea — it is the CSS finally agreeing with the
     * file beside it.
     *
     * ⛔⛔ WHY VIOLET AND NOT THE LITERAL BRAND SWEEP. The obvious move is
     * blue → cyan → violet, straight off the brand sheet. MEASURED against
     * WCAG 1.4.11 (3.0 for a non-text indicator), worst plane in each theme:
     *
     *     brand-blue    #1e63e8   light 4.58   dark 2.83  ✗ fails dark
     *     brand-cyan    #29c3e0   light 1.84   dark 7.04  ✗ fails light
     *     brand-accent-b #a78bfa  light 2.38   dark 5.45  ✗ fails light
     *     brand-violet  #8b5cf6   light 3.70   dark 3.50  ✅
     *     brand-accent-a #4f83ff  light 3.05   dark 4.25  ✅
     *
     * ⇒ The cyan is a LIGHT-MODE FAILURE at 1.84 and the blue a dark-mode one.
     * Only violet clears 3.0 in both themes unchanged, so the sweep runs
     * accent → violet, and the dark side steps up to accent-b (5.45 there,
     * where its light-mode 2.38 cannot reach it).
     *
     * ⛔ CHROME ONLY — the nav indicator, the identity mark, the determinate
     * progress fill. ⛔ NEVER status, never a chart series, never a table state,
     * never anything a person reads a VALUE from. Those stay functionalist: in
     * the data layer, a colour must mean something, and a colour that means
     * "we are Omnixa" means nothing about the row.
     * ⛔ And NOT the focus ring — `--omx-color-accent-ring` stays as it is.
     * The ring is an accessibility instrument before it is a brand surface, and
     * it is not the place to spend novelty. */
    /* 🔧 CORRECTED 2026-09-19 — THESE WERE LITERAL HEX AND THAT WAS A DEFECT.
     * `#8b5cf6` and `#a78bfa` are `--omx-brand-violet` and `--omx-brand-accent-b`
     * copied by value. ⇒ The brand ramp above and the chrome that is supposed to
     * BE the brand could drift apart silently: re-point `--omx-brand-violet` at a
     * new brand sheet and the nav indicator, the progress fill and the sign-in
     * mark would all keep the old colour, with nothing failing anywhere.
     * ⭐ One fact, one place. The ramp is the fact; this is a name for it. */
    --omx-color-brand-mark: light-dark(var(--omx-brand-violet), var(--omx-brand-accent-b));
    --omx-brand-sweep: linear-gradient(135deg,
                         var(--omx-color-accent) 0%,
                         var(--omx-color-brand-mark) 100%);

    /* -- COLOUR · control surfaces -------------------------------------------
     * 🔴 CHANGED 2026-09-19 — `control-active` was #dde7f6, ΔE2000 **2.35**
     * from `control-hover`. The just-noticeable difference is about 2.3, so the
     * pressed state sat at the threshold of being a different colour at all.
     * That mattered the moment `:active` went onto 30 more controls: a press
     * nobody can perceive is indistinguishable from a control that did not
     * register the press, which is what makes people click twice.
     * #ced8e7 measures ΔE 5.26 from hover.
     * ⭐ IT IS DELIBERATELY NEUTRAL, NOT BLUE. `accent-subtle` means SELECTED
     * and persists; a press is transient and means "your finger is down". Two
     * states that mean different things must not converge on one blue — the
     * same defect as 1.2, one layer down. Measured ΔE 4.11 apart.
     * ⚠️ `text-subtle` measures 3.60 on it, under the 4.5 the PLANES are held
     * to. Accepted, and already true of `control-hover`: a press lasts while a
     * button is held down. It is not a state anybody reads text in. */
    --omx-color-control:         light-dark(#f1f5fc, #1a2434);
    --omx-color-control-hover:   light-dark(#e7eef9, #223044);
    --omx-color-control-active:  light-dark(#ced8e7, #36414f);

    /* -- COLOUR · status -----------------------------------------------------
     * A FIVE-STEP RAMP per tone, because a pair was not enough and every
     * component invented its own fifth step: Toast.css used `-text` as a BORDER
     * colour, Banner.css declared a local `--omx-banner-fg`, Badge and Button
     * each did something else again. Four components, four answers, one missing
     * token.
     *
     *   bg      the wash a block of status text sits on
     *   subtle  the same wash one step stronger — hover on bg, or a nested fill
     *   border  the rule, bar or icon stroke. NON-TEXT, so the bar is 3.0:1
     *   solid   a filled chip or button in the tone
     *   on      the text that goes ON solid
     *   text    the text that goes ON bg or subtle
     *
     * ⛔ `-text` is a TEXT colour. Using it for a border was never wrong by
     * eye — it is dark and it showed up — but it is the step that has to clear
     * 4.5:1, so it drags every bar to a weight the design never asked for.
     * ⭐ MEASURED, both themes, worst plane: text/bg 4.99–8.46 · text/subtle
     * 4.60–6.96 · on/solid 4.63–6.32 · border ≥3.06 · solid ≥3.20. */
    --omx-color-info-bg:        light-dark(#e9f1ff, #14263f);
    --omx-color-info-subtle:    light-dark(#dbe8ff, #1b3557);
    --omx-color-info-border:    light-dark(#4a84e8, #4a7ac9);
    --omx-color-info-solid:     light-dark(#2660d8, #4d85e6);
    --omx-color-info-on:        light-dark(#ffffff, #08111f);
    --omx-color-info-text:      light-dark(#295fd4, #8fb6ff);
    --omx-color-success-bg:     light-dark(#e9f8f0, #12291f);
    --omx-color-success-subtle: light-dark(#cdeade, #17392a);
    --omx-color-success-border: light-dark(#2e9265, #2f8f63);
    --omx-color-success-solid:  light-dark(#1f7a52, #3aa876);
    --omx-color-success-on:     light-dark(#ffffff, #06140d);
    --omx-color-success-text:   light-dark(#1f6d4a, #6fd3a3);
    --omx-color-warning-bg:     light-dark(#fff7e9, #2e2314);
    --omx-color-warning-subtle: light-dark(#fdeed6, #3f301a);
    --omx-color-warning-border: light-dark(#b3760f, #a3782f);
    --omx-color-warning-solid:  light-dark(#9c5c17, #c08b38);
    --omx-color-warning-on:     light-dark(#ffffff, #1a1206);
    /* 🔴 FIXED 2026-09-12 — was #a7641d, inherited verbatim from the first RCL.
     * MEASURED at 4.40:1 on its own wash: a WCAG 1.4.3 AA failure for normal
     * text, and the ONLY status pair in the set that failed. #9c5c17 measures
     * 4.99:1, which puts it with the rest of the family (info 5.02 · danger
     * 5.09 · success 5.73) instead of alone below the line.
     * ⭐ It was never going to be found by eye — amber on cream looks fine and
     * is not. Only the number said otherwise. */
    --omx-color-warning-text:   light-dark(#9c5c17, #e3b273);
    --omx-color-danger-bg:      light-dark(#ffedf1, #2f1720);
    --omx-color-danger-subtle:  light-dark(#fddfe5, #43202c);
    --omx-color-danger-border:  light-dark(#d9505e, #b8515f);
    --omx-color-danger-text:    light-dark(#a84652, #f09aa6);
    --omx-color-danger-solid:   light-dark(#d12f3d, #d13f50);
    /* The text that goes ON danger-solid. Declared as a token because
     * Button.css used to hardcode #fff — a literal colour in a component
     * is exactly what the status-pair rule exists to prevent, and it is
     * what made the dark-mode danger button fail at 3.74:1. */
    --omx-color-danger-on:      #ffffff;
    --omx-color-neutral-bg:     light-dark(#eef2f7, #1c2533);
    --omx-color-neutral-subtle: light-dark(#dee5ee, #273243);
    --omx-color-neutral-border: light-dark(#7c8aa0, #6a7890);
    --omx-color-neutral-solid:  light-dark(#4a5568, #7b8a9f);
    --omx-color-neutral-on:     light-dark(#ffffff, #080d14);
    --omx-color-neutral-text:   light-dark(#4a5568, #a4b0c2);

    /* -- COLOUR · selection + marks ------------------------------------------ */
    --omx-color-selection-bg:   light-dark(rgba(47, 111, 235, 0.2), rgba(91, 140, 240, 0.28));
    --omx-color-selection-text: var(--omx-color-text);
    --omx-color-rating-on:      light-dark(#a86f12, #f5a623);   /* a filled star. 4.6:1 on surface. */
    --omx-color-rating-off:     light-dark(#c7d2e0, #3d4c64);

    /* -- CHART SERIES --------------------------------------------------------
     * Six categorical colours, ordered so the FIRST TWO are distinguishable by
     * everyone: blue and amber differ in hue and in lightness, which is what
     * survives deuteranopia and protanopia. Most ERP charts have two series.
     * ⚠️ Colour is never the only encoding — Chart also emits a data table, and
     * a line chart varies its dash pattern. */
    --omx-chart-1: light-dark(#2660d8, #7aa5f5);
    --omx-chart-2: light-dark(#b3730f, #e3b273);
    --omx-chart-3: light-dark(#1f6d4a, #6fd3a3);
    --omx-chart-4: light-dark(#a84652, #f09aa6);
    --omx-chart-5: light-dark(#6b46c1, #b4a0f0);
    --omx-chart-6: light-dark(#0e7490, #5bc0d8);
    --omx-chart-grid: light-dark(#e2e9f2, #2a374b);

    /* -- SHADOW --------------------------------------------------------------
     * Used only where something genuinely floats. A panel gets a border.
     *
     * ⭐ SINCE 1.1 THE SHADOW IS ALSO THE ONLY THING SEPARATING `surface` FROM
     * `surface-raised` — both are #ffffff — so this scale now carries plane
     * information, not decoration.
     *
     * 🔴 `xl` ADDED 2026-09-19. THE ELEVATION ORDER WAS INVERTED ABOVE THE
     * MODAL: drawer and modal were both `lg` — byte-identical, though a modal
     * opens ON TOP of a drawer — while popover, toast and tooltip, every one of
     * which can be painted OVER a modal, were `md`: a LIGHTER shadow than the
     * thing they cover.
     *
     * ⛔ THE Z SCALE DOES NOT GOVERN THESE. Modal, Drawer and Popover are
     * <dialog>/[popover] elements in the browser's TOP LAYER; their own files
     * say so and set no z-index. Top-layer order is invocation order, so the
     * invariant is not "shadow tracks z-index" — it is:
     *
     *   ⭐ AN OVERLAY'S SHADOW IS NEVER LIGHTER THAN THE SHADOW OF ANYTHING IT
     *      CAN BE PAINTED OVER.
     *
     * Five steps against more than five ranks means ties are unavoidable; the
     * rule is monotonicity, not a one-to-one mapping. ⇒ drawer `lg` (an
     * edge-anchored panel casts on one edge only), modal/popover/toast/tooltip
     * `xl`. */
    --omx-shadow-xs: 0 1px 2px light-dark(rgba(16, 25, 40, 0.06), rgba(0, 0, 0, 0.30));
    --omx-shadow-sm: 0 1px 3px light-dark(rgba(16, 25, 40, 0.08), rgba(0, 0, 0, 0.38)), 0 1px 2px light-dark(rgba(16, 25, 40, 0.04), rgba(0, 0, 0, 0.24));
    --omx-shadow-md: 0 4px 12px light-dark(rgba(16, 25, 40, 0.10), rgba(0, 0, 0, 0.45));
    --omx-shadow-lg: 0 12px 32px light-dark(rgba(16, 25, 40, 0.14), rgba(0, 0, 0, 0.55));
    --omx-shadow-xl: 0 16px 40px light-dark(rgba(16, 25, 40, 0.20), rgba(0, 0, 0, 0.66));
    --omx-shadow-focus: 0 0 0 3px var(--omx-color-accent-ring);

    /* A hairline that works on ANY background — it is the surface's own colour
     * at low alpha, not a colour of its own, which is why it is black in light
     * and white in dark rather than a pair of picked greys. Stops a pale avatar
     * or chip dissolving into a pale surface. */
    --omx-color-hairline: light-dark(rgb(0 0 0 / 8%), rgb(255 255 255 / 8%));

    /* -- Z ------------------------------------------------------------------
     * Twelve steps, each named for what sits there. A raw z-index in a
     * component is C5. */
    /* 🔧 Added 2026-09-12 by L9 Print. The scale had a floor of 0 and no way to
     * say "behind the content" — a watermark, a decorative wash. The first use
     * wrote a raw -1 and C5 caught it. A scale with twelve rungs upward and
     * none downward is a scale with a hole, not a rule. */
    --omx-z-behind:     -1;
    --omx-z-base:        0;
    --omx-z-raised:      10;
    --omx-z-sticky:      100;
    --omx-z-topbar:      200;
    --omx-z-sidebar:     300;
    --omx-z-dropdown:    400;
    --omx-z-scrim:       500;
    --omx-z-drawer:      600;
    --omx-z-modal:       700;
    --omx-z-popover:     800;
    --omx-z-toast:       900;
    --omx-z-tooltip:     1000;

    /* -- MOTION --------------------------------------------------------------
     * Rams: purposeful transitions only. Nothing here is decorative.
     * ⛔ `transition: all` is forbidden — name the properties. */
    --omx-duration-instant: 60ms;
    --omx-duration-fast:    140ms;
    --omx-duration-normal:  220ms;
    --omx-duration-slow:    320ms;

    --omx-easing-standard: cubic-bezier(0.2, 0, 0, 1);
    --omx-easing-in:       cubic-bezier(0.4, 0, 1, 1);
    --omx-easing-out:      cubic-bezier(0, 0, 0.2, 1);

    /* -- BREAKPOINTS ---------------------------------------------------------
     * FOUR widths. Only these four.
     *
     *   640px  phone      768px  tablet
     *   960px  content grids collapse to one column
     *  1024px  the shell sidebar changes
     *
     * 🔴 THESE CANNOT BE USED IN A MEDIA QUERY, and that is a CSS limitation,
     *    not an oversight: a media condition is evaluated before custom
     *    properties resolve, so `@media (max-width: var(--omx-bp-md))` silently
     *    never matches. `@custom-media` is a Level-5 draft nobody ships and this
     *    package has no build step to polyfill it. The tokens exist for JS and
     *    for CONTAINER queries, which can read them. Media queries must repeat
     *    the literal — `check-omx` C4 enforces that only these four appear.
     *
     * ⭐ Prefer a CONTAINER query. A component does not care how wide the
     *    window is; it cares how wide its slot is. Media queries are for the
     *    shell. */
    --omx-bp-sm: 640px;
    --omx-bp-md: 768px;
    --omx-bp-lg: 960px;
    --omx-bp-xl: 1024px;
  }

  /* ===========================================================================
   * DARK
   * Not an inversion. The dark canvas is a cool slate to match the palette's
   * blue accent; text is off-white, never #fff; shadows go deeper and more
   * transparent rather than keeping their light values; the accent is lifted so
   * it still clears 4.5:1 on a dark surface.
   *
   * 🔴 IT USED TO BE DEFINED THREE TIMES. One light block, then the whole dark
   * palette written out TWICE — once for `[data-theme="dark"]` and once, byte
   * for byte, inside `@media (prefers-color-scheme: dark)` for the system state.
   * 71 tokens, 142 duplicated lines, and nothing but discipline keeping the two
   * copies equal. (They were still equal when this was collapsed — measured, 0
   * drift — which is luck, not a guarantee.)
   *
   * ⭐ NOW EACH TOKEN IS DECLARED ONCE, as `light-dark(light, dark)`, and the
   * THEME IS SELECTED BY `color-scheme` ALONE:
   *
   *     .omx-ui                      color-scheme: light dark   -> follows the OS
   *     .omx-ui[data-theme="dark"]   color-scheme: dark         -> explicit
   *     .omx-ui[data-theme="light"]  color-scheme: light        -> explicit
   *
   * ⭐⭐ THAT IS WHY THIS IS WORTH MORE THAN THE LINE COUNT. The three-state
   * rule used to be hand-written per component as
   * `.omx-ui:not([data-theme="light"])` INSIDE a media query — and on 2026-09-19
   * five of the seven files that tried it got it wrong, shipping a dark avatar
   * and a 2.41:1 select chevron onto light pages. `color-scheme` makes the
   * system state structural: there is no third branch left to forget.
   * ⇒ see `check-omx` C19, which guards the components that still do it by hand.
   *
   * ⚠️ `light-dark()` NEEDS `color-scheme` TO BE SET. It is set on `.omx-ui`
   * above; a token used outside that scope resolves to its LIGHT value.
   *
   * 🔴🔴 ⛔ ON A BROWSER WITHOUT `light-dark()` THIS DOES NOT DEGRADE TO LIGHT —
   * IT DEGRADES TO NOTHING. A custom property is not validated at parse time, so
   * the unknown function is stored happily; the failure happens at SUBSTITUTION,
   * which makes the consuming declaration invalid at computed-value time and
   * resets it to `initial`. MEASURED — background on an unsupporting engine:
   *
   *   background: var(--token)                       -> rgba(0,0,0,0)
   *   background: #eef1f6; background: var(--token)  -> rgba(0,0,0,0)  ⛔
   *   background: var(--token, #eef1f6)              -> rgba(0,0,0,0)  ⛔
   *   @supports (color: light-dark(#fff,#000)) { … }  -> WORKS
   *
   * ⛔⛔ THE TWO OBVIOUS FALLBACKS BOTH DO NOTHING. The earlier safe declaration
   * is discarded with the invalid one, and the `var()` fallback never fires
   * because the property IS set — to garbage. ⭐ Only `@supports` gates it.
   *
   * ✅ RULED AND CLOSED 2026-09-20 — option B. `ci/build-assets.py` GENERATES an
   * `@supports not (color: light-dark(#fff, #000))` block from these very
   * declarations and appends it to `wwwroot/css/omx.css`, last in `omx.base`.
   * A browser WITH `light-dark()` skips the block and keeps both themes; a
   * browser without it gets the LIGHT palette — a readable product instead of
   * transparent backgrounds and black text.
   *
   * ⭐⭐ IT IS GENERATED, NOT WRITTEN, AND THAT IS THE WHOLE POINT. The fallback
   * was rejected once because it meant writing the light palette out a second
   * time — the duplication this section removed. That objection dies when the
   * second copy is DERIVED from the first: one place to author, and
   * `build-assets.py --check` fails the build the moment the two disagree.
   * ⚠️ 69 declarations, not the 64 a whole-value regex finds: `light-dark()` is
   * embedded inside the shadow values, twice in `--omx-shadow-sm`. The
   * generator matches parens rather than trusting a pattern. */
   * ⚠️ Shadows merge per COLOUR, not per value — the geometry is identical in
   * both themes, so only the rgba() inside each layer flips.
   * ======================================================================== */
  /* -- TEXT SCALE ------------------------------------------------------------
   * The type counterpart to `data-density`, and deliberately separate from it:
   * density is GEOMETRY (row height, cell padding) and this is TYPE. Changing
   * one should not change the other — a person who wants tight rows does not
   * necessarily want small text, and the old RCL conflated them.
   *
   * ⚠️ Applies to the element AND its descendants, for the same reason density
   * does: matching the root only meant nesting a second `.omx-ui` was the only
   * way to scope it, and that region then silently fell back to the system theme. */

  .omx-ui[data-text-scale="compact"],
  .omx-ui[data-text-scale="compact"] .omx-ui {
    /* 14 -> 13px body, 12 -> 11px caption. The old package's density, for a
     * register-heavy tenant that asks for it. */
    --omx-text-scale: 0.93;
  }

  /* ⭐ THE WALL. A display board is read from across a factory floor, not from
   * arm's length, so it needs a scale no seated user would ever choose — which
   * is exactly why it lives here beside the other two rather than being set on
   * the page. Same multiplier, same ramp, same one place to change it. */
  .omx-ui[data-text-scale="board"],
  .omx-ui[data-text-scale="board"] .omx-ui {
    --omx-text-scale: 2.4;
  }

  .omx-ui[data-text-scale="large"],
  .omx-ui[data-text-scale="large"] .omx-ui {
    /* 14 -> 16px body. Not an accessibility substitute — browser zoom and the
     * rem base already do that — but a comfortable default for a shared screen. */
    --omx-text-scale: 1.14;
  }


  /* -- THE THEME SWITCH, ENTIRE ------------------------------------------------
   * Two declarations. Everything else is `light-dark()` in the block above. */
  .omx-ui[data-theme="dark"]  { color-scheme: dark; }
  .omx-ui[data-theme="light"] { color-scheme: light; }

  /* ===========================================================================
   * DENSITY — an attribute, never a stylesheet.
   * One build serves all three. Only sizes change; colour and type ramp do not.
   *
   *   comfortable  the default. Reading and light entry.
   *   cozy         a full working day in a grid.
   *   compact      a data-entry operator who wants rows on screen.
   * ======================================================================== */
  /* 🔧 REVISED 2026-09-12 — these were `.omx-ui[data-density=…]` only, which
   * made density a property of the ROOT and nothing else. Two things broke:
   *   · a dense grid inside a comfortable page was not expressible at all
   *   · the only workaround was nesting a second `.omx-ui`, which re-declares
   *     the light palette and then loses it to the system-dark media query —
   *     a nested region silently flipped to dark inside a light page.
   * Matching descendants too makes density a REGION property, which is what a
   * data-entry screen actually needs. `.omx-ui` stays the single THEME root. */
  .omx-ui,
  .omx-ui[data-density="comfortable"],
  .omx-ui [data-density="comfortable"] {
    --omx-control-height:  32px;
    --omx-control-pad-x:   var(--omx-space-5);
    --omx-row-height:      36px;
    --omx-cell-pad-y:      var(--omx-space-4);
    --omx-cell-pad-x:      var(--omx-space-5);
    --omx-panel-pad:       var(--omx-space-6);
    --omx-stack-gap:       var(--omx-space-6);
    --omx-field-gap:       var(--omx-space-5);
  }
  .omx-ui[data-density="cozy"],
  .omx-ui [data-density="cozy"] {
    --omx-control-height:  28px;
    --omx-control-pad-x:   var(--omx-space-4);
    --omx-row-height:      30px;
    --omx-cell-pad-y:      var(--omx-space-3);
    --omx-cell-pad-x:      var(--omx-space-4);
    --omx-panel-pad:       var(--omx-space-5);
    --omx-stack-gap:       var(--omx-space-5);
    --omx-field-gap:       var(--omx-space-4);
  }
  .omx-ui[data-density="compact"],
  .omx-ui [data-density="compact"] {
    --omx-control-height:  24px;
    --omx-control-pad-x:   var(--omx-space-3);
    --omx-row-height:      26px;
    --omx-cell-pad-y:      var(--omx-space-1);
    --omx-cell-pad-x:      var(--omx-space-3);
    --omx-panel-pad:       var(--omx-space-4);
    --omx-stack-gap:       var(--omx-space-4);
    --omx-field-gap:       var(--omx-space-3);
  }

  /* ===========================================================================
   * SMALL VIEWPORTS
   * Two things change, and only these two.
   *   1. Controls grow to 16px type and a 44px target — below 16px mobile Safari
   *      zooms the page on input focus, and 44px is the WCAG 2.5.8 comfortable
   *      target (its hard minimum is 24).
   *   2. The OPERABLE sizes stop following the stored density: a 26px row is
   *      not operable with a thumb. ⚠️ Comfort sizes — `control-pad-x`,
   *      `panel-pad`, `stack-gap`, `field-gap` — deliberately keep following it.
   *
   * 🔴🔴 THE SELECTOR LIST IS DUPLICATED FROM THE DENSITY BLOCK ABOVE, AND THAT
   * IS THE WHOLE FIX. This rule used to be `.omx-ui` alone, and every
   * declaration in it was inert whenever a density was stored:
   * `.omx-ui[data-density="cozy"]` is specificity (0,2,0), `.omx-ui` is (0,1,0),
   * and A MEDIA QUERY ADDS NO SPECIFICITY. The stored preference simply won.
   *
   * MEASURED 2026-09-20 at 375px, with this media query matching and
   * `<body class="omx-ui" data-density="cozy">`: `--omx-control-height` resolved
   * to **28px** and `--omx-row-height` to **30px** against
   * `--omx-size-touch: 44px`. Twenty-two buttons and six menu items all
   * rendered 28–29px. Every control in the package, on every phone, whenever
   * density was cozy or compact.
   *
   * ⭐⭐ AND IT READ AS WORKING, WHICH IS WHY IT SURVIVED. The one declaration
   * here that NOTHING else competes for — `--omx-font-size-control` — applied
   * perfectly, so the half that stops iOS zooming on focus worked while the
   * half that sizes the target did not. One rule, five declarations, one winner.
   *
   * ⛔ A NEW DENSITY VALUE MUST BE ADDED HERE TOO. Equal specificity plus later
   * source order is what makes this win, so the two lists have to stay the same
   * list. There is no way to say "whatever the density block matched" in CSS.
   * ======================================================================== */
  @media (max-width: 768px) {
    .omx-ui,
    .omx-ui[data-density="comfortable"],
    .omx-ui [data-density="comfortable"],
    .omx-ui[data-density="cozy"],
    .omx-ui [data-density="cozy"],
    .omx-ui[data-density="compact"],
    .omx-ui [data-density="compact"] {
      --omx-font-size-control: 1rem;   /* 16px — stops the iOS zoom */
      --omx-control-height:    var(--omx-size-touch);
      --omx-row-height:        var(--omx-size-touch);
      --omx-cell-pad-y:        var(--omx-space-5);
      --omx-cell-pad-x:        var(--omx-space-5);
    }
  }

  /* ===========================================================================
   * REDUCED MOTION — collapse the durations at the token level so that every
   * component honours it without writing its own media query.
   * ======================================================================== */
  @media (prefers-reduced-motion: reduce) {
    .omx-ui {
      --omx-duration-instant: 0ms;
      --omx-duration-fast:    0ms;
      --omx-duration-normal:  0ms;
      --omx-duration-slow:    0ms;
    }
  }
}

/* =============================================================================
 * Component-local variables — the one legal exception to "tokens live here"
 *
 * A component may declare variables scoped to its own block when it needs a
 * derived value in several rules. They must be named for the component and must
 * resolve to a token:
 *
 *     .omx-sidebar {
 *       --omx-sidebar-width: 248px;
 *       --omx-sidebar-bg: var(--omx-color-surface-sunken);
 *     }
 *
 * What that buys: the first RCL promoted nineteen `--color-sidebar-*` entries
 * into the global token set, so every consumer inherited a sidebar's private
 * business. Keep private things private.
 * ========================================================================== */

/* --- Foundation/reset.css ------------------------------------------ */
/* =============================================================================
 * L0 · Foundation · reset.css
 *
 * Scoped to `.omx-ui`. This package never restyles a host page's bare elements.
 * That is the difference between a design system and a stylesheet that fights
 * whatever it is dropped into.
 * ========================================================================== */

@layer omx.reset {

  /* \U0001f534\U0001f534 [hidden] ON ITS OWN DOES NOT HIDE ANYTHING HERE.
   *
   * The UA stylesheet gives `[hidden]` `display: none`, and that rule loses to
   * ANY author `display:` declaration — including `.omx-button { display: flex }`.
   *
   * ⚠️ MEASURED, and it had already shipped: `Board.js` sets `.hidden` on its
   * four "Move" buttons once the script is running, and all four rendered
   * `hidden = true`, `display: flex`, 32px tall and fully visible. Its own
   * comment says "with JS, changing the select IS the action, so a second click
   * would be a step for nothing" — an intent that had never once taken effect.
   *
   * ⛔ !important is deliberate and is the only one in this file. `hidden` is
   * not a style, it is a statement that the element is not part of the page;
   * a component that needs an exception has the wrong element. */
  [hidden] {
    display: none !important;
  }

  .omx-ui,
  .omx-ui *,
  .omx-ui *::before,
  .omx-ui *::after {
    box-sizing: border-box;
  }

  .omx-ui {
    margin: 0;
    font-family: var(--omx-font-sans);
    font-size: var(--omx-font-size-body);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text);
    background-color: var(--omx-color-canvas);
    -webkit-font-smoothing: antialiased;
    text-size-adjust: 100%;

    /* Stops the double-tap zoom delay on every interactive element at once. */
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
  }

  /* Headings carry no margin of their own — spacing is the layout's job, so a
   * heading in a panel and a heading in a table header behave the same. */
  .omx-ui :is(h1, h2, h3, h4, h5, h6) {
    margin: 0;
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    letter-spacing: var(--omx-letter-spacing-tight);
    text-wrap: balance;          /* no single-word last lines in a title */
  }

  /* 🔴 THIS PACKAGE HAD NO HEADING HIERARCHY AT ALL. The rule above said
   * `font-size: inherit`, so an <h1> and an <h6> rendered at exactly the same
   * size as the paragraph beside them, differing only in weight. Every gate
   * passed: the markup was semantic, the outline was correct, screen readers
   * announced the levels. It was only ever wrong for people reading the screen.
   *
   * ⭐ THIS STAYS IN `omx.reset`, THE WEAKEST LAYER, ON PURPOSE. `inherit` was
   * not arbitrary — it let a component put <h3> inside a card and control the
   * size itself. Any component that sets a font-size lives in `omx.component`
   * and still wins, so those are untouched; what changes is a heading NOBODY
   * sized, which previously fell back to body text.
   *
   * ⚠️ h1 → h2 IS A 28px → 18px CLIFF, and it is the ramp's, not this mapping's.
   * The ramp is 32/28/18/16/14/13/12: crowded at the bottom, and empty between
   * 18 and 28. Mapping onto it as it stands is what makes that gap visible
   * instead of theoretical. ⛔ Owner decision — not pre-empted here. */
  /* ⚠️ THE WHOLE MAP SHIFTED WITH THE 22px STEP, NOT JUST h2 — and it had to.
   * Remapping h2 alone (18 -> 22) left h3 on `lead` (16), so the 28->18 cliff the
   * ruling removed reappeared one level down as 22->16 = 1.375, WORSE than the
   * 18->16 it replaced. It would also have orphaned `title` (18): a named step in
   * the ramp that no heading used.
   * ⭐ Every step now carries a level, and no gap exceeds 1.27:
   *     h1 28  h2 22  h3 18  h4 16  h5 14  h6 13
   *     1.273   1.222   1.125   1.143   1.077
   * ⛔ `caption` (12) is deliberately NOT a heading level — it is for captions,
   * meta and footnotes, and an h6 that small stops reading as a heading at all. */
  /* ⭐ THE BRAND FACE ON h1 ONLY — and it was `:is(h1, h2)` for about an hour.
   *
   * 🔴 A TAG IS NOT A SIZE, AND THIS FILE SAYS SO TWELVE LINES ABOVE. The map
   * below is a DEFAULT that components override freely — deliberately, so a
   * card can put an <h3> in it and set its own size. So "h2" does not mean
   * "the 22px step"; it means "second-level structure, at whatever size the
   * component chose". MEASURED at render across 20 routes with the h2 rule in:
   *
   *     h2.omx-panel__title           18px            12 per page, wearing it
   *     h2.omx-appearance__caption    18px             1 per page, wearing it
   *     h2.omx-sidebar__section-label 12px UPPERCASE   3 per page, wearing it
   *
   * ⇒ The rule put a display face on a 12px uppercase nav label. The ruling
   * gave the face to the TOP OF THE RAMP; that is a size claim, and binding it
   * to a tag handed it to three components that had opted out by resizing.
   *
   * ⭐ h1 SURVIVES BECAUSE IT WAS MEASURED, not because it is a heading. Every
   * h1 this package renders is 28px or 22px — `statepage__title` 28 (7),
   * `print__title` 28 (2), `auth__app` 22 (1), across all 20 routes. Nothing
   * overrides h1 downward, so on h1 the tag and the step still agree.
   * ⛔ Everything below h1 reaches the face through `.omx-text-display`, by
   * asking for the step by name. `check-omx` C22 holds the allow-list. */
  .omx-ui h1 { font-family: var(--omx-font-display); }
  .omx-ui h1 { font-size: var(--omx-font-size-display);  }
  .omx-ui h2 { font-size: var(--omx-font-size-title-lg); }
  .omx-ui h3 { font-size: var(--omx-font-size-title);    }
  .omx-ui h4 { font-size: var(--omx-font-size-lead);     }
  .omx-ui h5 { font-size: var(--omx-font-size-body);     }
  .omx-ui h6 { font-size: var(--omx-font-size-label);    }

  .omx-ui :is(p, figure, blockquote, dl, dd, ul, ol) { margin: 0; }
  .omx-ui :is(ul, ol) { padding: 0; list-style: none; }

  /* An anchor with an href is a link. An anchor without one is not, and must not
   * look clickable. */
  .omx-ui a { color: var(--omx-color-text-link); text-decoration: none; }
  .omx-ui a[href]:hover { text-decoration: underline; }
  .omx-ui a[href]:active { color: var(--omx-color-accent-active); }
  .omx-ui a:not([href]) { color: inherit; cursor: default; }

  /* Form controls inherit type by default; the platform default is a different
   * face at a different size, which is where most "why is this font wrong"
   * bugs come from. */
  .omx-ui :is(button, input, select, textarea, optgroup) {
    font: inherit;
    font-size: var(--omx-font-size-control);
    color: inherit;
    margin: 0;
  }
  .omx-ui button { background: none; border: 0; padding: 0; cursor: pointer; }
  .omx-ui :is(button, [role="button"]):disabled { cursor: not-allowed; }
  .omx-ui textarea { resize: vertical; }

  /* Images and media never overflow their column, and never shift layout. */
  .omx-ui :is(img, svg, video, canvas) { display: block; max-width: 100%; }
  .omx-ui img { height: auto; }

  /* Tables are data, not layout. */
  .omx-ui table { border-collapse: collapse; border-spacing: 0; }
  .omx-ui :is(th, td) { text-align: start; padding: 0; font-weight: inherit; }

  /* Numbers line up in columns or they are unreadable. */
  .omx-ui :is([data-omx-numeric], .omx-numeric) {
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  .omx-ui ::selection {
    background: var(--omx-color-selection-bg);
    color: var(--omx-color-selection-text);
  }

  .omx-ui hr {
    border: 0;
    border-top: var(--omx-border-width) solid var(--omx-color-border-subtle);
    margin: 0;
  }

  .omx-ui code, .omx-ui kbd, .omx-ui samp, .omx-ui pre {
    font-family: var(--omx-font-mono);
    font-size: 0.95em;
  }

  /* A heading linked to from a hash must not land under a sticky topbar. */
  .omx-ui :is(h1, h2, h3, h4, h5, h6)[id] {
    scroll-margin-top: var(--omx-space-11);
  }

  /* Native selects in Windows dark mode inherit the OS colours and become
   * unreadable unless both are set explicitly. */
  .omx-ui select {
    background-color: var(--omx-color-surface);
    color: var(--omx-color-text);
  }
}

/* --- Foundation/a11y.css ------------------------------------------- */
/* =============================================================================
 * L0 · Foundation · a11y.css
 *
 * The accessibility primitives every layer above inherits. Three of the rules
 * here exist because the first RCL failed them in a way no gate could see:
 *
 *   1. `.tags input { border:0; outline:none }` with no focus rule anywhere —
 *      keyboard focus was invisible on that control. (components.css:564)
 *   2. `.range input[type=range] { outline:none }`, no focus style on track or
 *      thumb. (components.css:538)
 *   3. `.drawer-body { overflow-y:auto }` with no overscroll containment, so
 *      reaching the end of a drawer scrolled the page behind it.
 *      (components.css:396)
 *
 * All three are handled below as defaults, so a component has to opt OUT to be
 * broken rather than opt IN to be correct.
 * ========================================================================== */

@layer omx.base {

  /* ===========================================================================
   * FOCUS
   * One ring, everywhere, on :focus-visible only — so a mouse click does not
   * leave a ring behind but a Tab key always does.
   *
   * ⛔ `outline: none` is forbidden in this package without a replacement in the
   *    SAME rule. `check-omx` C8 fails the build otherwise.
   * ======================================================================== */
  .omx-ui :focus-visible {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
    border-radius: var(--omx-radius-xs);
  }

  /* Controls that draw their own border replace the outline with a ring, so the
   * focus indicator follows the control's shape instead of boxing it. */
  .omx-ui :is(input, select, textarea, [data-omx-control]):focus-visible {
    outline: none;
    border-color: var(--omx-color-accent);
    box-shadow: var(--omx-shadow-focus);
  }

  /* A compound control (a field with an addon, a tag input, a stepper) shows the
   * ring on the wrapper, because the thing the user perceives as focused is the
   * whole control, not the bare <input> inside it. */
  .omx-ui [data-omx-focus-group]:focus-within {
    border-color: var(--omx-color-accent);
    box-shadow: var(--omx-shadow-focus);
  }
  .omx-ui [data-omx-focus-group]:focus-within :is(input, textarea):focus-visible {
    outline: none;
    box-shadow: none;
  }

  /* Range inputs get no outline from the browser worth keeping — the thumb is
   * what the user watches, so the thumb is what gets the ring. */
  .omx-ui input[type="range"]:focus-visible {
    outline: none;
  }
  .omx-ui input[type="range"]:focus-visible::-webkit-slider-thumb {
    box-shadow: var(--omx-shadow-focus);
  }
  .omx-ui input[type="range"]:focus-visible::-moz-range-thumb {
    box-shadow: var(--omx-shadow-focus);
  }

  /* Windows High Contrast: system colours replace ours, and a box-shadow ring
   * disappears entirely. Force a real outline back. */
  @media (forced-colors: active) {
    .omx-ui :focus-visible,
    .omx-ui :is(input, select, textarea, [data-omx-control]):focus-visible {
      outline: 2px solid Highlight;
      outline-offset: 2px;
    }
  }

  /* ===========================================================================
   * SCROLL CONTAINMENT
   * Any element that scrolls contains its overscroll by default. Reaching the
   * bottom of a drawer, a modal body or a table viewport must not start
   * scrolling the page behind it.
   * ======================================================================== */
  .omx-ui [data-omx-scroll] {
    overflow: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
  }

  /* ===========================================================================
   * SCREEN-READER-ONLY
   * `.omx-sr-only` hides visually and keeps the text in the accessibility tree.
   * The `focusable` variant reveals itself on focus — that is the skip link.
   * ======================================================================== */
  /* ===========================================================================
   * IMPLICIT GRID TRACKS
   * 🔴🔴 A grid with no `grid-template-columns` places its items in an IMPLICIT
   * column, and an implicit track is `auto` — sized by its widest child. So one
   * long string sets the width for the whole component, and the parent's
   * constraint is ignored.
   *
   * Measured three times in this package, in both directions:
   *   · `.omx-signin` inside a 206px card rendered a 320px track, because the
   *     SSO button's label is long — the auth page scrolled sideways at 320px.
   *   · `.omx-auth__stage` sized its card to the card's own max-content.
   *   · `.omx-page` with `place-content: center` collapsed to ZERO, because the
   *     card asked for `100%` of a track that was sized by the card.
   *
   * ⭐ ONE rule instead of 97: `grid-auto-columns` controls exactly the implicit
   * track and nothing else. A grid that declares its own columns is untouched,
   * and a `grid-auto-flow: column` component (Board, Calendar) that sets its own
   * `grid-auto-columns` still wins on specificity of intent — it declares one.
   *
   * ⚠️ `minmax(0, 1fr)` rather than `1fr`: a bare `1fr` still has an `auto`
   * minimum, which is the half that refuses to shrink. */
  .omx-ui [class*="omx-"] {
    grid-auto-columns: minmax(0, 1fr);
  }

  .omx-sr-only,
  .omx-sr-only-focusable:not(:focus):not(:focus-within) {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;

    /* 🔴🔴 WITHOUT THIS, A 1px HIDDEN ELEMENT WIDENS THE WHOLE DOCUMENT.
     * `position: absolute` with no inset keeps the element's STATIC position —
     * and inside a horizontally-scrolled region (a board, a wide pivot, a
     * table) that static position is hundreds of pixels to the right. Being
     * absolute, it resolves against the nearest positioned ancestor, which for
     * most of these is the INITIAL CONTAINING BLOCK — so it escapes the
     * scroller that was clipping everything else and pushes the page out.
     *
     * Measured at 375px, before: /gallery/archetype/card-board scrolled
     * sideways by 203px, /gallery/datadisplay by 220px — and the offender was
     * ONE `.omx-sr-only` span 1px wide sitting at x=577. `document.body`
     * measured a correct 375px throughout; only `documentElement` was wider,
     * which is what made it so hard to attribute.
     *
     * ⭐ Pinning it costs nothing: the element is 1px and clipped, so where it
     * sits is invisible either way. What it must not do is take part in
     * deciding how wide the page is. */
    inset-inline-start: 0;
  }

  .omx-skip-link {
    position: absolute;
    inset-block-start: var(--omx-space-2);
    inset-inline-start: var(--omx-space-2);
    z-index: var(--omx-z-tooltip);
    padding: var(--omx-space-4) var(--omx-space-6);
    background: var(--omx-color-surface-raised);
    color: var(--omx-color-text-link);
    border: var(--omx-border-width) solid var(--omx-color-accent);
    border-radius: var(--omx-radius-md);
    box-shadow: var(--omx-shadow-md);
  }

  /* ===========================================================================
   * LIVE REGIONS
   * Async results — a saved toast, a validation summary, a filtered row count —
   * must announce. A visually empty live region is correct and intentional.
   * ======================================================================== */
  .omx-ui [data-omx-live] { position: relative; }

  /* ===========================================================================
   * TOUCH TARGETS
   * Anything interactive that renders smaller than 44px gets its hit area
   * extended with a pseudo-element rather than by growing the visual control.
   * Opt in with `data-omx-target="extend"`.
   * ======================================================================== */
  .omx-ui [data-omx-target="extend"] { position: relative; }
  .omx-ui [data-omx-target="extend"]::after {
    content: "";
    position: absolute;
    inset: 50% auto auto 50%;
    translate: -50% -50%;
    min-width: var(--omx-size-touch);
    min-height: var(--omx-size-touch);
    width: 100%; height: 100%;
  }

  /* ===========================================================================
   * MOTION
   * Durations already collapse to 0ms under prefers-reduced-motion (tokens.css).
   * This is the belt for anything that animates without using a token — it must
   * still stop, and an infinite loop must stop looping rather than run at 0ms.
   * ======================================================================== */
  @media (prefers-reduced-motion: reduce) {
    .omx-ui *,
    .omx-ui *::before,
    .omx-ui *::after {
      animation-duration: 1ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 1ms !important;
      scroll-behavior: auto !important;
    }
  }

  /* ===========================================================================
   * PRINT
   * L9 owns document layout; this is only the baseline so that a page printed
   * without a print component is still legible.
   * ======================================================================== */
  @media print {
    .omx-ui {
      background: #fff;
      color: #000;
      --omx-shadow-xs: none; --omx-shadow-sm: none;
      --omx-shadow-md: none; --omx-shadow-lg: none;
    }
    .omx-ui [data-omx-print="hide"] { display: none !important; }
    .omx-ui [data-omx-scroll] { overflow: visible !important; height: auto !important; }
  }
}

/* --- Foundation/absence.css ---------------------------------------- */
/* =============================================================================
 * L0 · Foundation · Absence — a value that is not a value.
 *
 * 🔴🔴 THE DEFECT THIS EXISTS FOR IS RENDERING ABSENCE AS `0`, BLANK, OR AN EM
 * DASH. All three occupy the position of an answer. The rule the markup follows
 * is that the REASON goes where the number would have gone — so this file's
 * whole job is to make sure that reason cannot be mistaken for a figure.
 *
 * ⭐ THREE CUES, AND NOT ONE OF THEM IS COLOUR ALONE (WCAG 1.4.1):
 *   1. it leaves the numeric rhythm — `omx-table__cell--absent` start-aligns it
 *      inside an end-aligned money column, which is the loudest signal of the
 *      three and the only one visible from across a printed page;
 *   2. italic, which survives a monochrome laser;
 *   3. the muted text colour, which is a designed token and clears 4.5:1.
 * ========================================================================== */

@layer omx.base {

  .omx-absent {
    font-style: italic;
    color: var(--omx-color-text-muted);

    /* ⛔ Undo tabular figures. The reason is prose, and prose set in tabular
     * figures has the letter-spacing of a ledger column — it looks like a
     * number that failed to format. */
    font-variant-numeric: normal;

    /* It is not a figure, so it does not carry the figure's weight. */
    font-weight: var(--omx-font-weight-regular);
  }

  /* ⚠️ The totals row is bold, and an inherited bold would make a refusal look
   * like the loudest number on the page. */
  .omx-absent--total {
    font-weight: var(--omx-font-weight-regular);
  }

  /* 🔴 In forced-colors the author colour is discarded, so cue 3 is gone and
   * cue 2 is all that is left of the typography. GrayText is the system's own
   * "de-emphasised" slot and is the one value guaranteed to still contrast. */
  @media (forced-colors: active) {
    .omx-absent { color: GrayText; }
  }

  /* Ink. The muted grey is a designed SCREEN value; on paper it prints as a
   * wash that photocopies to nothing, and "the figure is missing" is exactly
   * the sentence that must survive a photocopy. Italic still carries it. */
  @media print {
    .omx-absent { color: #000; }
  }
}

/* --- Foundation/effective.css -------------------------------------- */
/* =============================================================================
 * L0 · Foundation · Effective — where a version sits in time.
 *
 * 🔴🔴 THE DEFECT THIS EXISTS FOR IS "THE LATEST ROW WINS". A rule is a value
 * WITH A DATE RANGE; which one applies depends on the date being asked about,
 * not on which row was saved last. The failure is silent — nothing looks broken
 * until somebody backdates, and then the number is confidently wrong.
 *
 * ⛔⛔ THE ROW IS NOT DIMMED, AND THAT IS THE WHOLE DESIGN. The requirement is
 * that a superseded version stays "addressable and visible". A superseded rule
 * was CORRECT IN ITS TIME — it is not an error, not deleted, and not less true
 * than the current one for the dates it covered. Dimming it, striking it
 * through, or hiding it in a drawer all say otherwise. So the row's own text
 * keeps full contrast and only this chip differs.
 *
 * ⭐ AND THE CHIP CARRIES WORDS, NOT JUST A COLOUR (WCAG 1.4.1). "1 Jan – 30
 * Jun 2026 · superseded" is legible with every colour stripped out — and the
 * window IS the reason the row is superseded, so it is the honest label rather
 * than a decoration beside one.
 * ========================================================================== */

@layer omx.base {

  .omx-effective {
    display: inline-block;
    margin-inline-start: var(--omx-space-2);
    padding-inline: var(--omx-space-2);
    font-size: var(--omx-font-size-caption);
    line-height: 1.5;
    white-space: nowrap;
    border-radius: var(--omx-radius-sm);
    /* 🔴 --omx-color-border-subtle measures 1.22 light / 1.14 dark on this
     * plane and is INVISIBLE — the same value that made the brand plate's
     * first instrument marks present in the DOM and absent on screen.
     * `superseded`, whose only mark is a DASHED border, therefore showed no
     * border at all: three of four states drew a boundary and the fourth
     * silently did not.
     * ⛔ Not made loud. The structure register (1.95 / 1.90) is perceptible
     * as a boundary and still far quieter than the toned states at
     * 3.65-4.16 — a superseded rule was correct in its time. */
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    color: var(--omx-color-text-muted);

    /* Dates are figures. A column of windows that does not line up is a column
     * nobody reads down. */
    font-variant-numeric: tabular-nums;
  }

  /* In force on the date asked about. ⚠️ Rendered at all only because a table
   * showing several versions needs to say WHICH one is live — on a single-row
   * view it is noise the caller can leave off by not setting AsOf. */
  .omx-effective[data-omx-effective="current"] {
    border-color: var(--omx-color-success-border);
    color: var(--omx-color-success-text);
  }

  /* Decided, not yet in force. */
  .omx-effective[data-omx-effective="future"] {
    border-color: var(--omx-color-info-border);
    color: var(--omx-color-info-text);
  }

  /* Ended, and something took over. The NORMAL end of a version's life, so it
   * is the quietest of the four — neutral border, muted text, no alarm. */
  .omx-effective[data-omx-effective="superseded"] {
    border-style: dashed;
  }

  /* 🔴 Ended and NOTHING replaced it ⇒ the system has no answer for today.
   * The only one of the four that is a DEFECT rather than a lifecycle stage,
   * so it is the only one that gets a warning tone. */
  .omx-effective[data-omx-effective="expired"] {
    border-color: var(--omx-color-warning-border);
    color: var(--omx-color-warning-text);
    font-weight: var(--omx-font-weight-medium);
  }

  /* 🔴 In forced-colors the author colour is discarded entirely, so all four
   * states would look identical — which is exactly the collapse the chip's
   * WORDS are there to survive. The border style is kept because it is the one
   * differentiator the system palette does not flatten. */
  @media (forced-colors: active) {
    .omx-effective { border-color: CanvasText; color: CanvasText; }
  }

  /* Ink. The muted grey photocopies to nothing, and "this version was replaced"
   * is exactly the sentence that has to survive a photocopy. */
  @media print {
    .omx-effective { color: #000; border-color: #000; }
  }
}

/* --- Foundation/field-lock.css ------------------------------------- */
/* ==========================================================================
 * L0 · Foundation · FieldLock — a value you cannot change, and why.
 *
 * 🔴 IT IS NOT A GREYED CONTROL. A disabled box looks typeable and is not, and
 * the reader's conclusion is that the software is broken. The value renders as
 * TEXT; the reason renders as a sentence under it.
 *
 * ⭐ THE STRUCTURE REGISTER, deliberately — 1px --omx-color-border-strong,
 * measured 1.95 light / 1.90 dark. A lock is a fact about the FORM's shape, the
 * same class of fact as a group boundary. ⛔ Not the instrument register: that
 * one says "which thing am I looking at", and spending it here would let a
 * locked quantity out-rank the page's own identity.
 *
 * ⛔ Lives in Foundation because both an L1 primitive (Input, rendering the
 * value) and an L5 field (rendering the sentence) write these classes — the
 * same argument that puts FieldContext at L0.
 * ========================================================================== */

@layer omx.base {

  /* ⚠️ min-block-size is the CONTROL height, not a round number. A form with
     some fields locked and some editable must stay on one grid; without this
     the text collapses to its line box and every locked row sits 12px shorter
     than its neighbours. */
  .omx-field-lock__value {
    display: flex;
    align-items: center;
    min-block-size: var(--omx-control-height);
    margin: 0;
    padding-inline-start: var(--omx-space-4);
    font-size: var(--omx-font-size-body);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text);

    /* The quiet mark that says STATED, not ENTERED. ⛔ No background, no box:
       anything that fills a rectangle here starts to read as a control again,
       which is the entire defect. */
    border-inline-start: var(--omx-border-width) solid var(--omx-color-border-strong);

    /* A locked value is still a value someone may need to copy. */
    user-select: text;
    overflow-wrap: anywhere;
  }

  /* Numbers line up with the numbers in the editable fields above and below.
     ⚠️ The absence inside a locked-and-empty value undoes this for itself --
     absence.css sets font-variant-numeric: normal, because a reason set in
     tabular figures reads as a number that failed to format. */
  .omx-field--fixed .omx-field-lock__value {
    font-variant-numeric: tabular-nums;
  }

  .omx-field-lock {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-2);
    margin: var(--omx-space-2) 0 0;
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-muted);
  }

  .omx-field-lock__icon {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
    /* Optical: a 1em glyph beside a 1.5 line-height caption rides high. */
    margin-block-start: .25em;
  }

  /* ⛔ The reason is NOT dimmed further than the caption already is. It is the
     only thing on screen that explains why the field is dead, so it is the last
     text that should be hard to read. */
  .omx-field-lock__reason {
    min-inline-size: 0;
  }

  /* The route out — quieter than the reason, because it is the second question
     a reader asks, not the first. */
  .omx-field-lock__route {
    min-inline-size: 0;
    color: var(--omx-color-text-subtle);
  }

  /* ⛔ NOT colour alone (WCAG 1.4.1). The icon and the sentence both carry the
     state; this rule only stops the mark vanishing where the border colour
     cannot be trusted. */
  @media (forced-colors: active) {
    .omx-field-lock__value { border-inline-start-color: CanvasText; }
    .omx-field-lock { color: CanvasText; }
  }

  /* Ink. The muted grey is a designed SCREEN value; on paper "you cannot change
     this" is exactly the sentence that must survive a photocopy. */
  @media print {
    .omx-field-lock { color: #000; }
  }

}

/* --- Foundation/provenance.css ------------------------------------- */
@layer omx.base {

  /* L0 · Foundation · provenance — which source won, at the cell.
   *
   * ⭐ THE SUMMARY IS THE ANSWER. A reader who never opens anything still sees
   * which source produced the figure; opening it shows what that source beat.
   * ⛔ So this is styled as a QUIET LABEL, not a control: it sits with the
   * value, it does not compete with it, and it is legible before any hover. */
  .omx-prov {
    display: inline;
  }

  .omx-prov__tag {
    display: inline;
    cursor: pointer;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    /* \u26d4 The default triangle is removed, because in a dense column of figures a
     * row of triangles reads as a column of controls — the tag itself is the
     * affordance, and it is underlined on focus and hover like the link it
     * behaves as. */
    list-style: none;
  }

  .omx-prov__tag::-webkit-details-marker { display: none; }

  .omx-prov__tag:hover,
  .omx-prov__tag:focus-visible { text-decoration: underline; }

  /* ⛔ A :hover with no :active does nothing for touch or for a keyboard, and
   * this element TOGGLES — a press that gives no feedback reads as a dead
   * control on exactly the devices that cannot hover. */
  .omx-prov__tag:active { color: var(--omx-color-text); }

  /* ⛔ NO FOCUS RULE HERE ON PURPOSE. `a11y.css` already gives every
   * :focus-visible element a ring, and a <summary> is focusable by default —
   * a second declaration would be the same fact in two files, which is how
   * the two drift. The underline above is an EXTRA affordance, not a copy. */

  /* ⚠️ An ASSUMED value is marked on the face of the cell, never only inside
   * the disclosure. ASSUMED_LIVE is not LIVE, and a distinction that requires a
   * click is a distinction nobody makes. */
  /* 🔴 STYLED FROM THE ATTRIBUTE, NOT A SECOND CLASS. This shipped as both
   * `data-omx-prov="assumed"` and `.omx-prov--assumed`, which is one fact in two
   * places — and `check-omx` C16 caught the attribute as a promise nothing read.
   * ⭐ The attribute wins because a CONSUMING module styles by it too; a modifier
   * class only this file knows about would have to be mirrored downstream. */
  .omx-prov[data-omx-prov="assumed"] .omx-prov__tag {
    color: var(--omx-color-warning-text);
    font-style: italic;
  }

  .omx-prov__body {
    margin-block-start: var(--omx-space-1);
    padding-inline-start: var(--omx-space-2);
    border-inline-start: var(--omx-border-width) solid var(--omx-color-border-strong);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-tight);
    text-align: start;
    /* ⛔ A chain in a numeric column must not inherit the column's end-alignment
     * or tabular figures — it is prose about the number, not another number. */
    font-variant-numeric: normal;
  }

  .omx-prov__step { display: block; }

  /* ⭐ The winner is the only one that reads at full strength. The losers are
   * evidence, and evidence that shouts competes with the answer. */
  .omx-prov__step[data-omx-source="supplied"],
  .omx-prov__step[data-omx-source="assumed"] {
    color: var(--omx-color-text);
    font-weight: var(--omx-font-weight-medium);
  }

  /* \U0001f534 A withheld source is marked, because "there is no contract rate" and
   * "there is one and you may not see it" send a reader in opposite directions
   * — and creating a second rate is how a duplicate gets into a system. */
  .omx-prov__step[data-omx-source="not-permitted"] { font-style: italic; }

  .omx-prov__shadowed { display: block; margin-block-start: var(--omx-space-1); }
}

/* --- Foundation/row-age.css ---------------------------------------- */
@layer omx.base {

  /* L0 · Foundation · row age — how old one row's figure is.
   *
   * 🔴🔴 ONE RULE, AND NO STATE SELECTORS. There is no "stale" variant and never
   * will be: PLANNING forbids inventing a bound, and KNITDYE requires the row to
   * fail open. A figure three days old looks exactly like one three minutes old
   * except for the words — the reader judges, the component does not. */
  .omx-row-age {
    display: block;
    margin-block-start: var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    font-variant-numeric: tabular-nums;
    text-align: start;
  }
}

/* --- Foundation/Brand/Brand.css ------------------------------------ */
/* ==========================================================================
 * L0 · Foundation · Brand — the Omnixa identity lockup.
 *
 * 🔴 THE THEME SWAP IS THE WHOLE POINT OF THIS FILE. The wordmark ink is
 * #1F2C3A and vanishes on a dark panel, and the dark asset is a REMAPPED image
 * (ink lifted to white by brightness; the gradient x and violet tagline lifted
 * separately) — `filter: invert()` would wreck both. So both images are in the
 * DOM and exactly one is shown.
 *
 * ⚠️ THREE THEME STATES, NOT TWO — the same trap the rest of this package
 * documents. An explicit choice stamps `data-theme` on the root; the DEFAULT
 * ("system") stamps nothing and only `prefers-color-scheme` separates light
 * from dark. So the dark wordmark must be selected by BOTH, and the light one
 * restored under `[data-theme="light"]`, or a light-mode toggle inside a
 * dark-OS session shows the wrong file.
 *
 * ⚠️ @layer omx.base, NOT omx.component — and that is enforced, not chosen.
 * `check-omx` C10 allows a file under Foundation/ to write only into omx.base
 * or omx.reset, and it failed this file's first version. The architecture doc
 * calls Brand "identity plumbing, not a widget", and the layer rule agrees with
 * it: the lockup sits with the tokens and the reset, beneath every component.
 * ========================================================================== */

@layer omx.base {

  .omx-brand {
    /* ⚠️ DEFAULTS DECLARED HERE, not only in the inline style. The view sets
     * these three per instance, but a var() with no declaration anywhere is
     * unresolvable to a reader and to C1 — and if the inline style is ever
     * dropped the lockup would collapse to zero rather than fall back. */
    --omx-brand-size: 28px;
    --omx-brand-word-w: 86px;
    --omx-brand-word-h: 20px;

    display: inline-flex;
    flex-direction: column;
    gap: var(--omx-space-1);
    text-decoration: none;
    color: inherit;
    /* The lockup is one object: it never stretches to its container. */
    inline-size: fit-content;
  }

  /* omx-no-press: this SUPPRESSES the underline reset.css gives every a[href]:hover — it is not an affordance. The lockup is deliberately inert-looking; a logo that flashes on press reads as a bug */
  a.omx-brand:hover { text-decoration: none; }

  /* ⛔ NO FOCUS RULE HERE, DELIBERATELY. `a11y.css` already gives every
   * focusable thing in `.omx-ui` one ring on :focus-visible. This file's first
   * version re-declared it against two tokens that do not exist — which is both
   * "a second way to do what the package already does" and the reason C1 fired.
   * The global ring is the right ring. */

  .omx-brand__row {
    display: inline-flex;
    align-items: center;
    /* ⭐ 0.34, not 0.28. The old PNG monogram carried internal padding, so
     * its INK stopped short of its box and the box gap overstated the optical
     * one. The vector mark's ring touches its box edge, so the same number
     * reads tighter ⚠️ measured: ink-to-ink fell by roughly the padding the
     * raster used to carry. The box gap grows to hold the optical gap still. */
    /* ⭐ 0.14, down from 0.34 across two steps. The gap was tuned against a
     * box that was a third empty; now that the box is the letters, the same
     * number read as a hole. Owner: start the title nearer the mark — and
     * the 3px it returns goes straight into the title's width. */
    gap: calc(var(--omx-brand-size) * 0.14);

    /* 🔴 A LOGO DOES NOT MIRROR. Everything else in this package is built from
     * logical properties so it flips correctly under `dir="rtl"` — and the first
     * RTL render flipped the lockup too, putting the wordmark before the mark.
     * ⚠️ That is a composition the brand never approved: the artwork is a fixed
     * arrangement, and the pack ships no RTL variant of it. `direction: ltr`
     * pins the lockup's internal order while the PAGE around it still mirrors.
     * ⭐ Found by looking at the Arabic screenshot, not by reading the CSS. */
    direction: ltr;
  }

  /* Sized from the model, not from the file. The width/height attributes are on
   * the element too, so the box is right on the FIRST paint — a logo that
   * resizes when its image lands is a layout shift on the most looked-at
   * element on the page. */
  .omx-brand__mark {
    inline-size: var(--omx-brand-size);
    block-size: var(--omx-brand-size);
    /* The artwork is square; say so, so a wrong asset cannot distort silently. */
    aspect-ratio: 1 / 1;
    object-fit: contain;
    flex: none;
  }

  /* 🔴🔴 A GLOW BEHIND THE MARK WAS BUILT HERE AND THEN REMOVED,
   * BECAUSE IT WAS MEASURED AND IT WAS INVISIBLE. A radial brand wash sat
   * behind the monogram at 24% accent; sampling the rendered pixels either
   * side of it gave a channel delta of **1 in light and 0 in dark**. It was
   * decoration that decorated nothing, plus two hover states and a press state
   * wired to something no one could see.
   * ⭐ The idea was wrong at this size, not merely too faint: a glow needs a
   * dark ground or a large mark, and this is a 28px monogram on near-white.
   * What actually carries the identity zone was measured too -- the seam at
   * delta 195/224, the accent rule at 397/383, the plinth at 25/19. Three of
   * the four moves were doing the work; this one was not.
   * ⛔ Do not reintroduce it without measuring it. An effect nobody can see
   * is the same defect class as a `data-omx-*` nothing reads. */

  /* 🔴🔴 SIZED BY ITS LETTERS, NOT BY ITS CANVAS ⛔ the fix for "the
   * title is smaller than it should be". The artwork is 900x173 but every
   * letter's ink is in rows 46..158: 113px, 65.3% of the box. The model now
   * divides by that share, so a ratio means LETTERS at that ratio.
   * ⚠️ The artwork is untouched; only the arithmetic changed. */
  .omx-brand__word {
    inline-size: var(--omx-brand-word-w);
    block-size: var(--omx-brand-word-h);
    aspect-ratio: 900 / 173;
    object-fit: contain;
    flex: none;

    /* 🔴 AND THE SECOND HALF OF THE SAME DEFECT. The letter band's centre is
     * row 102; the canvas centre is row 86. The letters sit 16px LOW in a 173
     * canvas — 9.2% of the box — so centring the BOX leaves the LETTERS below
     * the mark's centre line. This lifts them back onto it. */
    translate: 0 calc(var(--omx-brand-word-h) * -0.092);
  }

  /* ⚠️ The raster wordmark's sizing block lived here and is gone with
   * it. `WordWidth`/`WordHeight` on the model are what fed it; they remain
   * only so a consumer still drawing the PNG from its own markup can size
   * it correctly. Nothing in this package renders it. */

  /* -- a tenant's own logo -------------------------------------------------
   * Sized by HEIGHT only: the package cannot know a customer logo's aspect
   * ratio, so constraining width would distort it. `max-inline-size` keeps a
   * very wide lockup from pushing the sidebar apart. */
  .omx-brand__tenant {
    block-size: var(--omx-brand-size);
    inline-size: auto;
    max-inline-size: calc(var(--omx-brand-size) * 8);
    object-fit: contain;
    flex: none;
  }

  /* ⚠️ These two rules exist ONLY when a dark file was supplied -- the view does
   * not emit `--dark` otherwise, so a single tenant image is never hidden by
   * them. Same three-state theme handling as the wordmark below. */
  .omx-brand__tenant--dark { display: none; }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-brand__tenant--light { display: none; }
    .omx-ui:not([data-theme="light"]) .omx-brand__tenant--dark  { display: block; }
  }
  .omx-ui[data-theme="dark"] .omx-brand__tenant--light { display: none; }
  .omx-ui[data-theme="dark"] .omx-brand__tenant--dark  { display: block; }
  .omx-ui[data-theme="light"] .omx-brand__tenant--light { display: block; }
  .omx-ui[data-theme="light"] .omx-brand__tenant--dark  { display: none; }

  /* -- the theme swap ------------------------------------------------------
   * 🔴 The ink is #1F2C3A and vanishes on a dark panel, and the dark asset is
   * a REMAPPED image, not something `filter: invert()` can fake. Both are in
   * the DOM and exactly one is shown.
   * ⚠️ THREE theme states: an explicit choice stamps `data-theme`, the
   * default stamps nothing and only `prefers-color-scheme` separates them.
   * 🔴 And these key off `.omx-ui`, NOT `:root` ⛔ this package puts
   * `data-theme` on the BODY, and when this one file disagreed, a user who
   * switched the app to dark kept the light wordmark.
   * ⚠️ A live-text title was tried here and REVERTED on the owner's call;
   * the diagnosis it produced is what the sizing fix above is built on. */
  .omx-brand__word--dark { display: none; }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-brand__word--light { display: none; }
    .omx-ui:not([data-theme="light"]) .omx-brand__word--dark  { display: block; }
  }
  .omx-ui[data-theme="dark"] .omx-brand__word--light { display: none; }
  .omx-ui[data-theme="dark"] .omx-brand__word--dark  { display: block; }
  .omx-ui[data-theme="light"] .omx-brand__word--light { display: block; }
  .omx-ui[data-theme="light"] .omx-brand__word--dark  { display: none; }

  .omx-on-dark .omx-brand__word--light,
  .omx-brand--on-dark .omx-brand__word--light { display: none; }
  .omx-on-dark .omx-brand__word--dark,
  .omx-brand--on-dark .omx-brand__word--dark { display: block; }

  /* Ink on white, always ⛔ never the dark wordmark, whatever theme
   * the screen was in when Print was pressed. */
  @media print {
    .omx-brand__word--light { display: block; }
    .omx-brand__word--dark  { display: none; }
  }

  /* Windows High Contrast drops background images and can flatten artwork;
   * keep the lockup from being repainted into the forced palette. */
  @media (forced-colors: active) {
    .omx-brand__mark,
    .omx-brand__word { forced-color-adjust: none; }
  }
}

/* --- Primitives/Avatar/Avatar.css ---------------------------------- */
/* =============================================================================
 * L1 · Primitives · Avatar
 * ========================================================================== */

@layer omx.component {

  .omx-avatar {
    /* The hue is set inline from a hash of the person's NAME, so one person is
     * one colour everywhere — across pages, across modules, across sessions.
     * Hashing the name and not the row index is what makes that true.
     * Saturation and lightness are fixed here, so no generated hue can drift
     * out of contrast with the initials sitting on it. */
    --omx-avatar-hue: 220;

    display: inline-grid;
    place-items: center;
    inline-size: var(--omx-size-avatar-sm);
    block-size: var(--omx-size-avatar-sm);
    flex: 0 0 auto;
    overflow: hidden;

    border-radius: var(--omx-radius-pill);
    background: hsl(var(--omx-avatar-hue) 42% 88%);
    color: hsl(var(--omx-avatar-hue) 55% 26%);
    /* A hairline stops a pale avatar dissolving into a pale surface. */
    box-shadow: inset 0 0 0 1px var(--omx-color-hairline);
  }

  .omx-avatar--lg {
    inline-size: var(--omx-size-avatar-md);
    block-size: var(--omx-size-avatar-md);
  }

  .omx-avatar__img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
  }

  .omx-avatar__initials {
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    line-height: 1;
    letter-spacing: 0.02em;
    user-select: none;
  }
  .omx-avatar--lg .omx-avatar__initials { font-size: var(--omx-font-size-label); }

  /* 🔴🔴 FIXED 2026-09-19 — THE SECOND SELECTOR WAS NOT INSIDE THE MEDIA QUERY.
   * `.omx-ui:not([data-theme="light"])` matches the DEFAULT, UNSTAMPED state —
   * which tokens.css documents as "system", the state every user is in until
   * they open Appearance and pick something. So this dark rule applied on a
   * LIGHT page: MEASURED at OS colorScheme=light with no attribute, the canvas
   * was rgb(238,241,246) and the avatar was rgb(46,86,80) — a dark teal chip on
   * a near-white page, for everyone who never touched the theme setting.
   * ⭐ THREE STATES, NOT TWO: explicit dark · explicit light · system. Only the
   * media query separates the third, and tokens.css and Brand.css both wrap it.
   * This file was the one that did not. */
  .omx-ui[data-theme="dark"] .omx-avatar {
    background: hsl(var(--omx-avatar-hue) 30% 26%);
    color: hsl(var(--omx-avatar-hue) 60% 84%);
    /* The hairline no longer needs a dark override — it is a token now, and
     * the token already flips. */
  }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-avatar {
      background: hsl(var(--omx-avatar-hue) 30% 26%);
      color: hsl(var(--omx-avatar-hue) 60% 84%);
    }
  }
}

/* --- Primitives/Badge/Badge.css ------------------------------------ */
/* =============================================================================
 * L1 · Primitives · Badge
 *
 * Six tones × three variants from two local variables. Every tone is a token
 * PAIR — a wash and a text colour that clears 4.5:1 on it — so a badge can
 * never be assembled from a background without its matching foreground.
 * ========================================================================== */

@layer omx.component {

  .omx-badge {
    --omx-badge-bg: var(--omx-color-neutral-bg);
    --omx-badge-fg: var(--omx-color-neutral-text);

    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-3);

    /* Does NOT follow --omx-control-height: a badge sits inside a line of text,
     * so it is sized by the type it sits in, not by the control scale. A badge
     * as tall as a button would wreck a table row. */
    padding: var(--omx-space-1) var(--omx-space-4);

    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-medium);
    line-height: 1.45;
    white-space: nowrap;

    color: var(--omx-badge-fg);
    background: var(--omx-badge-bg);
    border: var(--omx-border-width) solid transparent;
    border-radius: var(--omx-radius-sm);
  }

  .omx-badge__text { overflow: hidden; text-overflow: ellipsis; }

  .omx-badge__dot {
    inline-size: 6px;
    block-size: 6px;
    border-radius: var(--omx-radius-pill);
    background: currentColor;
    flex: 0 0 auto;
  }

  .omx-badge__icon {
    inline-size: 12px;
    block-size: 12px;
    flex: 0 0 auto;
    fill: currentColor;
  }

  /* -- tones: each sets the PAIR, never one half ---------------------------- */
  .omx-badge--neutral { --omx-badge-bg: var(--omx-color-neutral-bg); --omx-badge-fg: var(--omx-color-neutral-text); }
  .omx-badge--info    { --omx-badge-bg: var(--omx-color-info-bg);    --omx-badge-fg: var(--omx-color-info-text); }
  .omx-badge--success { --omx-badge-bg: var(--omx-color-success-bg); --omx-badge-fg: var(--omx-color-success-text); }
  .omx-badge--warning { --omx-badge-bg: var(--omx-color-warning-bg); --omx-badge-fg: var(--omx-color-warning-text); }
  .omx-badge--danger  { --omx-badge-bg: var(--omx-color-danger-bg);  --omx-badge-fg: var(--omx-color-danger-text); }
  .omx-badge--accent  { --omx-badge-bg: var(--omx-color-accent-subtle); --omx-badge-fg: var(--omx-color-accent-hover); }

  /* -- variants ------------------------------------------------------------- */
  /* Solid inverts the pair: the former foreground becomes the fill, and the
   * text takes the surface colour. Keeps the contrast relationship intact
   * instead of inventing a second palette. */
  .omx-badge--solid {
    background: var(--omx-badge-fg);
    color: var(--omx-color-surface);
    border-color: var(--omx-badge-fg);
  }

  .omx-badge--outline {
    background: transparent;
    border-color: color-mix(in srgb, var(--omx-badge-fg) 38%, transparent);
  }

  @media (forced-colors: active) {
    /* HCM strips the wash, so tone becomes invisible. The border is what keeps
     * the badge legible as a distinct object — the WORD is what keeps it
     * meaningful, which is why Text has no null path. */
    .omx-badge { border-color: CanvasText; background: Canvas; color: CanvasText; }
  }

  @media print {
    .omx-badge { border-color: #000; background: transparent; color: #000; }
  }
}

/* --- Primitives/Break/Break.css ------------------------------------ */
/* =============================================================================
 * L1 · Primitives · Break — where a printed document is allowed to split.
 *
 * ⭐ THE ONE PAGINATION CONTROL THAT WORKS IN EVERY ENGINE. The page NUMBER
 * needs CSS Paged Media margin boxes, which today only Chromium implements;
 * `break-before: page` is not in that position — it is supported everywhere,
 * and it is what a caller needs in order to say "this section starts its own
 * sheet". Measured 2026-09-21 against a real PDF: a probe with one forced break
 * produced exactly the expected page count, and a named landscape page flipped
 * that page's MediaBox to 842x595.
 *
 * 🔴 WHY A UTILITY LAYER AND NOT THE COMPONENT ONE. Components already set
 * `break-inside: avoid` on `.omx-table__row`, `.omx-card`, `.omx-list__item` and
 * a dozen more. A break class in `@layer omx.component` would tie with those and
 * be decided by source order — which file the bundler happened to write first.
 * `omx.utility` sits ABOVE `omx.component`, so a caller's explicit instruction
 * wins over a component's default, every time.
 *
 * ⚠️ NO `@media print` WRAPPER. `break-before`/`break-after` are inert on a
 * continuous screen anyway, and wrapping them would hide them from a paged
 * preview — which is the only way anyone looks at pagination without printing.
 * ========================================================================== */

@layer omx.utility {

  /* Start this element on a new sheet. */
  .omx-break-before { break-before: page; }

  /* End the sheet after this element. */
  .omx-break-after { break-after: page; }

  /* Keep this block whole.
   * ⛔ NOT the same kind of thing as the two above — this one DOES act on
   * screen, inside a multi-column container. */
  .omx-break-avoid { break-inside: avoid; }
}

/* --- Primitives/Button/Button.css ---------------------------------- */
/* =============================================================================
 * L1 · Primitives · Button
 *
 * Five variants, one geometry. Height, padding and gap all come from the
 * density block in Foundation/tokens.css, so a button is 32px comfortable,
 * 28px cozy, 24px compact and 44px on a phone without this file knowing that.
 * ========================================================================== */

@layer omx.component {

  .omx-button {
    /* Component-local derivations. Named for the component, resolved from
     * tokens — the one legal exception to "tokens live in tokens.css". */
    --omx-button-bg:        var(--omx-color-control);
    --omx-button-bg-hover:  var(--omx-color-control-hover);
    --omx-button-bg-active: var(--omx-color-control-active);
    --omx-button-fg:        var(--omx-color-text);
    --omx-button-border:    var(--omx-color-border-strong);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--omx-space-3);

    /* 🔧 `min-block-size`, NOT `block-size`. A hard height CLIPS whatever the
     * larger text scales put inside it instead of growing — the same trap the
     * topbar was fixed for, one layer down and on every control in the package.
     * MEASURED at `data-text-scale="board"` (×2.4) on /gallery/primitives:
     * **12 of 17 buttons clipped**, a 24px box holding a label that needed
     * 33.6px. At `large` (×1.14) nothing clips, which is why it survived — the
     * scale most people actually move to is the one that still fits.
     * ⭐ Nothing moves at the default scale: the min IS the height until the
     * content asks for more, which is the property a hard height was only
     * pretending to have. */
    min-block-size: var(--omx-control-height);
    padding-inline: var(--omx-control-pad-x);
    min-inline-size: 0;               /* lets a long label truncate, not overflow */

    font: inherit;
    font-size: var(--omx-font-size-control);
    font-weight: var(--omx-font-weight-medium);
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;

    color: var(--omx-button-fg);
    background: var(--omx-button-bg);
    border: var(--omx-border-width) solid var(--omx-button-border);
    border-radius: var(--omx-radius-md);
    cursor: pointer;
    user-select: none;

    /* ⛔ Never `transition: all` — it animates layout properties by accident and
     * makes every hover a compositor job. Name them. */
    transition:
      background-color var(--omx-duration-fast) var(--omx-easing-standard),
      border-color     var(--omx-duration-fast) var(--omx-easing-standard),
      color            var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-button__label {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .omx-button__icon {
    inline-size: var(--omx-size-icon-inline);
    block-size:  var(--omx-size-icon-inline);
    flex: 0 0 auto;
    fill: currentColor;
  }

  /* -- interaction ---------------------------------------------------------
   * Hover and active must be MORE contrasty than rest, never less — a state
   * that reduces contrast reads as "going away", not "responding". */
  .omx-button:hover  { background: var(--omx-button-bg-hover); }
  .omx-button:active { background: var(--omx-button-bg-active); }

  /* Focus ring is Foundation's job (a11y.css). Nothing here overrides it. */

  /* -- unavailable ----------------------------------------------------------
   * Two shapes, on purpose. `disabled` leaves the tab order and can say
   * nothing; `aria-disabled` stays reachable and keeps its tooltip. Both look
   * identical, because to a sighted user they mean the same thing. */
  /* 🔴 FIXED 2026-09-12, found by looking at the gallery — not by any gate.
   * This block used to set `background` directly and leave the foreground
   * alone. On `--primary` the foreground is `--omx-color-accent-text`
   * (near-white, correct on the blue fill), so a disabled primary button
   * rendered WHITE LABEL ON PALE GREY: an empty box. Every variant was one
   * `Disabled = true` away from an invisible label.
   * The fix is to re-point the three LOCAL variables instead of the final
   * properties, so one rule is correct for all five variants and any future
   * one. ⭐ A component that overrides half of a variant's contract is worse
   * than one that overrides none of it. */
  .omx-button:disabled,
  .omx-button[aria-disabled="true"] {
    --omx-button-bg:        var(--omx-color-control);
    --omx-button-bg-hover:  var(--omx-color-control);
    --omx-button-bg-active: var(--omx-color-control);
    --omx-button-fg:        var(--omx-color-text-muted);
    --omx-button-border:    var(--omx-color-border);
    opacity: 0.65;
    cursor: not-allowed;
  }
  /* Link and ghost have no fill to grey out; keep them flat so a disabled
   * inline action does not grow a box it never had. */
  .omx-button--link:disabled,
  .omx-button--link[aria-disabled="true"],
  .omx-button--ghost:disabled,
  .omx-button--ghost[aria-disabled="true"] {
    --omx-button-bg: transparent;
    --omx-button-bg-hover: transparent;
    --omx-button-bg-active: transparent;
    --omx-button-border: transparent;
  }
  .omx-button[aria-disabled="true"] { pointer-events: none; }
  .omx-button[aria-disabled="true"]:focus-visible { pointer-events: auto; }

  /* -- busy -----------------------------------------------------------------
   * The control keeps its width while the label swaps, so a row of buttons
   * does not reflow the moment one is clicked. */
  .omx-button[aria-busy="true"] {
    cursor: progress;
    pointer-events: none;
  }

  .omx-button__spinner {
    inline-size: var(--omx-size-icon-inline);
    block-size:  var(--omx-size-icon-inline);
    flex: 0 0 auto;
    border: 2px solid currentColor;
    border-block-start-color: transparent;
    border-radius: var(--omx-radius-pill);
    opacity: 0.75;
    animation: omx-button-spin 620ms linear infinite;
  }

  @keyframes omx-button-spin { to { rotate: 360deg; } }

  /* prefers-reduced-motion is handled globally in a11y.css: the animation is
   * clamped to one 1ms iteration, so this becomes a static ring rather than a
   * spinner running invisibly fast. */

  /* =========================================================================
   * VARIANTS — each one only re-points the four local variables.
   * ====================================================================== */

  .omx-button--primary {
    --omx-button-bg:        var(--omx-color-accent);
    --omx-button-bg-hover:  var(--omx-color-accent-hover);
    --omx-button-bg-active: var(--omx-color-accent-active);
    --omx-button-fg:        var(--omx-color-accent-text);
    --omx-button-border:    var(--omx-color-accent);
  }

  .omx-button--secondary {
    --omx-button-bg:        var(--omx-color-surface);
    --omx-button-bg-hover:  var(--omx-color-control-hover);
    --omx-button-bg-active: var(--omx-color-control-active);
  }

  .omx-button--ghost {
    --omx-button-bg:        transparent;
    --omx-button-bg-hover:  var(--omx-color-control-hover);
    --omx-button-bg-active: var(--omx-color-control-active);
    --omx-button-fg:        var(--omx-color-text-muted);
    --omx-button-border:    transparent;
  }
  .omx-button--ghost:hover { --omx-button-fg: var(--omx-color-text); }
  .omx-button--ghost:active { --omx-button-fg: var(--omx-color-text); }

  .omx-button--danger {
    --omx-button-bg:        var(--omx-color-danger-solid);
    --omx-button-bg-hover:  color-mix(in srgb, var(--omx-color-danger-solid) 86%, black);
    --omx-button-bg-active: color-mix(in srgb, var(--omx-color-danger-solid) 74%, black);
    --omx-button-fg:        var(--omx-color-danger-on);
    --omx-button-border:    var(--omx-color-danger-solid);
  }

  .omx-button--link {
    --omx-button-bg:        transparent;
    --omx-button-bg-hover:  transparent;
    --omx-button-bg-active: transparent;
    --omx-button-fg:        var(--omx-color-text-link);
    --omx-button-border:    transparent;

    /* 🔴 IT LOOKS LIKE A LINK. IT IS STILL A TARGET.
     * `block-size: auto` is right and stays — it drops the base's HARD height so
     * the variant never grows a box it never had. But auto alone made the whole
     * control the height of its own text: MEASURED at 375px, **17.6 x 30px**,
     * under WCAG 2.5.8's hard floor of 24 x 24, on the one control in the set
     * that has no fill to aim at.
     * ⇒ `min-block-size` gives it the target without giving it a box: the
     * background and border are transparent, so the extra height is invisible
     * and clickable, and it follows --omx-control-height, which is the 44px
     * touch size below 768px.
     *
     * ⚠️ WHY THIS IS SAFE HERE AND WOULD NOT BE INLINE IN PROSE. The base is
     * `display: inline-flex`, so a 44px one sitting in a sentence would inflate
     * that line box. MEASURED across the eight gallery routes: two instances,
     * both standalone controls — a cluster of variants and the filter bar's
     * "Clear all" — and NEITHER has a text sibling. ⛔ If this variant is ever
     * put inside a sentence, the fix is a pseudo-element hit area, not this.
     *
     * ⚠️ The INLINE axis is still the label's own width: `padding-inline: 0` is
     * what makes it read as a link. Both instances measure 30px or wider, so
     * nothing fails today, but a two-character label would.
     *
     * 🔧 The `block-size: auto` that used to sit here is GONE. It existed only
     * to escape the base's hard height, and the base no longer has one — two
     * rules quietly claiming the same thing is how they drift apart. */
    min-block-size: var(--omx-control-height);
    padding-inline: 0;
    font-weight: var(--omx-font-weight-regular);
  }
  .omx-button--link:hover { text-decoration: underline; }
  .omx-button--link:active { --omx-button-fg: var(--omx-color-accent-active); }

  /* =========================================================================
   * SHAPES
   * ====================================================================== */

  .omx-button--icon {
    inline-size: var(--omx-control-height);   /* square */
    padding-inline: 0;
  }
  .omx-button--icon .omx-button__icon {
    inline-size: var(--omx-size-icon-action);
    block-size:  var(--omx-size-icon-action);
  }

  .omx-button--block {
    display: flex;
    inline-size: 100%;
  }

  /* An icon-only button below the touch minimum gets its hit area extended
   * rather than its visual size grown — a 24px compact toolbar stays compact
   * and stays operable. */
  @media (pointer: coarse) {
    .omx-button--icon { position: relative; }
    .omx-button--icon::after {
      content: "";
      position: absolute;
      inset: 50% auto auto 50%;
      translate: -50% -50%;
      min-inline-size: var(--omx-size-touch);
      min-block-size:  var(--omx-size-touch);
      inline-size: 100%;
      block-size: 100%;
    }
  }

  /* Windows High Contrast strips background colours, so variants become
   * indistinguishable. Give every button a real border back. */
  @media (forced-colors: active) {
    .omx-button { border-color: ButtonBorder; color: ButtonText; }
    .omx-button--primary { border-width: var(--omx-border-width-strong); }
  }

  @media print {
    .omx-button { display: none; }
  }
}

/* --- Primitives/Checkbox/Checkbox.css ------------------------------ */
/* =============================================================================
 * L1 · Primitives · Checkbox
 *
 * The native input is kept and restyled with `appearance: none`, rather than
 * hidden behind a decorative span. That keeps it focusable, keeps it in the
 * accessibility tree, keeps form autofill and reset working, and means the
 * :checked / :indeterminate / :disabled states come from the browser instead of
 * from JavaScript that can fall out of sync with the DOM.
 * ========================================================================== */

@layer omx.component {

  .omx-checkbox {
    /* The <label> is the root, so the box and the words are ONE hit target.
     * A dead zone between a checkbox and its caption is the most common form
     * defect there is, and this is the shape that makes it impossible. */
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
    gap: var(--omx-space-4);
    padding-block: var(--omx-space-2);
    cursor: pointer;
    /* Below the touch minimum the whole row still clears 44px. */
    min-block-size: var(--omx-space-8);
  }

  .omx-checkbox__box {
    appearance: none;
    grid-column: 1;
    grid-row: 1;
    inline-size: 16px;
    block-size: 16px;
    margin: 0;
    /* Optically centres the box on the first line of a 13px/1.5 label. */
    margin-block-start: 1px;

    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border-control);
    border-radius: var(--omx-radius-sm);
    cursor: inherit;

    transition:
      background-color var(--omx-duration-fast) var(--omx-easing-standard),
      border-color     var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-checkbox:hover .omx-checkbox__box:not(:disabled) {
    border-color: var(--omx-color-border-strong);
  }
  /* ⚠️ `accent`, NOT `border-control` — which is this control's RESTING border,
   * so the press would land on the colour it already had. ⭐ These three rest at
   * the DARKEST border token and LIGHTEN on hover, the opposite of the rule
   * Button.css states ("more contrasty than rest, never less"). Pressing toward
   * `accent` is also the colour the control takes when CHECKED, so the press
   * previews the state it is about to commit to. */
  .omx-checkbox:active .omx-checkbox__box:not(:disabled) { border-color: var(--omx-color-accent); }
  /* 🔴 AND THE CHECKED STATE NEEDS ITS OWN PRESS. A checked control already
   * rests at `accent`, so the rule above lands on the colour it had and the
   * press is invisible in exactly the state a person is in when they go to
   * UNCHECK something. Measured: rest #2660d8 -> active #2660d8. */
  .omx-checkbox:active .omx-checkbox__box:not(:disabled):checked,
  .omx-checkbox:active .omx-checkbox__box:not(:disabled)[data-omx-indeterminate="true"] {
    background: var(--omx-color-accent-active);
    border-color: var(--omx-color-accent-active);
  }

  .omx-checkbox__box:checked,
  .omx-checkbox__box[data-omx-indeterminate="true"] {
    background: var(--omx-color-accent);
    border-color: var(--omx-color-accent);
  }

  /* The tick and the dash are drawn by the sibling span, positioned over the
   * box, so the input itself never has to carry content. */
  .omx-checkbox__mark {
    grid-column: 1;
    grid-row: 1;
    inline-size: 16px;
    block-size: 16px;
    margin-block-start: 1px;
    pointer-events: none;
    display: grid;
    place-items: center;
  }

  .omx-checkbox__mark::before {
    content: "";
    inline-size: 9px;
    block-size: 5px;
    border: 2px solid var(--omx-color-accent-text);
    border-block-start: 0;
    border-inline-end: 0;
    rotate: -45deg;
    translate: 0 -1px;
    opacity: 0;
    transition: opacity var(--omx-duration-instant) var(--omx-easing-standard);
  }

  .omx-checkbox__box:checked + .omx-checkbox__mark::before { opacity: 1; }

  /* Indeterminate is a dash, not a tick — "some of the children", never "yes". */
  .omx-checkbox__box[data-omx-indeterminate="true"] + .omx-checkbox__mark::before {
    opacity: 1;
    inline-size: 9px;
    block-size: 0;
    border: 0;
    border-block-start: 2px solid var(--omx-color-accent-text);
    rotate: none;
    translate: none;
  }

  .omx-checkbox__text {
    grid-column: 2;
    display: grid;
    gap: var(--omx-space-1);
    min-inline-size: 0;
  }

  .omx-checkbox__label {
    font-size: var(--omx-font-size-body);
    line-height: var(--omx-line-height-body);
  }

  .omx-checkbox__hint {
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
    line-height: var(--omx-line-height-body);
  }

  .omx-checkbox__box[aria-invalid="true"] { border-color: var(--omx-color-danger-solid); }

  .omx-checkbox--disabled,
  .omx-checkbox__box:disabled { cursor: not-allowed; }
  .omx-checkbox--disabled { opacity: 0.65; }

  /* Focus ring comes from Foundation. The box is a real input, so it is already
   * covered by the :focus-visible rule there — nothing to add here. */

  @media (forced-colors: active) {
    .omx-checkbox__box { border-color: ButtonBorder; }
    .omx-checkbox__box:checked { background: Highlight; border-color: Highlight; }
    .omx-checkbox__mark::before { border-color: HighlightText; }
  }
}

/* --- Primitives/Chip/Chip.css -------------------------------------- */
/* =============================================================================
 * L1 · Primitives · Chip — a badge that DOES something.
 * ========================================================================== */

@layer omx.component {

  .omx-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-3);
    padding: var(--omx-space-2) var(--omx-space-4);

    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-medium);
    line-height: 1.45;
    white-space: nowrap;

    color: var(--omx-color-text);
    background: var(--omx-color-control);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-pill);

    transition:
      background-color var(--omx-duration-fast) var(--omx-easing-standard),
      border-color     var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-chip--removable { padding-inline-end: var(--omx-space-2); }

  .omx-chip__text { overflow: hidden; text-overflow: ellipsis; }
  .omx-chip__icon { inline-size: 12px; block-size: 12px; flex: 0 0 auto; fill: currentColor; }

  /* -- toggle ---------------------------------------------------------------
   * aria-pressed IS the state; there is no companion class to drift from it. */
  .omx-chip--toggle { cursor: pointer; }
  .omx-chip--toggle:hover:not(:disabled) { background: var(--omx-color-control-hover); }
  .omx-chip--toggle:active:not(:disabled) { background: var(--omx-color-control-active); }
  .omx-chip--toggle[aria-pressed="true"] {
    background: var(--omx-color-accent-subtle);
    border-color: var(--omx-color-accent);
    color: var(--omx-color-accent-hover);
  }
  .omx-chip--toggle:disabled { opacity: 0.65; cursor: not-allowed; }

  /* -- remove ---------------------------------------------------------------
   * Its own control, a SIBLING of the text, never nested inside a button.
   * A button inside a button is invalid HTML and the inner one stops receiving
   * clicks — which is why a removable chip is a span, not a button. */
  .omx-chip__remove {
    position: relative;
    display: grid;
    place-items: center;
    inline-size: 16px;
    block-size: 16px;
    flex: 0 0 auto;
    padding: 0;
    color: var(--omx-color-text-muted);
    background: none;
    border: 0;
    border-radius: var(--omx-radius-pill);
    cursor: pointer;
  }

  /* 16px visual, 44px hit area — a filter chip in a dense toolbar has to stay
   * small and has to stay tappable. */
  .omx-chip__remove::after {
    content: "";
    position: absolute;
    inset: 50% auto auto 50%;
    translate: -50% -50%;
    min-inline-size: var(--omx-size-touch);
    min-block-size: var(--omx-size-touch);
  }

  .omx-chip__remove:hover {
    color: var(--omx-color-danger-text);
    background: var(--omx-color-danger-bg);
  }
  .omx-chip__remove:active { background: var(--omx-color-danger-subtle); }

  .omx-chip__remove-icon { inline-size: 12px; block-size: 12px; }

  @media (forced-colors: active) {
    .omx-chip { border-color: ButtonBorder; }
    .omx-chip--toggle[aria-pressed="true"] { background: Highlight; color: HighlightText; }
  }
}

/* --- Primitives/Input/Input.css ------------------------------------ */
/* =============================================================================
 * L1 · Primitives · Input
 *
 * The bare control. Label, hint and error are L5 DataEntry/Field — keeping them
 * apart is what lets a table cell reuse this control without inheriting a form
 * row's layout.
 * ========================================================================== */

@layer omx.component {

  /* ⭐ A SCALED FIGURE IS A COLUMN, so it lines up like one: end-aligned and
     in tabular figures, where digit 3 of one row sits above digit 3 of the next.
     Proportional figures make a column of quantities impossible to scan, and
     that is the whole reason the scale was declared. */
  .omx-input--numeric {
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  /* \U0001f534 THE RESIDUAL. Not decoration — it is the figure that stops a rounded
     schedule silently failing to sum to its own balance.
     ⛔ Not a warning colour: nothing is wrong, something is merely not shown,
     and painting it red would train people to ignore it. */
  .omx-residual {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-3);
    margin: var(--omx-space-2) 0 0;
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-muted);
    font-variant-numeric: tabular-nums;
  }

  .omx-residual__label {
    font-variant-numeric: normal;
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-residual__exact { color: var(--omx-color-text-subtle); }

  @media print {
    /* A rounding difference is exactly the line an auditor needs off a
       photocopy. */
    .omx-residual { color: #000; }
  }

  .omx-input {
    --omx-input-bg:     var(--omx-color-surface);
    --omx-input-border: var(--omx-color-border-control);

    display: block;
    block-size: var(--omx-control-height);
    padding-inline: var(--omx-space-4);
    min-inline-size: 0;

    font: inherit;
    /* 16px under 768px — below that mobile Safari zooms the page on focus.
     * The token does the work; see tokens.css. */
    font-size: var(--omx-font-size-control);
    color: var(--omx-color-text);

    background: var(--omx-input-bg);
    border: var(--omx-border-width) solid var(--omx-input-border);
    border-radius: var(--omx-radius-md);

    transition:
      border-color var(--omx-duration-fast) var(--omx-easing-standard),
      box-shadow   var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-input--block { inline-size: 100%; }

  /* A control must read as a control before it is touched. The border is
   * deliberately `--omx-color-border-control`, darker than a panel divider —
   * a field that only appears on hover is a field people miss. */
  /* omx-no-press: a text field, as above — the border lift says editable, the press belongs to the caret */
  .omx-input:hover:not(:disabled):not([readonly]) {
    --omx-input-border: var(--omx-color-border-strong);
  }

  /* Focus ring is Foundation's (a11y.css) — it swaps the outline for a ring on
   * anything with [data-omx-control]. Nothing here overrides it. */

  .omx-input::placeholder {
    color: var(--omx-color-text-subtle);
    opacity: 1;                       /* Firefox dims placeholders by default */
  }

  .omx-input:disabled {
    --omx-input-bg: var(--omx-color-surface-sunken);
    color: var(--omx-color-text-subtle);
    cursor: not-allowed;
  }

  /* Read-only is not disabled: the value is still selectable and copyable,
   * which is the whole point of showing it. */
  .omx-input[readonly] {
    --omx-input-bg: var(--omx-color-surface-sunken);
    --omx-input-border: var(--omx-color-border);
    cursor: default;
  }

  /* No `.omx-input--invalid` companion class: `aria-invalid` is the state.
   * Two representations of one fact is two things to keep in sync. */
  .omx-input[aria-invalid="true"] {
    --omx-input-border: var(--omx-color-danger-solid);
  }
  .omx-input[aria-invalid="true"]:focus-visible {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--omx-color-danger-solid) 30%, transparent);
  }

  /* Numbers line up or they cannot be compared down a column. */
  .omx-input[inputmode="decimal"],
  .omx-input[inputmode="numeric"],
  .omx-input[data-omx-numeric] {
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  .omx-input[type="date"],
  .omx-input[type="time"],
  .omx-input[type="datetime-local"] {
    /* The native picker icon inherits the OS colour scheme; without this it is
     * a black glyph on a dark field. */
    color-scheme: inherit;
  }

  textarea.omx-input {
    block-size: auto;
    min-block-size: calc(var(--omx-control-height) * 2);
    padding-block: var(--omx-space-3);
    line-height: var(--omx-line-height-body);
    resize: vertical;
  }

  @media (forced-colors: active) {
    .omx-input { border-color: ButtonBorder; }
    .omx-input[aria-invalid="true"] { border-width: var(--omx-border-width-strong); }
  }

  @media print {
    /* A printed form shows its values, not its chrome. */
    .omx-input {
      border: 0;
      border-block-end: 1px solid #000;
      border-radius: 0;
      background: transparent;
      padding-inline: 0;
    }
  }
}

/* --- Primitives/Link/Link.css -------------------------------------- */
/* =============================================================================
 * L1 · Primitives · Link
 *
 * Most links need nothing: Foundation/reset.css already styles a bare <a>. This
 * file is for the three cases where a link has to SAY something extra about
 * where it goes — and all three are accessibility requirements, not decoration.
 * ========================================================================== */

@layer omx.component {

  .omx-link {
    color: var(--omx-color-text-link);
    text-decoration: none;
    border-radius: var(--omx-radius-xs);
  }
  .omx-link:hover { text-decoration: underline; }
  .omx-link:active { color: var(--omx-color-accent-active); }

  /* Inside a paragraph a link must be distinguishable WITHOUT colour — WCAG
   * 1.4.1, and the reason body links are underlined everywhere on the web. A
   * link in a toolbar or a nav is surrounded by other controls and does not
   * need it; a link mid-sentence does. */
  .omx-link--inline { text-decoration: underline; text-underline-offset: 2px; }

  /* ===========================================================================
   * LEAVING THE APP
   * A link that opens a new tab changes the user's context without warning,
   * which is a WCAG 3.2.5 concern. The icon is the visible signal and the
   * sr-only text is the announced one — BOTH, because either alone serves only
   * half the people.
   * ======================================================================== */
  .omx-link--external::after {
    content: "";
    display: inline-block;
    inline-size: 0.7em;
    block-size: 0.7em;
    margin-inline-start: 0.25em;
    vertical-align: baseline;
    background-color: currentColor;
    /* A mask, not an <img>: it inherits currentColor, so it is correct in both
     * themes and in every tone with no second asset. */
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M4.5 2h5.5v5.5M10 2 5 7M8 8.5v1.5H2V4h1.5' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain;
  }

  /* A download is also a context change, and a different one. */
  .omx-link--download::after {
    content: "";
    display: inline-block;
    inline-size: 0.75em;
    block-size: 0.75em;
    margin-inline-start: 0.25em;
    background-color: currentColor;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M6 1.5v6.5M3.5 5.5 6 8l2.5-2.5M2 10h8' fill='none' stroke='black' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain;
  }

  .omx-link--quiet { color: var(--omx-color-text); }
  .omx-link--quiet:hover { color: var(--omx-color-text-link); }
  /* 🔴 FOUND BY C20 ON ITS FIRST RUN. `.omx-link:active` (line 17) and
   * `.omx-link--quiet:hover` (above) have the SAME specificity — 0,2,0 — so
   * while a quiet link is pressed, and hover and active are both true, the
   * LATER rule wins and repaints it back to the hover colour.
   * ⇒ a quiet link had no press state at all, and the `:active` rule that was
   * supposed to cover it was present, matching, and losing on source order. */
  .omx-link--quiet:active { color: var(--omx-color-accent-active); }

  @media (forced-colors: active) {
    /* HCM drops background-color, and these icons ARE a background behind a
     * mask — so they vanish entirely. The sr-only text is what still works,
     * which is the reason it is never optional. */
    .omx-link--external::after,
    .omx-link--download::after { display: none; }
  }

  @media print {
    /* On paper a link's destination is invisible, so an external one prints its
     * href. Internal links are left alone: a page of inline URLs is unreadable
     * and the app's own routes mean nothing off-screen. */
    .omx-link--external::after { content: " (" attr(href) ")"; mask: none; background: none;
                                 inline-size: auto; block-size: auto;
                                 font-size: var(--omx-font-size-caption); }
  }
}

/* --- Primitives/Radio/Radio.css ------------------------------------ */
/* =============================================================================
 * L1 · Primitives · Radio
 *
 * Same construction as Checkbox: the native input kept and restyled with
 * `appearance: none`, so focus, the accessibility tree, form reset and the
 * :checked state all come from the browser rather than from script.
 * ========================================================================== */

@layer omx.component {

  .omx-radio-group {
    display: grid;
    gap: var(--omx-space-2);
    /* A fieldset carries browser default padding, margin and a border that
     * nothing in this design uses. */
    margin: 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
  }

  .omx-radio-group--inline {
    grid-auto-flow: column;
    justify-content: start;
    gap: var(--omx-space-8);
    align-items: start;
  }
  /* The legend must stay on its own row even when the options are in a column. */
  .omx-radio-group--inline > .omx-radio-group__legend,
  .omx-radio-group--inline > .omx-radio-group__hint { grid-column: 1 / -1; }

  .omx-radio-group__legend {
    /* A legend is not positioned like other elements; float:none + padding:0
     * puts it back into normal flow so the grid can lay it out. */
    float: none;
    padding: 0;
    margin-block-end: var(--omx-space-2);
  }

  .omx-radio-group__hint {
    margin-block-start: calc(var(--omx-space-2) * -1);
    margin-block-end: var(--omx-space-2);
  }

  .omx-radio {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
    gap: var(--omx-space-4);
    padding-block: var(--omx-space-2);
    min-block-size: var(--omx-space-8);
    cursor: pointer;
  }

  .omx-radio__dot {
    appearance: none;
    grid-column: 1;
    grid-row: 1;
    inline-size: 16px;
    block-size: 16px;
    margin: 0;
    margin-block-start: 1px;

    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border-control);
    /* Round, always. A square radio and a round checkbox are the two shapes
     * people read fastest, and swapping them costs a beat every time. */
    border-radius: var(--omx-radius-pill);
    cursor: inherit;

    transition: border-color var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-radio:hover .omx-radio__dot:not(:disabled) { border-color: var(--omx-color-border-strong); }
  /* ⚠️ `accent`, NOT `border-control` — which is this control's RESTING border,
   * so the press would land on the colour it already had. ⭐ These three rest at
   * the DARKEST border token and LIGHTEN on hover, the opposite of the rule
   * Button.css states ("more contrasty than rest, never less"). Pressing toward
   * `accent` is also the colour the control takes when CHECKED, so the press
   * previews the state it is about to commit to. */
  .omx-radio:active .omx-radio__dot:not(:disabled) { border-color: var(--omx-color-accent); }
  /* 🔴 AND THE CHECKED STATE NEEDS ITS OWN PRESS. A checked control already
   * rests at `accent`, so the rule above lands on the colour it had and the
   * press is invisible in exactly the state a person is in when they go to
   * UNCHECK something. Measured: rest #2660d8 -> active #2660d8. */
  .omx-radio:active .omx-radio__dot:not(:disabled):checked { border-color: var(--omx-color-accent-active); }

  .omx-radio__dot:checked { border-color: var(--omx-color-accent); border-width: 5px; }

  .omx-radio__mark {
    grid-column: 1;
    grid-row: 1;
    inline-size: 16px;
    block-size: 16px;
    margin-block-start: 1px;
    pointer-events: none;
  }

  .omx-radio__text  { grid-column: 2; display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-radio__label { font-size: var(--omx-font-size-body); line-height: var(--omx-line-height-body); }
  .omx-radio__hint  { font-size: var(--omx-font-size-caption); color: var(--omx-color-text-muted); }

  .omx-radio__dot[aria-invalid="true"] { border-color: var(--omx-color-danger-solid); }

  .omx-radio--disabled { opacity: 0.65; cursor: not-allowed; }
  .omx-radio__dot:disabled { cursor: not-allowed; }

  @media (forced-colors: active) {
    .omx-radio__dot { border-color: ButtonBorder; }
    .omx-radio__dot:checked { border-color: Highlight; }
  }
}

/* --- Primitives/Select/Select.css ---------------------------------- */
/* =============================================================================
 * L1 · Primitives · Select — a native <select>, styled.
 * ========================================================================== */

@layer omx.component {

  .omx-select {
    --omx-select-bg:     var(--omx-color-surface);
    --omx-select-border: var(--omx-color-border-control);

    display: block;
    block-size: var(--omx-control-height);
    /* Room on the end for the chevron. Logical so it flips in RTL. */
    padding-inline: var(--omx-space-4) var(--omx-space-9);
    min-inline-size: 0;

    font: inherit;
    font-size: var(--omx-font-size-control);
    color: var(--omx-color-text);

    /* Both are required, and for different reasons: `background-color` because
     * Windows dark mode otherwise paints a native select with OS colours, and
     * `color` because the OS then also picks the text colour. Setting one and
     * not the other produces black-on-black. */
    background-color: var(--omx-select-bg);

    border: var(--omx-border-width) solid var(--omx-select-border);
    border-radius: var(--omx-radius-md);
    cursor: pointer;

    /* The native arrow cannot be recoloured, so it is replaced. `currentColor`
     * in an inline SVG keeps it correct in both themes with no second asset. */
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%235d6d86' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--omx-space-4) center;
    background-size: 12px;

    transition:
      border-color var(--omx-duration-fast) var(--omx-easing-standard),
      box-shadow   var(--omx-duration-fast) var(--omx-easing-standard);
  }

  /* RTL: the chevron follows the text direction. */
  .omx-ui:dir(rtl) .omx-select { background-position: left var(--omx-space-4) center; }

  .omx-select--block { inline-size: 100%; }

  .omx-select:hover:not(:disabled) { --omx-select-border: var(--omx-color-border-strong); }
  /* ⚠️ NOT `border-control` — that is this control's RESTING border, so a press
   * would land on the colour it already had. ⭐ And note hover here runs the
   * wrong way: rest is #7c8aa0 and hover LIGHTENS to #adbbcd, against Button's
   * own stated rule that a state must be more contrasty than rest, never less.
   * The press is therefore the accent: pressing a select opens its list. */
  .omx-select:active:not(:disabled) { --omx-select-border: var(--omx-color-accent); }

  .omx-select:disabled {
    --omx-select-bg: var(--omx-color-surface-sunken);
    color: var(--omx-color-text-subtle);
    cursor: not-allowed;
  }

  .omx-select[aria-invalid="true"] { --omx-select-border: var(--omx-color-danger-solid); }

  /* The option list is painted by the OS, not by us. Setting these two is the
   * only influence we have, and without them a dark theme yields a white popup
   * with white text on Windows. */
  .omx-select option {
    background-color: var(--omx-color-surface);
    color: var(--omx-color-text);
  }

  /* The empty choice reads as a prompt, not as a value. */
  .omx-select:has(option[value=""]:checked) { color: var(--omx-color-text-subtle); }

  /* 🔴 FIXED 2026-09-19 — the system branch was NOT inside the media query,
   * so this dark rule also matched the DEFAULT, UNSTAMPED state and applied
   * on a light page. Three states, not two: explicit dark, explicit light,
   * and system — only `@media (prefers-color-scheme: dark)` separates the
   * third. Five files had this; tokens.css and Brand.css did not. */
  .omx-ui[data-theme="dark"] .omx-select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%239aa8bd' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-select{
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%239aa8bd' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    }
  }

  @media (forced-colors: active) {
    .omx-select { border-color: ButtonBorder; }
    /* The data-URI chevron is dropped entirely in HCM; restore the native one. */
    .omx-select { appearance: auto; background-image: none; padding-inline-end: var(--omx-space-4); }
  }

  @media print {
    .omx-select {
      appearance: none; background-image: none;
      border: 0; border-block-end: 1px solid #000; border-radius: 0;
      padding-inline: 0;
    }
  }
}

/* --- Primitives/Spinner/Spinner.css -------------------------------- */
/* =============================================================================
 * L1 · Primitives · Spinner
 *
 * ⚠️ Rarely the right answer on its own. A spinner says "something is
 * happening" and nothing more. L6 Skeleton says WHAT is happening and holds the
 * layout, so the page does not jump when content lands. Reach for this only
 * when the wait is short and the shape of the result is genuinely unknown.
 * ========================================================================== */

@layer omx.component {

  .omx-spinner {
    display: inline-block;
    inline-size: var(--omx-size-icon-action);
    block-size: var(--omx-size-icon-action);
    border: 2px solid currentColor;
    border-block-start-color: transparent;
    border-radius: var(--omx-radius-pill);
    opacity: 0.75;
    animation: omx-spinner-spin 620ms linear infinite;
  }

  .omx-spinner--sm {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
  }

  .omx-spinner--lg {
    inline-size: var(--omx-size-icon-feature);
    block-size: var(--omx-size-icon-feature);
    border-width: 3px;
  }

  @keyframes omx-spinner-spin { to { rotate: 360deg; } }

  /* Under prefers-reduced-motion, a11y.css clamps this to a single 1ms
   * iteration — so it becomes a STATIC ring rather than a spinner running
   * invisibly fast. A static ring on its own communicates nothing, which is
   * why the block below always carries a label and a live region. */
  .omx-spinner-block {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-5);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-body);
  }
}

/* --- Primitives/Switch/Switch.css ---------------------------------- */
/* =============================================================================
 * L1 · Primitives · Switch
 *
 * A real checkbox input with role="switch", visually hidden but NOT display:none
 * — it stays focusable and in the accessibility tree, and the track reacts to
 * its :checked / :focus-visible / :disabled states through sibling selectors.
 * ========================================================================== */

@layer omx.component {

  .omx-switch {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: start;
    gap: var(--omx-space-6);
    padding-block: var(--omx-space-2);
    min-block-size: var(--omx-space-8);
    cursor: pointer;
  }

  /* Label leading (default) puts the control on the trailing edge, which is the
   * settings-list convention: read the setting, then reach for the control. */
  .omx-switch__text  { grid-column: 1; display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-switch__track { grid-column: 2; }

  .omx-switch--label-last .omx-switch__text  { grid-column: 2; }
  .omx-switch--label-last .omx-switch__track { grid-column: 1; }
  .omx-switch--label-last { grid-template-columns: auto 1fr; }

  .omx-switch__label {
    font-size: var(--omx-font-size-body);
    line-height: var(--omx-line-height-body);
  }
  .omx-switch__hint {
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
    line-height: var(--omx-line-height-body);
  }

  /* Clipped, not hidden. `display:none` or `visibility:hidden` would take the
   * input out of the tab order and out of the accessibility tree, and then the
   * switch would need a JS keyboard handler that reimplements Space badly. */
  .omx-switch__input {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    padding: 0; margin: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .omx-switch__track {
    position: relative;
    display: block;
    inline-size: 32px;
    block-size: 18px;
    margin-block-start: 2px;
    background: var(--omx-color-control-active);
    border: var(--omx-border-width) solid var(--omx-color-border-control);
    border-radius: var(--omx-radius-pill);
    transition:
      background-color var(--omx-duration-fast) var(--omx-easing-standard),
      border-color     var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-switch__thumb {
    position: absolute;
    inset-block-start: 2px;
    inset-inline-start: 2px;
    inline-size: 12px;
    block-size: 12px;
    background: var(--omx-color-surface-raised);
    border-radius: var(--omx-radius-pill);
    box-shadow: var(--omx-shadow-xs);
    /* `translate` rather than `inset-inline-start` — a compositor property, so
     * the thumb never triggers layout. */
    transition: translate var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-switch__input:checked + .omx-switch__track {
    background: var(--omx-color-accent);
    border-color: var(--omx-color-accent);
  }
  .omx-switch__input:checked + .omx-switch__track .omx-switch__thumb {
    translate: 14px 0;
  }
  /* RTL: "on" is still the far side, whichever side that is. */
  .omx-ui:dir(rtl) .omx-switch__input:checked + .omx-switch__track .omx-switch__thumb {
    translate: -14px 0;
  }

  /* The input is clipped, so Foundation's focus ring lands on something 1px
   * wide and invisible. The ring is forwarded to the track instead — the only
   * place in this package where a component restates the focus style, and it is
   * restated with the same token, not a new one. */
  .omx-switch__input:focus-visible + .omx-switch__track {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
  }

  .omx-switch:hover .omx-switch__input:not(:disabled):not(:checked) + .omx-switch__track {
    border-color: var(--omx-color-border-strong);
  }
  /* ⚠️ `accent`, NOT `border-control` — which is this control's RESTING border,
   * so the press would land on the colour it already had. ⭐ These three rest at
   * the DARKEST border token and LIGHTEN on hover, the opposite of the rule
   * Button.css states ("more contrasty than rest, never less"). Pressing toward
   * `accent` is also the colour the control takes when CHECKED, so the press
   * previews the state it is about to commit to. */
  /* 🔴 AND `:not(:checked)` IS LOAD-BEARING — the hover rule above has it and I
   * dropped it first time round. Without it, pressing an ON switch turned its
   * accent track border grey: the control reading as LESS engaged at the exact
   * moment it was being pressed. Measured: #2660d8 -> #7c8aa0. */
  .omx-switch:active .omx-switch__input:not(:disabled):not(:checked) + .omx-switch__track { border-color: var(--omx-color-accent); }
  /* 🔴 AND THE CHECKED STATE NEEDS ITS OWN PRESS. A checked control already
   * rests at `accent`, so the rule above lands on the colour it had and the
   * press is invisible in exactly the state a person is in when they go to
   * UNCHECK something. Measured: rest #2660d8 -> active #2660d8. */
  .omx-switch:active .omx-switch__input:not(:disabled):checked + .omx-switch__track {
    background: var(--omx-color-accent-active);
    border-color: var(--omx-color-accent-active);
  }

  .omx-switch--disabled { opacity: 0.65; cursor: not-allowed; }

  @media (forced-colors: active) {
    .omx-switch__track { border-color: ButtonBorder; background: ButtonFace; }
    .omx-switch__input:checked + .omx-switch__track { background: Highlight; }
    .omx-switch__thumb { background: ButtonText; }
    .omx-switch__input:checked + .omx-switch__track .omx-switch__thumb { background: HighlightText; }
  }
}

/* --- Primitives/Text/Text.css -------------------------------------- */
/* =============================================================================
 * L1 · Primitives · Text — the type ramp, as classes.
 *
 * These sit in @layer omx.utility, ABOVE omx.component, because a utility has
 * to be able to beat a component. That is the entire reason a utility exists:
 * the caller has already chosen the component and is now correcting one
 * property on one instance.
 *
 * ⛔ There is no `.omx-text-red` and no `.omx-text-18`. A utility named for a
 * literal is a hardcoded value with a class around it. Every class below
 * resolves to a token.
 * ========================================================================== */

@layer omx.utility {

  /* ⭐ OWNER RULING 2026-09-19 — "professional and exclusive not traditional".
   * The brand face carries the TOP of the ramp and nothing else.
   *
   * 🔴 THE PACKAGE HAD A BRAND AND DID NOT USE IT: two woff2 files shipped to set
   * ONE piece of text (the sidebar wordmark), while everything a person saw was
   * Inter plus a safe blue — the default SaaS admin palette, which is exactly why
   * it read as traditional.
   * ⭐⭐ AND tokens.css HAD ALREADY PROMISED THIS. Its deviation note rejects a
   * distinctive CDN face for on-prem and hinting reasons — both still correct —
   * and justifies it with "Distinctiveness lives in the brand face and the
   * palette instead." The deviation was taken; the compensation was never built.
   *
   * ⛔ BODY, LABEL, CAPTION, CONTROLS AND EVERY TABLE CELL STAY Inter. A dense
   * grid read for eight hours needs hinting and tabular figures, not personality.
   * Nobody reads a page title for eight hours — that is where a product signs its
   * name, and it is the only place this face belongs.
   * ⚠️ Poppins ships 600 and 700 ONLY. Every step below uses `semibold` (600), so
   * there is no weight that falls back to Inter mid-ramp. Verified before applying;
   * adding a step at 400 or 500 here would silently break the ramp. */
  .omx-text-display {
    font-family: var(--omx-font-display);
    font-size: var(--omx-font-size-display);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    letter-spacing: var(--omx-letter-spacing-tight);
    text-wrap: balance;
  }

  /* ⛔⛔ THE METRIC STEP DOES **NOT** TAKE THE BRAND FACE, AND THAT IS MEASURED.
   * The ruling named "metric, display and the 22px step". I put the face on all
   * three, then probed the shipped woff2 digit by digit at 32px:
   *
   *     Inter,   tabular-nums   digit-width spread  0.000px   ✅
   *     Poppins, normal         digit-width spread  9.575px
   *     Poppins, tabular-nums   digit-width spread  9.575px   ← IDENTICAL
   *
   * ⇒ THE SUBSET HAS NO `tnum` FEATURE. `font-variant-numeric: tabular-nums`
   * becomes a silent no-op, "1" renders 11.6px wide against "4" at 21.2px, and
   * a column of figures stops lining up — while the CSS still reads as though
   * it aligns. `KpiCard.css` calls tabular figures "the only reason they are in
   * a row", and it is right.
   * ⭐ THE RULING'S OWN REASON EXCLUDES IT. It rests on "a dense grid needs
   * hinting and tabular figures, not personality" — and a KPI figure is a
   * number read for comparison, not a title read once. The face goes on the
   * steps a person READS, never the ones they COMPARE.
   * ⛔ `.omx-kpi__figure` is the real metric surface in this package and is
   * excluded for the same reason. `check-omx` C22 enforces both. */
  .omx-text-metric {
    font-size: var(--omx-font-size-metric);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    font-variant-numeric: tabular-nums;
  }

  .omx-text-title {
    font-size: var(--omx-font-size-title);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    text-wrap: balance;
  }

  .omx-text-lead {
    font-size: var(--omx-font-size-lead);
    line-height: var(--omx-line-height-body);
  }

  .omx-text-body {
    font-size: var(--omx-font-size-body);
    line-height: var(--omx-line-height-body);
  }

  .omx-text-label {
    font-size: var(--omx-font-size-label);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-muted);
  }

  .omx-text-caption {
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-subtle);
  }

  /* All-caps needs the extra tracking or it reads as one solid block. */
  .omx-text-overline {
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    color: var(--omx-color-text-muted);
  }

  .omx-text-muted  { color: var(--omx-color-text-muted); }
  .omx-text-subtle { color: var(--omx-color-text-subtle); }
  .omx-text-mono   { font-family: var(--omx-font-mono); font-size: 0.95em; }

  /* Money and quantities. Right-aligned AND tabular — either one alone leaves a
   * column you still cannot scan down. */
  .omx-text-numeric {
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  /* A negative figure is not "red". Colour is the secondary cue; the caller
   * still writes the sign, so it survives monochrome print and a colour-blind
   * reader. WCAG 1.4.1. */
  .omx-text-negative { color: var(--omx-color-danger-text); }

  /* Prose needs a measure. Past roughly 75 characters the eye loses the line. */
  .omx-text-measure { max-inline-size: var(--omx-measure); }

  /* One line, then an ellipsis. `min-inline-size: 0` is here because a flex or
   * grid child defaults to min-content width and simply refuses to shrink —
   * without it nothing truncates and the layout overflows instead. */
  .omx-text-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-inline-size: 0;
  }

  .omx-text-clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
  .omx-text-clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }

  /* A long identifier — an account code, a file name — breaks rather than
   * pushing the whole layout sideways. */
  .omx-text-break { overflow-wrap: anywhere; }
}

/* --- Primitives/Textarea/Textarea.css ------------------------------ */
/* =============================================================================
 * L1 · Primitives · Textarea — many lines of text.
 *
 * The box, the border, the focus ring and `resize: vertical` all come from
 * `textarea.omx-input` in Input.css, which predates this component. What is
 * here is only what a textarea needs and an input does not: growing with its
 * content, and a ceiling on that growth.
 * ========================================================================== */

@layer omx.component {

  .omx-textarea {
    /* `field-sizing: content` is the whole auto-grow feature, in one line and
     * with no script: the control sizes to what is typed, bounded by rows and
     * max-block-size. Browsers without it fall back to the `rows` floor plus
     * the resize handle, which is exactly the behaviour every textarea had
     * before — so nothing is lost where it is missing. */
    field-sizing: content;

    /* The ceiling. Past this a long note would push the buttons that submit it
     * off the bottom of the screen, and a person would be typing into a form
     * whose Save they can no longer see. */
    max-block-size: calc(
      var(--omx-textarea-max-rows, 12) * 1em * var(--omx-line-height-body)
      + var(--omx-space-3) * 2
    );
    overflow-y: auto;
    overscroll-behavior: contain;

    /* ⛔ `resize: none` is NOT set, deliberately. Suppressing the handle takes
     * away the one control a person has over a box too small for what they were
     * asked to write. Only the horizontal axis is pinned (by Input.css's
     * `resize: vertical`), because a textarea wider than its form breaks the
     * layout around it. */
  }

  @media print {
    /* On paper the content is the point; a scroll box prints one screenful and
     * silently drops the rest. */
    .omx-textarea {
      max-block-size: none;
      overflow: visible;
      field-sizing: content;
      border-color: #000;
    }
  }
}

/* --- Layout/Cluster/Cluster.css ------------------------------------ */
/* =============================================================================
 * L2 · Layout · Cluster — a row that always wraps.
 * ========================================================================== */

@layer omx.layout {

  .omx-cluster {
    --omx-cluster-gap: var(--omx-space-5);

    display: flex;
    flex-wrap: wrap;           /* never optional — see ClusterTagHelper */
    align-items: center;
    gap: var(--omx-cluster-gap);
    min-inline-size: 0;
  }

  .omx-cluster--center  { justify-content: center; }
  .omx-cluster--end     { justify-content: flex-end; }
  .omx-cluster--between { justify-content: space-between; }

  .omx-cluster--align-start    { align-items: flex-start; }
  .omx-cluster--align-end      { align-items: flex-end; }
  /* Baseline is what a label and its value want: their text sits on one line
   * even when the two have different font sizes. */
  .omx-cluster--align-baseline { align-items: baseline; }

  /* A cluster child that holds text must be allowed to shrink, or the whole row
   * refuses to wrap and overflows instead. This is the flexbox default that
   * surprises everyone: min-width is `auto`, which means min-content. */
  .omx-cluster > * { min-inline-size: 0; }
}

/* --- Layout/Grid/Grid.css ------------------------------------------ */
/* =============================================================================
 * L2 · Layout · Grid — columns that collapse on the CONTAINER's width.
 *
 * ⭐⭐ Every step below is a @container query, never a media query. A KPI grid
 * in a 300px side panel on a 1920px monitor must show one column, and a media
 * query cannot know that — it only knows the window is wide. That mistake in
 * the first RCL is the reason this file exists in this shape.
 * ========================================================================== */

@layer omx.layout {

  .omx-grid {
    --omx-grid-gap: var(--omx-space-6);
    --omx-grid-min: 220px;

    display: grid;
    gap: var(--omx-grid-gap);
    min-inline-size: 0;

    /* Its own container, so nested grids each answer for their own width. */
    container-type: inline-size;
    container-name: omx-grid;
  }

  .omx-grid--top { align-items: start; }

  /* -- auto mode: as many columns as fit --------------------------------------
   * `min(Npx, 100%)` is load-bearing: without the inner min(), a container
   * narrower than --omx-grid-min loses the argument and the page scrolls
   * sideways. With it, a single column simply shrinks. */
  .omx-grid--auto {
    grid-template-columns: repeat(auto-fit, minmax(min(var(--omx-grid-min), 100%), 1fr));
  }

  /* -- fixed mode: an exact count that steps DOWN -----------------------------
   * The ladder is monotonic on purpose: 6 → 4 → 3 → 2 → 1, never back up. A
   * non-monotonic ladder means dragging a window narrower can ADD a column,
   * which reads as a bug even when it is intentional. */
  .omx-grid--cols-1 { grid-template-columns: minmax(0, 1fr); }
  .omx-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .omx-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .omx-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .omx-grid--cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .omx-grid--cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }

  /* The steps are keyed off the SLOT the grid sits in (omx-slot, set by every
   * Region and every Panel body), because a grid cannot query itself — a
   * container query element cannot be its own query target. */
  @container omx-slot (max-width: 1100px) {
    .omx-grid--cols-6 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .omx-grid--cols-5 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }
  @container omx-slot (max-width: 860px) {
    .omx-grid--cols-6,
    .omx-grid--cols-5,
    .omx-grid--cols-4 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  }
  @container omx-slot (max-width: 620px) {
    .omx-grid--cols-6,
    .omx-grid--cols-5,
    .omx-grid--cols-4,
    .omx-grid--cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  @container omx-slot (max-width: 380px) {
    .omx-grid--cols { grid-template-columns: minmax(0, 1fr); }
  }

  /* A grid child holding text must be allowed to shrink below its content. */
  .omx-grid > * { min-inline-size: 0; }

  @media print {
    /* Print has no container queries in any engine worth relying on, so the
     * ladder is replaced with a flat two-up. */
    .omx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
  }
}

/* --- Layout/Page/Page.css ------------------------------------------ */
/* =============================================================================
 * L2 · Layout · Page + Region
 *
 * The archetype frame. Each archetype gets its region layout HERE, once, so a
 * module never lays out a page — it names one and fills the slots.
 *
 * 🔴 TWO ELEMENTS, ON PURPOSE.
 *   .omx-page        declares the container. It carries NO grid.
 *   .omx-page__body  carries the grid and is what the container query targets.
 *
 * A container query matches DESCENDANTS of the container, never the container
 * element itself. The first version of this file put the region grid on
 * `.omx-page` and queried `@container omx-page` — which matched nothing, at any
 * width, silently. Grid.css states this constraint in a comment; this file broke
 * it one file later, which is why `check-omx` C13 now enforces it.
 *
 * ⚠️ Overlays still do NOT belong inside a page — but for a different reason
 * than this file used to give. See the correction block below.
 * ========================================================================== */

/* 🔴 CORRECTION 2026-09-12 — MEASURED, against a claim I had written here.
 *
 * This file previously said `container-type: inline-size` implies
 * `contain: layout` and therefore makes the element a containing block for
 * `position: fixed`. **That is false**, and it was used as the justification
 * for a design decision. Measured in Chrome, three probes:
 *
 *   inside .omx-panel__body (container-type)  ->  0,0 942x526  (the viewport)
 *   inside .omx-page        (container-type)  ->  0,0 942x526  (the viewport)
 *   inside an explicit contain:layout box     ->  50,280 300x100  (TRAPPED)
 *
 * and `getComputedStyle(el).contain` reads **"none"** on every container-type
 * element. So `container-type: inline-size` applies inline-size and style
 * containment but NOT layout containment; a fixed child still resolves against
 * the viewport. Only an explicit `contain: layout` traps it — the control above
 * proves the mechanism is real, just not the one I attributed it to.
 *
 * ⭐ The <dialog> decision STANDS, on the reasons that survive measurement:
 * the top layer is painted above every stacking context and outside every
 * `overflow: hidden` ancestor (a fixed div is not), and showModal() supplies
 * the focus trap, the inert background, Escape and focus restoration.
 * ⭐⭐ The lesson is the one this estate keeps relearning: a spec claim carried
 * in from memory is not a measurement, and it reads exactly like one once it
 * is written down in a comment.
 */

@layer omx.layout {

  .omx-page {
    padding: var(--omx-panel-pad);
    min-inline-size: 0;

    /* The page is a container so its regions can query the PAGE, not the
     * window. The shell's sidebar sits outside this element, which is exactly
     * why the window is the wrong thing to ask. */
    container-type: inline-size;
    container-name: omx-page;
  }

  .omx-page__body {
    --omx-page-gap: var(--omx-stack-gap);

    display: grid;
    gap: var(--omx-page-gap);
    grid-template-columns: minmax(0, 1fr);
    min-inline-size: 0;
  }

  .omx-page--narrow {
    max-inline-size: 1100px;
    margin-inline: auto;
  }

  /* ===========================================================================
   * REGION — a named slot.
   * ⭐ Every region is its own container. Whatever is dropped in sizes itself
   * against the SLOT. The same table in a 280px aside and a 1100px main area is
   * one component behaving correctly in both, with no caller involvement.
   * ======================================================================== */
  .omx-region {
    min-inline-size: 0;   /* without this a grid child refuses to shrink */
    container-type: inline-size;
    container-name: omx-slot;
  }

  .omx-region[data-omx-scroll] { min-block-size: 0; }

  /* ===========================================================================
   * ARCHETYPE LAYOUTS
   * One block per shape. A module that names an archetype gets its region
   * arrangement for free — and a page whose markup does not match its declared
   * archetype becomes visible rather than merely wrong.
   * ======================================================================== */

  /* -- main + aside: record-detail · entity-form · approval-decision --------- */
  .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body,
  .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body,
  .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body {
    grid-template-areas:
      "header"
      "main"
      "aside"
      "footer";
  }

  .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body > [data-omx-slot="header"],
  .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body > [data-omx-slot="header"],
  .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body > [data-omx-slot="header"] { grid-area: header; }

  .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body > [data-omx-slot="main"],
  .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body > [data-omx-slot="main"],
  .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body > [data-omx-slot="main"]   { grid-area: main; }

  .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body > [data-omx-slot="aside"],
  .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body > [data-omx-slot="aside"],
  .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body > [data-omx-slot="aside"]  { grid-area: aside; }

  .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body > [data-omx-slot="footer"],
  .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body > [data-omx-slot="footer"],
  .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body > [data-omx-slot="footer"] { grid-area: footer; }

  /* 🔴 A CONTAINER query, not a media query. The page is narrow when the PAGE is
   * narrow — which on a 1920px monitor with an open sidebar and a drawer is a
   * completely different moment from when the window is narrow. */
  /* 🔴 `:has()` IS LOAD-BEARING — WITHOUT IT THIS RESERVED A COLUMN FOR AN ASIDE
   * THAT WAS NOT THERE. `aside` is OPTIONAL on all three of these archetypes
   * (Archetype.cs says so), so a page is entirely within its rights to omit it
   * — and this rule used to hand that page a 352px empty track anyway.
   *
   * Measured at a 1600px viewport, before the guard:
   *   /gallery/dataentry      cols "937px 352px", NO aside element -> 368px dead
   *   /gallery/collaboration  cols "937px 352px", NO aside element -> 368px dead
   *
   * ⚠️ This is a LIBRARY defect, not a page one: every consumer module writing
   * an entity-form without a sidebar got the same hole, and the page has no way
   * to opt out of a grid it never asked for.
   *
   * ⭐ The two-column layout is a consequence of HAVING an aside, so the rule
   * has to ask whether one exists rather than assume the archetype implies it.
   * With no aside the base single-column rule above still applies, which is the
   * correct full-width stack. */
  @container omx-page (min-width: 880px) {
    .omx-page[data-omx-archetype="record-detail"]     > .omx-page__body:has(> [data-omx-slot="aside"]),
    .omx-page[data-omx-archetype="entity-form"]       > .omx-page__body:has(> [data-omx-slot="aside"]),
    .omx-page[data-omx-archetype="approval-decision"] > .omx-page__body:has(> [data-omx-slot="aside"]) {
      grid-template-columns: minmax(0, 1fr) minmax(260px, 22rem);
      grid-template-areas:
        "header header"
        "main   aside"
        "footer footer";
    }
  }

  /* -- a single readable column: wizard-import ------------------------------
   * A wizard is a sequence of questions, and a question spread over 1600px is a
   * question people lose their place in.
   *
   * ⚠️ CORRECTION to what this comment used to say. It claimed the cap was safe
   * "because a wizard has no aside". That is FALSE — `Archetype.cs` lists
   * `aside` as an OPTIONAL region of wizard-import, and the gallery's own demo
   * route renders one. Measured: the aside STACKS BENEATH main at the same
   * 543px, which is the right outcome for a wizard (a column, with a side panel
   * under it) — so the RULE is correct and only its stated reason was wrong.
   *
   * The real reason the cap is safe here: wizard-import is NOT in the two-column
   * group below, so capping the body caps a single column. On entity-form, which
   * IS, the same cap crushed a two-column grid to 468px and left main 100px wide.
   * ⭐ The SAME rule is right for one shape and wrong for the other, and what
   * decides it is the number of columns — not the presence of an aside.
   *
   * 🔧 Moved here from Archetypes.css, which writes into a later layer and so
   * was overriding this file's own archetype rules. */
  .omx-page[data-omx-archetype="wizard-import"] > .omx-page__body {
    max-inline-size: 72ch;
    margin-inline: auto;
  }

  /* -- the wall: display-board ----------------------------------------------
   * 🔴 A BOARD IS NOT A NARROW PAGE, IT IS A FULL ONE. It fills the panel edge
   * to edge, because a 1920px screen seen from twelve metres has no room for a
   * reading measure -- the constraint that makes every other page readable makes
   * this one unreadable.
   *
   * ⭐ The type scale is lifted on the PAGE, not per component, so anything
   * dropped onto a board inherits it. A board built from components that each
   * had to remember to be large would be one forgotten component away from an
   * unreadable wall. */
  .omx-page[data-omx-archetype="display-board"] {
    /* ⛔ THE TYPE SCALE IS NOT SET HERE. `--omx-text-scale` is a global token,
     * and C1 rejects redefining one outside tokens.css — correctly: a second
     * definition is a second source of truth that drifts the first time the
     * global moves. The board layout instead sets `data-text-scale="board"` on
     * `.omx-ui`, and tokens.css owns what that means, beside "compact" and
     * "large". One mechanism, one place, no new concept. */
    block-size: 100%;
  }

  .omx-page[data-omx-archetype="display-board"] > .omx-page__body {
    display: grid;
    gap: var(--omx-space-7);
    /* ⛔ No max-inline-size. See above. */
    inline-size: 100%;
    /* ⭐ THE ROWS SPREAD DOWN THE PANEL. `align-content: space-between` was first
     * put on the stage -- which holds exactly ONE child, so it distributed
     * nothing and every row stayed bunched at the top with the lower third
     * empty. On a wall that is not cosmetic: TARGET 1,200 sat directly above
     * CUT 9,950 and the two read as a single column of related numbers. */
    /* ⭐ ONE ROW THAT FILLS, and the REGION does the spreading below.
     * 🔴 THREE WRONG VERSIONS BEFORE THIS ONE, all the same mistake at a
     * different level: `align-content: space-between` was put first on the
     * stage, then here -- and BOTH have exactly ONE child, so it distributed
     * nothing. Measured chain: stage 720 > page 680 > body 648 (1 child) >
     * REGION 391 (4 children). The rows live in the region, so that is where
     * they have to be spread. */
    block-size: 100%;
    grid-template-rows: 1fr;
  }

  /* The rows of a board spread down the panel. ⚠️ On the REGION, because that is
   * the element that actually holds them. */
  .omx-page[data-omx-archetype="display-board"] .omx-region[data-omx-slot="main"] {
    display: grid;
    block-size: 100%;
    gap: var(--omx-space-6);
    align-content: space-between;
  }

  /* -- one centred card, no chrome: auth-screen · system-state -------------- */
  .omx-page[data-omx-archetype="auth-screen"],
  .omx-page[data-omx-archetype="system-state"] {
    display: grid;
    /* 🔴🔴 `place-content: center` COLLAPSED THE PAGE TO ZERO WIDTH.
     * It is shorthand for align-content + JUSTIFY-CONTENT, and
     * `justify-content: center` sizes the column track to its CONTENT. The body
     * below asks for `min(420px, 100%)`, and that `100%` then resolves against a
     * track whose width depends on the body — circular, and the browser settles
     * it at 0. Measured at 1900px: `.omx-page__body` w=0, cols `0px`. The whole
     * page rendered as nothing but its own padding.
     *
     * ⭐ `align-content` alone centres VERTICALLY and leaves the track full
     * width; `justify-items` then centres the card INSIDE it, so the body's
     * `100%` has something real to resolve against. */
    align-content: center;
    justify-items: center;
    min-block-size: 100%;
    padding: var(--omx-space-11) var(--omx-panel-pad);
  }
  .omx-page[data-omx-archetype="auth-screen"]  > .omx-page__body,
  .omx-page[data-omx-archetype="system-state"] > .omx-page__body {
    inline-size: min(420px, 100%);
    /* ⛔ NOT `justify-items: center` here. That sizes each grid ITEM to its own
     * content instead of stretching it, and a region is `min-inline-size: 0`
     * by design — so the region collapsed to 0 INSIDE a correctly-sized 420px
     * body. The card is already centred by the page's own `justify-items`;
     * inside the card, the region must fill it. */
    justify-items: stretch;
  }

  @media print {
    .omx-page { padding: 0; }
    .omx-page__body {
      gap: var(--omx-space-6);
      /* A printed page has no viewport, so an aside must fall inline rather
       * than hold a column with nothing to sit beside it. */
      grid-template-columns: minmax(0, 1fr) !important;
      grid-template-areas: none !important;
    }
    .omx-page__body > .omx-region { grid-area: auto !important; }
  }
}

/* --- Layout/PageHeader/PageHeader.css ------------------------------ */
/* ==========================================================================
 * L2 · Layout · PageHeader — which page, and which one of them.
 *
 * ⭐ THE IDENTITY REGISTER. The rail states which module and which company you
 * are in; this states which record. Same class of fact, so the same mark — the
 * 1px instrument rule, measured 3.01 light / 2.54 dark. Grouping marks
 * elsewhere in the frame wear the structural tone instead, and that difference
 * is the whole hierarchy.
 * ⛔ Not --omx-color-border-subtle, which measures 1.22 / 1.14 on these grounds
 * and is invisible on both.
 *
 * ⛔ NO ACTIONS. Every archetype requiring a `header` region declares
 * `toolbar` as its own. A header that grew buttons would make that contract
 * unenforceable.
 * ========================================================================== */

@layer omx.layout {

  .omx-page-header {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  .omx-page-header__figure {
    flex: 0 0 auto;
    /* Optical, not arithmetic: the avatar's box is taller than the title's
       cap height, so aligning the boxes leaves the face sitting low. */
    margin-block-start: var(--omx-space-1);
  }

  /* ⚠️ min-inline-size: 0 is load-bearing, not tidy-up. Without it a flex item
     refuses to shrink below its content, so a long unbroken record code pushes
     the header wider than the page and scrolls the whole shell sideways. */
  .omx-page-header__naming {
    flex: 1 1 auto;
    min-inline-size: 0;
    display: grid;
    gap: var(--omx-space-2);
  }

  .omx-page-header__title-row {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-page-header__title {
    margin: 0;
    min-inline-size: 0;
    /* ⛔ NO font-family here. `reset.css` already gives `.omx-ui h1` the
       display face, so declaring it again would be a second place for the
       same decision — and it is what made C22 fire on a rule that was only
       ever repeating what the base layer had already said. */
    font-size: var(--omx-font-size-display);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    letter-spacing: var(--omx-letter-spacing-tight);
    color: var(--omx-color-text);
    /* ⛔ No truncation. A record's name is the one string on the page that must
       never be cut — two records whose names differ only past the ellipsis are
       exactly the pair somebody confuses. It wraps instead. */
    overflow-wrap: anywhere;
  }

  /* ⭐ DERIVED, NOT A WIDTH. flex:1 means the rule is whatever the title
     leaves, at any name length and any text scale, and yields to 0 before it
     pushes anything. Same device as the rail's module row and the bar's title
     zone — three places, one idea.
     ⚠️ The mask reads ALPHA only, so currentColor here is a carrier and not a
     colour: no raw colour enters this file. */
  .omx-page-header__rule {
    flex: 1 1 auto;
    min-inline-size: 0;
    block-size: var(--omx-border-width);
    background: var(--omx-brand-instrument);
    opacity: .75;
    color: var(--omx-brand-instrument);
    mask-image: linear-gradient(to right, transparent, currentColor 34%);
  }

  .omx-page-header__lead {
    margin: 0;
    font-size: var(--omx-font-size-lead);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-muted);
  }

  .omx-page-header__facts {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    /* ⭐ THE GAP IS THE SEPARATOR. No middot element, so a fact that turns out
       to be absent cannot leave a dangling character behind it — which is what
       the hand-built header did. */
    gap: var(--omx-space-2) var(--omx-space-4);
    margin-block-start: var(--omx-space-1);
  }

  .omx-page-header__fact {
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    color: var(--omx-color-text-subtle);
    min-inline-size: 0;
    overflow-wrap: anywhere;
  }

  /* ⚠️ SIZE ONLY. `.omx-absent` sits in @layer omx.base, which is EARLIER
     than omx.layout — so anything set here WINS over it, and inheriting the
     weight would have dressed an absence up as a real title.
     ⭐ The italic, the regular weight and the muted ink are absence.css's
     decision and stay its decision; all this rule does is stop the sentence
     rendering at body size inside a display-sized heading. */
  .omx-page-header__title .omx-absent {
    font-size: inherit;
    letter-spacing: inherit;
  }

  /* ⚠️ Narrow: the figure keeps its place rather than stacking. Measured at
     375 — the avatar is 48px and the naming column still clears 240px, which
     holds the lead on two lines instead of pushing the title under a picture
     and costing a whole row of vertical space on the smallest screen. */
  @container omx-content (max-width: 480px) {
    .omx-page-header {
      gap: var(--omx-space-4);
    }
    .omx-page-header__title {
      font-size: var(--omx-font-size-title);
    }
  }

  /* ⛔ NO MOTION IN THIS FILE, deliberately. The header states a fact; it does
     not animate into being, and an entrance on the page's own heading is the
     first thing that makes a dense screen feel slow. */

}

/* --- Layout/Section/Section.css ------------------------------------ */
/* =============================================================================
 * L2 · Layout · Section — a titled block.
 * ========================================================================== */

@layer omx.layout {

  .omx-section {
    --omx-section-gap: var(--omx-space-6);

    display: grid;
    gap: var(--omx-section-gap);
    min-inline-size: 0;
  }

  .omx-section__head {
    display: grid;
    gap: var(--omx-space-1);
  }

  .omx-section__title {
    /* Size comes from the type-ramp utility the tag helper adds, NOT from the
     * heading level. That separation is what stops people choosing <h3> because
     * they wanted smaller text — which is how a heading outline stops
     * describing the page. */
    color: var(--omx-color-text);
  }

  .omx-section__desc {
    max-inline-size: var(--omx-measure);
  }
}

/* --- Layout/Split/Split.css ---------------------------------------- */
/* =============================================================================
 * L2 · Layout · Split — two panes, stacking when the slot is narrow.
 * ========================================================================== */

@layer omx.layout {

  .omx-split {
    --omx-split-gap: var(--omx-space-8);
    --omx-split-cols: 1fr 1fr;
    --omx-split-lead: 260px;

    display: grid;
    gap: var(--omx-split-gap);
    /* Stacked is the DEFAULT, and the two-column form is the enhancement.
     * Building it the other way round means the narrow case is the one nobody
     * tested. */
    grid-template-columns: minmax(0, 1fr);
    min-inline-size: 0;
  }

  /* ⭐ Each pane is its own container, so a table in the narrow pane reacts to
   * the PANE. This is what lets one component serve a 260px sidebar and a
   * 1200px main area with no caller configuration. */
  .omx-split > * {
    min-inline-size: 0;
    container-type: inline-size;
    container-name: omx-slot;
  }

  @container omx-slot (min-width: 700px) {
    .omx-split         { grid-template-columns: var(--omx-split-cols); }
    .omx-split--fixed  { grid-template-columns: var(--omx-split-lead) minmax(0, 1fr); }
  }

  /* Visual order only — the DOM order, and therefore the reading and tab order,
   * is unchanged. ⚠️ Never use this to reorder a sequence that must be followed
   * in order; a keyboard user would traverse it the other way round. */
  .omx-split--reverse > *:first-child { order: 2; }
  .omx-split--reverse > *:last-child  { order: 1; }

  @media print {
    .omx-split { grid-template-columns: minmax(0, 1fr) !important; }
  }
}

/* --- Layout/Stack/Stack.css ---------------------------------------- */
/* =============================================================================
 * L2 · Layout · Stack — vertical rhythm.
 *
 * Spacing belongs to the container. Every element in this package ships with
 * zero margin (Foundation/reset.css) and a Stack supplies the gap — so the same
 * component spaces correctly in a panel, a page and a table cell without
 * knowing which it is in.
 * ========================================================================== */

@layer omx.layout {

  .omx-stack {
    display: grid;
    /* grid, not flex-column: `gap` behaves identically but grid children get
     * `min-width: auto` resolved against the track, so long content truncates
     * instead of forcing the stack wider. */
    gap: var(--omx-stack-gap);
    min-inline-size: 0;
  }

  .omx-stack--center { justify-items: center; text-align: center; }

  /* A stack of stacks keeps its own gap — nesting must not multiply spacing.
   * (No rule needed; documented because it is the thing people expect to break.) */
}

/* --- Layout/Toolbar/Toolbar.css ------------------------------------ */
/* =============================================================================
 * L2 · Layout · Toolbar — the actions row above a region.
 * ========================================================================== */

@layer omx.layout {

  .omx-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--omx-space-5);

    padding-block: var(--omx-space-4);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
    min-inline-size: 0;
  }

  .omx-toolbar--flush { border-block-end: 0; padding-block-start: 0; }

  .omx-toolbar > * { min-inline-size: 0; }

  /* A single child sits leading, not stretched across — space-between with one
   * item would otherwise look identical to flex-start and then jump when a
   * second item appears. */
  .omx-toolbar > :only-child { margin-inline-end: auto; }

  .omx-toolbar--sticky {
    position: sticky;
    inset-block-start: 0;
    /* Below the shell's own chrome. A toolbar that outranks the topbar covers
     * the thing the user is navigating with. */
    z-index: var(--omx-z-sticky);
    background: var(--omx-color-canvas);
    /* The background has to be opaque or content scrolls visibly under it. */
  }

  /* 🔴 A sticky toolbar must never cover the focused element. Browsers honour
   * scroll-padding on the scroll container, so a keyboard user tabbing into a
   * row beneath the toolbar gets it scrolled clear instead of hidden. */
  .omx-region:has(.omx-toolbar--sticky),
  [data-omx-scroll]:has(.omx-toolbar--sticky) {
    scroll-padding-block-start: calc(var(--omx-control-height) + var(--omx-space-8));
  }

  @media print {
    .omx-toolbar { display: none; }
  }
}

/* --- Surfaces/Card/Card.css ---------------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Card — one of many.
 * ========================================================================== */

@layer omx.component {

  .omx-card {
    /* `position: relative` is load-bearing, not cosmetic: it is what the link
     * overlay below stretches against. */
    position: relative;
    display: grid;
    grid-template-rows: auto 1fr;
    min-inline-size: 0;

    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    box-shadow: var(--omx-shadow-xs);

    transition:
      border-color var(--omx-duration-fast) var(--omx-easing-standard),
      box-shadow var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-card--sunken {
    background: var(--omx-color-surface-sunken);
    border-color: var(--omx-color-border-subtle);
    box-shadow: none;
  }

  .omx-card__head {
    display: grid;
    gap: var(--omx-space-1);
    padding: var(--omx-panel-pad);
    padding-block-end: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-card__body {
    padding: var(--omx-panel-pad);
    min-inline-size: 0;
    container-type: inline-size;
    container-name: omx-slot;
  }
  .omx-card__head + .omx-card__body { padding-block-start: 0; }
  .omx-card--flush > .omx-card__body { padding: 0; }
  .omx-card--flush { overflow: hidden; }

  .omx-card__desc { color: var(--omx-color-text-muted); }

  /* ===========================================================================
   * THE WHOLE-CARD LINK
   *
   * 🔴 The card is NOT an <a>. Making it one gives the link an accessible name
   * made of every word in the card, stops text being selectable, and nests any
   * button inside a link, which is invalid HTML.
   *
   * Instead the TITLE is the link and a transparent ::after stretches its hit
   * area over the card. One link, one sensible name, text still selectable —
   * and anything that needs its own click sits above the overlay.
   * ======================================================================== */
  .omx-card__link { color: inherit; text-decoration: none; }

  .omx-card--linked .omx-card__link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
  }

  .omx-card--linked:hover {
    border-color: var(--omx-color-border-strong);
    box-shadow: var(--omx-shadow-sm);
  }
  .omx-card--linked:hover .omx-card__link { text-decoration: underline; }
  .omx-card--linked:has(.omx-card__link:active) { border-color: var(--omx-color-border-control); box-shadow: var(--omx-shadow-xs); }

  /* The ring belongs to the CARD, not to the title, because the card is what
   * the overlay made clickable. */
  .omx-card--linked:has(.omx-card__link:focus-visible) {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
  }
  .omx-card__link:focus-visible { outline: none; }

  /* Anything interactive inside a linked card must sit ABOVE the overlay, or
   * the card's link swallows its clicks. */
  .omx-card--linked :is(button, a:not(.omx-card__link), input, select, textarea, [tabindex]) {
    position: relative;
    z-index: var(--omx-z-raised);
  }

  .omx-card[aria-current="true"] {
    border-color: var(--omx-color-accent);
    box-shadow: 0 0 0 1px var(--omx-color-accent);
  }

  @media (forced-colors: active) {
    .omx-card[aria-current="true"] { border-color: Highlight; border-width: var(--omx-border-width-strong); }
  }

  @media print {
    .omx-card { box-shadow: none; border-color: #000; break-inside: avoid; }
  }
}

/* --- Surfaces/Drawer/Drawer.css ------------------------------------ */
/* =============================================================================
 * L3 · Surfaces · Drawer — an edge-anchored <dialog>, in the top layer.
 * ========================================================================== */

@layer omx.component {

  .omx-drawer {
    padding: 0;
    margin: 0;                       /* a <dialog> is auto-centred by default */
    border: 0;
    background: var(--omx-color-surface);
    color: var(--omx-color-text);
    /* ⭐ `lg`, DELIBERATELY ONE STEP BELOW MODAL — not an oversight. A drawer
     * is anchored to an edge, so three of its four sides are off-screen and it
     * casts on one edge only; the same shadow reads far lighter here than on a
     * free-floating modal. */
    box-shadow: var(--omx-shadow-lg);

    max-block-size: 100dvh;
    block-size: 100dvh;

    /* 🔴🔴 `display: grid` HERE OVERRODE THE UA's `display: none` FOR A CLOSED
     * <dialog>. So every closed drawer stayed laid out, parked 480px off the
     * right edge by its own `translate: 100%` — and EVERY PAGE CONTAINING ONE
     * SCROLLED HORIZONTALLY. Measured on /gallery/surfaces at 1900px:
     * scrollWidth 2380, with the closed drawer sitting at x=1900..2380.
     *
     * ⭐ It is `display: none` until open. The slide-in still animates because
     * `display` is in the transition list with `allow-discrete` (see below),
     * which is exactly what that line was added for.
     *
     * ⚠️ A page-level horizontal scrollbar is the failure a responsive pass
     * hunts for, and this one was invisible to every check the package had:
     * the gate reads source, the assertions read markup, and the snapshot
     * baseline recorded the overflow as normal because it was already there. */
    display: none;
    grid-template-rows: auto minmax(0, 1fr);
    overflow: hidden;                /* the BODY scrolls */
  }

  .omx-drawer[open] { display: grid; }

  /* Logical edges, so RTL is handled by the same rule rather than a second one. */
  .omx-drawer--end {
    inset-block: 0;
    inset-inline-start: auto;
    inset-inline-end: 0;
    border-inline-start: var(--omx-border-width) solid var(--omx-color-border);
  }
  .omx-drawer--start {
    inset-block: 0;
    inset-inline-start: 0;
    inset-inline-end: auto;
    border-inline-end: var(--omx-border-width) solid var(--omx-color-border);
  }
  .omx-drawer--bottom {
    inset-inline: 0;
    inset-block-start: auto;
    inset-block-end: 0;
    inline-size: 100vw;
    max-inline-size: 100vw;
    block-size: auto;
    max-block-size: 85dvh;
    border-block-start: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-xl) var(--omx-radius-xl) 0 0;
  }

  .omx-drawer--sm { inline-size: min(22rem, 100vw); }
  .omx-drawer--md { inline-size: min(30rem, 100vw); }
  .omx-drawer--lg { inline-size: min(44rem, 100vw); }
  .omx-drawer--bottom.omx-drawer--sm,
  .omx-drawer--bottom.omx-drawer--md,
  .omx-drawer--bottom.omx-drawer--lg { inline-size: 100vw; }

  .omx-drawer::backdrop {
    background: rgb(16 25 40 / 44%);
  }
  /* 🔴 FIXED 2026-09-19 — the system branch was NOT inside the media query,
   * so this dark rule also matched the DEFAULT, UNSTAMPED state and applied
   * on a light page. Three states, not two: explicit dark, explicit light,
   * and system — only `@media (prefers-color-scheme: dark)` separates the
   * third. Five files had this; tokens.css and Brand.css did not. */
  .omx-ui[data-theme="dark"] .omx-drawer::backdrop {
    background: rgb(4 8 14 / 62%);
  }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-drawer::backdrop{
      background: rgb(4 8 14 / 62%);
    }
  }

  .omx-drawer__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--omx-space-6);
    padding: var(--omx-panel-pad);
    padding-block-end: var(--omx-space-5);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-drawer__heading { display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-drawer__close   { margin-block-start: -2px; flex: 0 0 auto; }

  .omx-drawer__body {
    padding: var(--omx-panel-pad);
    /* 🔴 overscroll containment comes from [data-omx-scroll] in
     * Foundation/a11y.css. Without it, reaching the end of a long drawer
     * scrolls the PAGE behind it — a real defect in the first RCL. */
    container-type: inline-size;
    container-name: omx-slot;
  }

  /* Slide in from the edge it is anchored to. `allow-discrete` is what lets a
   * display:none element animate at all. Durations are tokens, so reduced
   * motion collapses them to 0 in tokens.css. */
  .omx-drawer {
    transition:
      translate var(--omx-duration-normal) var(--omx-easing-standard),
      overlay   var(--omx-duration-normal) allow-discrete,
      display   var(--omx-duration-normal) allow-discrete;
  }
  .omx-drawer--end    { translate: 100% 0; }
  .omx-drawer--start  { translate: -100% 0; }
  .omx-drawer--bottom { translate: 0 100%; }
  .omx-drawer[open]   { translate: 0 0; }

  @starting-style {
    .omx-drawer--end[open]    { translate: 100% 0; }
    .omx-drawer--start[open]  { translate: -100% 0; }
    .omx-drawer--bottom[open] { translate: 0 100%; }
  }

  @media print {
    .omx-drawer { display: none; }
  }
}

/* --- Surfaces/Menu/Menu.css ---------------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Menu — the action menu, in the top layer.
 *
 * ⭐ No z-index, for the same reason Modal and Popover have none: a [popover] is
 * painted in the browser's top layer, above every stacking context and outside
 * every `overflow: hidden`. That is not a nicety here — a row menu lives inside
 * a scrolling table, which clips absolutely-positioned children and cannot be
 * argued out of it.
 *
 * ⛔ AND NO ANCHOR POSITIONING. `Surfaces/Popover` uses CSS anchor positioning
 * with a CENTRED fallback, which is right for a settings panel. A row-action
 * menu that lands in the middle of the screen is pointing at the wrong row —
 * silently, and only in the browsers that lack anchoring. Menu.js places this
 * one from the trigger's rect, in every browser.
 * ========================================================================== */

@layer omx.component {

  .omx-menu {
    /* ⭐⭐ THESE TWO NUMBERS ARE ALSO READ BY Menu.js. The script has to clamp
     * the panel into the viewport before the element has a box, so it must know
     * the same width and the same gutter this rule uses. Reading them back off
     * the element — rather than hard-coding a second copy in the script — is
     * what stops the two drifting apart the first time a size is adjusted. */
    --omx-menu-width: 14rem;
    --omx-menu-gutter: var(--omx-space-4);

    /* A [popover] carries UA defaults — margin:auto and a 1px border — that
     * centre it in the viewport. Both have to go before anything else. */
    margin: 0;
    inset: auto;
    position: fixed;      /* Menu.js writes the insets; see its header. */
    padding: var(--omx-space-2);

    inline-size: min(var(--omx-menu-width), calc(100vw - 2 * var(--omx-menu-gutter)));
    /* An upper bound only. Menu.js narrows it further to the space actually
     * available above or below the trigger, so a menu near the bottom of a tall
     * page scrolls inside itself rather than running off the screen. */
    max-block-size: min(22rem, calc(100dvh - var(--omx-space-11)));

    color: var(--omx-color-text);
    background: var(--omx-color-surface-raised);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    box-shadow: var(--omx-shadow-xl);
  }

  .omx-menu--sm { --omx-menu-width: 11rem; }
  .omx-menu--md { --omx-menu-width: 14rem; }

  /* The trigger reads as held-down while its menu is open. ⭐ This rule sits
   * after Button.css in the bundle, so it beats `--ghost:hover` at equal
   * specificity — which is what we want: open outranks hovered. */
  .omx-menu__trigger[aria-expanded="true"] { background: var(--omx-color-control-active); }

  .omx-menu__group {
    display: flex;
    flex-direction: column;
  }

  /* ⚠️ `display: contents` — the <form> around a posting item must not become a
   * box, or that one item sits in its own flex container and stops aligning
   * with its neighbours. The form still submits; it is `role="none"` already,
   * so nothing is lost from the accessibility tree either. */
  .omx-menu__form { display: contents; }

  .omx-menu__label {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    margin: 0;
    padding: var(--omx-space-3) var(--omx-space-4) var(--omx-space-2);
    color: var(--omx-color-text-muted);
  }
  .omx-menu__label-icon {
    flex: 0 0 auto;
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    fill: currentColor;
  }

  .omx-menu__rule {
    margin-block: var(--omx-space-2);
    margin-inline: var(--omx-space-3);
    border: 0;
    border-block-start: var(--omx-border-width) solid var(--omx-color-border);
  }

  .omx-menu__item {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    inline-size: 100%;
    /* ⭐ The control height token, not a number: 32px by default, and it drops
     * with density, so a menu item matches every other control on the page.
     *
     * 🔴 IT DOES NOT REACH 44px ON A PHONE, AND THAT IS NOT THIS COMPONENT'S
     * DOING. tokens.css bumps `--omx-control-height` to `--omx-size-touch`
     * inside `@media (max-width: 768px)` on `.omx-ui` — but
     * `.omx-ui[data-density="cozy"]` is (0,2,0) and a media query adds no
     * specificity, so a stored density preference wins and the bump never
     * applies. MEASURED at 375px with `data-density="cozy"`: 22 buttons and 6
     * menu items all render 28-29px against a token that says 44.
     * ⭐ The half of that rule nothing competes with — `--omx-font-size-control:
     * 1rem` — DOES apply, which is why it reads as working.
     * ⛔ Left alone deliberately: the fix belongs in tokens.css, it changes
     * every control in the package, and this component must not special-case
     * itself out of an estate-wide defect. Reported, not folded in. */
    min-block-size: var(--omx-control-height);
    padding-block: var(--omx-space-2);
    padding-inline: var(--omx-space-4);

    border: 0;
    border-radius: var(--omx-radius-md);
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: var(--omx-font-size-body);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
  }

  /* 🔴 The ring is INSET here, and that is not cosmetic. The global
   * `:focus-visible` ring sits 2px OUTSIDE the element; an item is the full
   * width of a panel that scrolls, so the outer ring is clipped by the panel's
   * own overflow on both inline edges — on the one control in the component
   * that only ever gets focus from the keyboard. */
  .omx-menu__item:focus-visible { outline-offset: -2px; }

  .omx-menu__item:hover { background: var(--omx-color-control-hover); text-decoration: none; }
  .omx-menu__item:active { background: var(--omx-color-control-active); }

  /* ⭐ The destructive item gets a RED wash rather than the neutral one, and it
   * gets its own press step: `danger-bg` is the lighter of the two tints and
   * `danger-subtle` the deeper, so hover → press still moves in the direction a
   * press should. Reusing `control-active` here would make the press on Void
   * look identical to the press on Edit. */
  .omx-menu__item--danger { color: var(--omx-color-danger-text); }
  .omx-menu__item--danger:hover { background: var(--omx-color-danger-bg); }
  .omx-menu__item--danger:active { background: var(--omx-color-danger-subtle); }

  /* Present, announced, reachable — and not available. See MenuModel.Disabled
   * for why this is aria-disabled rather than the disabled attribute. */
  .omx-menu__item[aria-disabled="true"] {
    color: var(--omx-color-text-muted);
    cursor: default;
  }
  .omx-menu__item[aria-disabled="true"]:hover { background: transparent; }

  .omx-menu__icon {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    flex: 0 0 auto;
    fill: currentColor;
  }

  /* ⭐ A LONG LABEL WRAPS; IT DOES NOT TRUNCATE. "Export selected rows to
   * Excel" ellipsised to "Export selected…" is a person guessing what a button
   * does before pressing it, and this is the surface that holds Delete. The
   * panel grows instead — it is already scroll-bounded above. */
  .omx-menu__text {
    flex: 1 1 auto;
    min-inline-size: 0;
  }

  .omx-menu__shortcut {
    flex: 0 0 auto;
    color: var(--omx-color-text-muted);
  }

  /* Entry. `allow-discrete` is what lets a display:none element animate at all;
   * the duration is a token, so reduced motion collapses it to 0ms. */
  .omx-menu {
    opacity: 0;
    transition:
      opacity var(--omx-duration-fast) var(--omx-easing-standard),
      overlay var(--omx-duration-fast) allow-discrete,
      display var(--omx-duration-fast) allow-discrete;
  }
  .omx-menu:popover-open { opacity: 1; }
  @starting-style {
    .omx-menu:popover-open { opacity: 0; }
  }

  @media print { .omx-menu { display: none; } }
}

/* --- Surfaces/Modal/Modal.css -------------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Modal
 *
 * ⭐⭐ A modal <dialog> is painted in the browser's TOP LAYER — above every
 * stacking context, outside every ancestor's containment and clipping. That is
 * why there is no z-index here and why nothing in this file fights an ancestor:
 * `--omx-z-modal` exists for hand-rolled overlays that cannot use <dialog>,
 * and this component is not one of them.
 * ========================================================================== */

@layer omx.component {

  .omx-modal {
    /* A <dialog> is display:none until opened, so every rule here applies only
     * to the open state and none of it needs a modifier class. */
    padding: 0;
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    background: var(--omx-color-surface-raised);
    color: var(--omx-color-text);
    /* ⭐ `xl`, ONE STEP ABOVE DRAWER. A modal opens on top of a drawer, so it
     * cannot carry the drawer's shadow — and it did, byte for byte. See the
     * elevation invariant in tokens.css. */
    box-shadow: var(--omx-shadow-xl);

    inline-size: min(var(--omx-modal-width, 32rem), calc(100vw - var(--omx-space-9)));
    max-block-size: min(40rem, calc(100dvh - var(--omx-space-9)));

    /* dvh, not vh: on mobile, vh is the tallest possible viewport, so a dialog
     * sized in vh is taller than the visible area while the URL bar is shown. */
    /* 🔴🔴 SAME BUG AS DRAWER, AND WORSE. `display: grid` on the base rule
     * overrides the UA's `display: none` for a closed <dialog>, so a closed
     * modal is LAID OUT IN THE PAGE. Measured on /gallery/surfaces: two closed
     * modals rendering at 512x166 each, in normal flow, in a package that had
     * passed 197 render assertions and a clean snapshot baseline.
     *
     * ⚠️ The drawer's version announced itself with a horizontal scrollbar
     * (its `translate: 100%` parked it off-screen). This one was silent — it
     * just put content on the page that nobody asked for.
     *
     * ⭐ `display` is in the transition list with `allow-discrete`, which is
     * what makes the open animation still work from `display: none`. */
    display: none;
    grid-template-rows: auto minmax(0, 1fr);
    overflow: hidden;   /* the BODY scrolls, not the dialog */
  }

  .omx-modal[open] { display: grid; }

  .omx-modal--sm { --omx-modal-width: 24rem; }
  .omx-modal--md { --omx-modal-width: 32rem; }
  .omx-modal--lg { --omx-modal-width: 48rem; }

  .omx-modal::backdrop {
    background: var(--omx-color-surface-overlay);
    /* ⚠️ ::backdrop does NOT inherit from the page, so a var() here resolves
     * against the ROOT, not against .omx-ui. The token is duplicated as a
     * literal fallback for exactly that reason. */
    background: rgb(16 25 40 / 44%);
    /* ⭐ RAISED 1px -> 4px, owner ruling 2026-09-20 ("everything blurred
     * behind"). 1px was a rounding error — it read as nothing, so the page
     * behind a modal still competed for attention at the exact moment the
     * modal is asking a question.
     * ⛔ Raised for EVERY modal, not for the one that prompted it: a design
     * system with one dialog that blurs differently has two dialog designs. */
    backdrop-filter: blur(4px);
  }

  /* 🔴 FIXED 2026-09-19 — the system branch was NOT inside the media query,
   * so this dark rule also matched the DEFAULT, UNSTAMPED state and applied
   * on a light page. Three states, not two: explicit dark, explicit light,
   * and system — only `@media (prefers-color-scheme: dark)` separates the
   * third. Five files had this; tokens.css and Brand.css did not. */
  .omx-ui[data-theme="dark"] .omx-modal::backdrop {
    background: rgb(4 8 14 / 62%);
  }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-modal::backdrop{
      background: rgb(4 8 14 / 62%);
    }
  }

  .omx-modal__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--omx-space-6);
    padding: var(--omx-panel-pad);
    padding-block-end: var(--omx-space-5);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-modal__heading { display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-modal__close   { margin-block-start: -2px; flex: 0 0 auto; }

  .omx-modal__body {
    padding: var(--omx-panel-pad);
    /* overscroll containment + stable gutter come from [data-omx-scroll]
     * in Foundation/a11y.css. */
  }

  /* The dialog is a container for its own content, so a table inside a small
   * modal collapses the same way it would in a small panel. */
  .omx-modal__body {
    container-type: inline-size;
    container-name: omx-slot;
  }

  /* Entry. `allow-discrete` is what lets a display:none element animate in at
   * all; without it the dialog simply appears. Durations are tokens, so
   * prefers-reduced-motion collapses them to 0 via tokens.css. */
  @starting-style {
    .omx-modal[open] { opacity: 0; translate: 0 -8px; }
    .omx-modal[open]::backdrop { opacity: 0; }
  }
  .omx-modal[open] { opacity: 1; translate: 0 0; }
  .omx-modal {
    opacity: 0;
    translate: 0 -8px;
    transition:
      opacity var(--omx-duration-fast) var(--omx-easing-standard),
      translate var(--omx-duration-fast) var(--omx-easing-standard),
      overlay var(--omx-duration-fast) allow-discrete,
      display var(--omx-duration-fast) allow-discrete;
  }

  @media (max-width: 640px) {
    /* On a phone a centred card wastes the bottom half of the screen and puts
     * the actions out of thumb reach. It becomes a sheet. */
    .omx-modal {
      inline-size: 100vw;
      max-inline-size: 100vw;
      max-block-size: 90dvh;
      margin-block-start: auto;
      margin-block-end: 0;
      border-radius: var(--omx-radius-xl) var(--omx-radius-xl) 0 0;
      border-inline: 0;
      border-block-end: 0;
    }
  }

  @media print {
    .omx-modal { display: none; }
  }
}

/* --- Surfaces/Panel/Panel.css -------------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Panel — the workhorse container.
 * ========================================================================== */

@layer omx.component {

  .omx-panel {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    min-inline-size: 0;

    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    /* Rams: a rule before a shadow. The border carries the separation; the
     * shadow only says "this is the nearer of two overlapping things". */
    box-shadow: var(--omx-shadow-xs);
  }

  .omx-panel--sunken {
    background: var(--omx-color-surface-sunken);
    box-shadow: none;
    border-color: var(--omx-color-border-subtle);
  }

  .omx-panel__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--omx-space-6);
    padding: var(--omx-panel-pad);
    padding-block-end: var(--omx-space-5);
  }

  .omx-panel__heading { display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-panel__desc    { max-inline-size: var(--omx-measure); }

  .omx-panel__body {
    padding: var(--omx-panel-pad);
    min-inline-size: 0;

    /* ⭐ The body is a container query root: content sizes itself against the
     * PANEL. One table serves a 280px aside and a 1100px main area with no
     * caller configuration.
     * 🔧 It does NOT trap position:fixed — that claim was measured false; see
     * the correction at the head of Layout/Page/Page.css. Overlays are still
     * <dialog> elements, for the reasons that survived the measurement. */
    container-type: inline-size;
    container-name: omx-slot;
  }

  /* A head followed by a body doubles the padding between them. */
  .omx-panel__head + .omx-panel__body { padding-block-start: 0; }

  .omx-panel--flush > .omx-panel__body { padding: 0; }
  /* A flush body still needs its corners clipped or a table's first row
   * square-corners over the panel's radius. */
  .omx-panel--flush { overflow: hidden; }

  /* ===========================================================================
   * COLLAPSIBLE — native <details>, so disclosure semantics, keyboard handling
   * and find-in-page expansion all come from the platform.
   * ======================================================================== */
  .omx-panel--collapsible > .omx-panel__head--toggle {
    cursor: pointer;
    list-style: none;              /* Firefox marker */
    align-items: center;
  }
  .omx-panel--collapsible > .omx-panel__head--toggle::-webkit-details-marker {
    display: none;                 /* Safari marker */
  }

  .omx-panel__chevron {
    inline-size: 12px;
    block-size: 12px;
    flex: 0 0 auto;
    color: var(--omx-color-text-muted);
    transition: rotate var(--omx-duration-fast) var(--omx-easing-standard);
  }
  .omx-panel--collapsible[open] > .omx-panel__head--toggle .omx-panel__chevron { rotate: 90deg; }

  /* The summary is the interactive element, so it takes the focus ring. */
  .omx-panel__head--toggle:focus-visible {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: -2px;
    border-radius: var(--omx-radius-lg) var(--omx-radius-lg) 0 0;
  }

  .omx-panel__head--toggle:hover { background: var(--omx-color-control-hover); }
  .omx-panel__head--toggle:active { background: var(--omx-color-control-active); }

  @media print {
    .omx-panel {
      box-shadow: none;
      border-color: #000;
      break-inside: avoid;
    }
    /* A collapsed panel prints empty and nobody notices until the document is
     * on paper. Print everything open. */
    .omx-panel--collapsible > .omx-panel__body { display: block !important; }
    .omx-panel__chevron { display: none; }
  }
}

/* --- Surfaces/Popover/Popover.css ---------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Popover — native [popover], in the top layer.
 *
 * ⭐ No z-index here, for the same reason Modal has none: a popover is painted
 * in the browser's top layer, above every stacking context and outside every
 * `overflow: hidden` ancestor. `--omx-z-popover` exists for anything that
 * cannot use the API; this component is not one of them.
 * ========================================================================== */

@layer omx.component {

  .omx-popover {
    /* A [popover] element has UA defaults — margin:auto and a 1px solid border
     * — that centre it in the viewport. Both have to go before anchoring. */
    margin: 0;
    inset: auto;
    padding: var(--omx-space-6);

    inline-size: min(var(--omx-popover-width, 20rem), calc(100vw - var(--omx-space-9)));
    max-block-size: min(24rem, calc(100dvh - var(--omx-space-11)));
    overflow: auto;
    overscroll-behavior: contain;

    color: var(--omx-color-text);
    background: var(--omx-color-surface-raised);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    /* 🔴 WAS `md` — LIGHTER THAN THE MODAL IT CAN OPEN INSIDE. A date picker
     * or a select menu in a modal is a popover, painted above that modal in the
     * top layer, and it read as sitting under it. */
    box-shadow: var(--omx-shadow-xl);

    container-type: inline-size;
    container-name: omx-slot;
  }

  .omx-popover--sm { --omx-popover-width: 15rem; }
  .omx-popover--md { --omx-popover-width: 20rem; }

  .omx-popover__title {
    margin-block-end: var(--omx-space-4);
    color: var(--omx-color-text-muted);
  }

  .omx-popover__close { margin-block-start: var(--omx-space-5); }

  /* ===========================================================================
   * ANCHORING
   *
   * CSS anchor positioning is the correct mechanism and is not yet everywhere,
   * so it is applied inside @supports and the fallback is a plain centred
   * panel. ⚠️ The FALLBACK is the important half: a popover that lands in the
   * middle of the screen is merely inelegant, whereas one positioned by a
   * script that assumed anchoring exists lands off-screen.
   * ======================================================================== */
  @supports (anchor-name: --omx-a) {
    [popovertarget] { anchor-name: --omx-popover-anchor; }

    .omx-popover {
      position-anchor: --omx-popover-anchor;
      inset-area: block-end span-inline-end;
      margin-block-start: var(--omx-space-3);
      /* Flip above, then to the other side, before giving up. */
      position-try-fallbacks: block-start span-inline-end, block-end span-inline-start, flip-block;
    }
  }

  @supports not (anchor-name: --omx-a) {
    .omx-popover:popover-open {
      position: fixed;
      inset-block-start: 50%;
      inset-inline-start: 50%;
      translate: -50% -50%;
    }
  }

  /* Entry. `allow-discrete` is what lets a display:none element animate at all;
   * the durations are tokens, so reduced motion collapses them. */
  .omx-popover {
    opacity: 0;
    transition:
      opacity var(--omx-duration-fast) var(--omx-easing-standard),
      overlay var(--omx-duration-fast) allow-discrete,
      display var(--omx-duration-fast) allow-discrete;
  }
  .omx-popover:popover-open { opacity: 1; }
  @starting-style {
    .omx-popover:popover-open { opacity: 0; }
  }

  @media print { .omx-popover { display: none; } }
}

/* --- Surfaces/Tooltip/Tooltip.css ---------------------------------- */
/* =============================================================================
 * L3 · Surfaces · Tooltip
 *
 * 🔴 THE RULE THAT COMES FIRST: a tooltip is never the only place information
 * lives. It cannot be reached by touch, it is dismissed by the next pointer
 * movement, and it is invisible in print. Anything a person must know to finish
 * their task belongs in the page.
 *
 * What it IS for: naming an icon-only control, and spelling out an abbreviation.
 *
 * ⚠️ WCAG 1.4.13 — content on hover must be dismissable (Escape), hoverable
 * (the pointer can move onto it without it vanishing) and persistent (it does
 * not disappear on a timer). All three are handled below and in Tooltip.js.
 * ========================================================================== */

@layer omx.component {

  .omx-tooltip {
    /* The WRAPPER, not the bubble: the trigger lives inside it so the bubble
     * can position against something. */
    position: relative;
    display: inline-flex;
  }

  .omx-tooltip__bubble {
    position: absolute;
    inset-block-end: calc(100% + var(--omx-space-3));
    inset-inline-start: 50%;
    translate: -50% 0;
    z-index: var(--omx-z-tooltip);

    inline-size: max-content;
    max-inline-size: 26ch;
    padding: var(--omx-space-3) var(--omx-space-5);

    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    text-align: start;
    /* Inverted, so it reads as an overlay rather than as more page. */
    color: var(--omx-color-text-inverse);
    background: var(--omx-color-text);
    border-radius: var(--omx-radius-sm);
    /* A tooltip can be triggered from inside a modal, so it sits in the same
     * band; `xl` is 16px/40px, which a 120px bubble carries without bulk. */
    box-shadow: var(--omx-shadow-xl);

    opacity: 0;
    visibility: hidden;
    transition:
      opacity var(--omx-duration-fast) var(--omx-easing-standard),
      visibility var(--omx-duration-fast) var(--omx-easing-standard);
  }

  /* Shown by hover, by keyboard focus, AND by the data attribute the script
   * sets — the attribute is what keeps it open while the pointer is ON it and
   * what Escape clears. */
  /* omx-no-press: a REVEAL of the bubble, not a state of the trigger. The bubble is never pressed */
  .omx-tooltip:hover > .omx-tooltip__bubble,
  .omx-tooltip:focus-within > .omx-tooltip__bubble,
  .omx-tooltip[data-omx-tooltip-open] > .omx-tooltip__bubble {
    opacity: 1;
    visibility: visible;
  }

  /* 🔴 Escape must hide it even while the pointer is still hovering — WCAG
   * 1.4.13's "dismissable". Without this the bubble reappears the moment the
   * script's attribute is removed, because :hover is still true. */
  .omx-tooltip[data-omx-tooltip-dismissed] > .omx-tooltip__bubble {
    opacity: 0;
    visibility: hidden;
  }

  .omx-tooltip__bubble::after {
    content: "";
    position: absolute;
    inset-block-start: 100%;
    inset-inline-start: 50%;
    translate: -50% 0;
    border: 5px solid transparent;
    border-block-start-color: var(--omx-color-text);
  }

  /* Below the trigger, for anything near the top of the viewport. */
  .omx-tooltip--below > .omx-tooltip__bubble {
    inset-block-end: auto;
    inset-block-start: calc(100% + var(--omx-space-3));
  }
  .omx-tooltip--below > .omx-tooltip__bubble::after {
    inset-block-start: auto;
    inset-block-end: 100%;
    border-block-start-color: transparent;
    border-block-end-color: var(--omx-color-text);
  }

  @media print {
    /* A tooltip cannot print, which is the other half of why it never holds
     * anything essential. */
    .omx-tooltip__bubble { display: none; }
  }
}

/* --- DataDisplay/AssignBoard/AssignBoard.css ----------------------- */
/* ==========================================================================
 * L4 · DataDisplay · AssignBoard — resource × time, and what fits.
 *
 * ⭐ HEAT IS A BORDER AND A WORD, NEVER A FILL ALONE. WCAG 1.4.1: colour is
 * never the only carrier. Every cell states its own fraction and its basis in
 * text, and the tint is a second signal for scanning — not the signal.
 *
 * ⛔ OVERLOAD IS NOT PAINTED AS AN ERROR. The drop was ALLOWED; the cell is
 * reporting a fact the planner now has to argue about with production. Red
 * would read as "the software refused", which is the one thing it did not do.
 * ========================================================================== */

@layer omx.component {

  /* 🔴🔴 THE PENDING BAR. It sits above the grid because the grid is what
   * misleads — blocks in their new places look placed. Structure register
   * border, no tone colour: an unsaved proposal is a state, not an error. */
  .omx-assignboard__pending {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-2) var(--omx-space-4);
    margin: 0 0 var(--omx-space-3);
    padding: var(--omx-space-2) var(--omx-space-3);
    border: var(--omx-border-width) dashed var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
  }

  .omx-assignboard__pending-count,
  .omx-assignboard__pending-unsaved,
  .omx-assignboard__pending-readonly { margin: 0; }

  .omx-assignboard__pending-effect { color: var(--omx-color-text-muted); }

  .omx-assignboard__pending-actions {
    display: flex;
    gap: var(--omx-space-2);
    align-items: center;
    margin-inline-start: auto;
  }

  .omx-assignboard__aside {
    flex-basis: 100%;
    margin: 0;
    padding-inline-start: var(--omx-space-4);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  /* ⛔ NOT BY COLOUR ALONE. A tentative block gets a DASHED edge — a shape
   * difference that survives greyscale and colour-blindness — and a line of
   * text saying where it came from. */
  .omx-assignboard__block[data-omx-tentative],
  .omx-assignboard__chip[data-omx-tentative] {
    outline: var(--omx-border-width-strong) dashed var(--omx-color-border-strong);
    outline-offset: -2px;
  }

  .omx-assignboard__tentative,
  .omx-assignboard__was {
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-assignboard__tentative { display: block; }

  .omx-assignboard__undo { font-size: var(--omx-font-size-caption); }

  /* ⭐ The reading that produced every minute figure on the board. Quiet — it
   * is a footnote to the numbers, not a message — but present, because an
   * unstated formula is the thing that makes a planner distrust a board. */
  .omx-assignboard__reading {
    margin: 0 0 var(--omx-space-3);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-tight);
  }

  .omx-assignboard {
    display: grid;
    gap: var(--omx-space-5);
  }

  .omx-assignboard__caption {
    margin: 0;
    color: var(--omx-color-text-muted);
  }

  /* ⚠️ tabindex=0 in the markup, so a keyboard can reach a scrollable region
     that contains no focusable child — a WCAG 2.1.1 trap the package has hit
     before. overscroll-contain stops a trackpad flick scrolling the page. */
  .omx-assignboard__scroll {
    overflow: auto;
    overscroll-behavior: contain;
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    border-radius: var(--omx-radius-md);
  }

  .omx-assignboard__grid {
    border-collapse: collapse;
    inline-size: 100%;
    font-size: var(--omx-font-size-caption);
  }

  .omx-assignboard__grid th,
  .omx-assignboard__grid td {
    border: var(--omx-border-width) solid var(--omx-color-border);
    padding: var(--omx-space-2);
    vertical-align: top;
    text-align: start;
  }

  .omx-assignboard__corner,
  .omx-assignboard__slot {
    background: var(--omx-color-surface-raised);
    font-weight: var(--omx-font-weight-medium);
    position: sticky;
    inset-block-start: 0;
  }

  .omx-assignboard__resource {
    min-inline-size: 9rem;
  }

  .omx-assignboard__resource-name {
    display: block;
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
  }

  /* Advisory. It explains the denominator; it never fences a drop. */
  .omx-assignboard__resource-note {
    display: block;
    color: var(--omx-color-text-subtle);
  }

  .omx-assignboard__cell {
    min-inline-size: 7rem;
  }

  .omx-assignboard__load {
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-2);
    align-items: baseline;
    font-variant-numeric: tabular-nums;
  }

  .omx-assignboard__pct {
    font-weight: var(--omx-font-weight-semibold);
    color: var(--omx-color-text);
  }

  .omx-assignboard__of,
  .omx-assignboard__basis {
    margin: 0;
    color: var(--omx-color-text-subtle);
    font-variant-numeric: tabular-nums;
  }

  /* ⛔ A WORD, not only a tint. */
  .omx-assignboard__over {
    margin: var(--omx-space-1) 0 0;
    color: var(--omx-color-text-muted);
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-assignboard__cell[data-omx-heat="over"] {
    background: var(--omx-color-warning-subtle);
  }

  /* \u2b50 A LEGAL TARGET, marked only while a drag is in flight. An ineligible
     cell is simply never marked -- it does not dim, it does not warn, it just
     never lights up. That is the spec's "not a target", made visible.
     \u26d4 Outline, not background: the cell's background already carries its HEAT,
     and overloading one channel with two meanings is how a planner reads
     "over capacity" as "cannot drop here". */
  .omx-assignboard__cell[data-omx-droppable],
  .omx-assignboard__tray[data-omx-droppable] {
    outline: 2px dashed var(--omx-brand-instrument);
    outline-offset: -3px;
  }

  .omx-assignboard__cell[data-omx-dropping],
  .omx-assignboard__tray[data-omx-dropping] {
    outline-style: solid;
    outline-width: 3px;
  }

  .omx-assignboard__cell[data-omx-heat="closed"] {
    background: var(--omx-color-surface-sunken);
  }

  .omx-assignboard__blocks,
  .omx-assignboard__tray-list {
    list-style: none;
    margin: var(--omx-space-2) 0 0;
    padding: 0;
    display: grid;
    gap: var(--omx-space-1);
  }

  /* ⭐ WIDTH FROM DATA. --omx-block-share is the minutes this block consumes as
     a percentage of what the cell has available, so a block that does not fit
     LOOKS like a block that does not fit. */
  .omx-assignboard__block {
    inline-size: calc(var(--omx-block-share, 100) * 1%);
    min-inline-size: 3rem;
    padding: var(--omx-space-1) var(--omx-space-2);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-accent-subtle);
    color: var(--omx-color-text);
    cursor: grab;
    display: flex;
    justify-content: space-between;
    gap: var(--omx-space-2);
    overflow: hidden;
  }

  .omx-assignboard__chip {
    padding: var(--omx-space-2) var(--omx-space-3);
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-surface-raised);
    cursor: grab;
    display: flex;
    gap: var(--omx-space-2);
    align-items: baseline;
  }

  .omx-assignboard__tray-list {
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }

  .omx-assignboard__block-mins {
    font-variant-numeric: tabular-nums;
    color: var(--omx-color-text-muted);
  }

  .omx-assignboard__block-detail {
    color: var(--omx-color-text-subtle);
  }

  .omx-assignboard__tray {
    display: grid;
    gap: var(--omx-space-2);
  }

  .omx-assignboard__tray-title,
  .omx-assignboard__tray-empty {
    margin: 0;
  }

  /* ⭐ The move forms are the MECHANISM, so they are reachable — visually
     compact, never display:none, and never hidden from the accessibility tree.
     A board whose only non-drag path was hidden would fail 2.5.7 while
     appearing to pass it. */
  .omx-assignboard__moves {
    display: grid;
    gap: var(--omx-space-2);
  }


  .omx-assignboard__move-label {
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
  }

  .omx-assignboard__summary {
    margin: 0;
    color: var(--omx-color-text-muted);
  }

  /* 🔴🔴 THE GHOST LAYER — recessed beneath the plan, never mistaken for it.
   * Dashed, hatched, muted, and the word "advisory" on every one: a shape and a
   * word, never tone alone. `cursor: default`, not `grab` — it cannot be moved,
   * and a pointer that says otherwise is a promise the board would break. */
  .omx-assignboard__advisory-note {
    margin: 0 0 var(--omx-space-2);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  /* 🔴 THE ADVICE NEVER SHAPES THE PLAN. Measured: a ghost chip wider than the
   * bookings in its column widened that column 163px → 172px — the suggestion
   * reshaping the committed grid it is meant to sit behind. `contain: inline-size`
   * gives the ghost layer an intrinsic width of zero, so it takes the width the
   * plan leaves it and wraps inside it, and contributes nothing to column sizing. */
  .omx-assignboard__ghosts {
    contain: inline-size;
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1);
    margin: var(--omx-space-1) 0 0;
    padding: 0;
    list-style: none;
  }

  .omx-assignboard__ghost {
    max-inline-size: 100%;
    flex-wrap: wrap;
    display: inline-flex;
    align-items: baseline;
    gap: var(--omx-space-2);
    padding: 0 var(--omx-space-2);
    border: var(--omx-border-width) dashed var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
    background: repeating-linear-gradient(135deg, transparent 0 4px,
                var(--omx-color-border-subtle) 4px 5px);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    cursor: default;
  }

  .omx-assignboard__ghost-tag { font-weight: var(--omx-font-weight-medium); }
  .omx-assignboard__ghost-mins { font-variant-numeric: tabular-nums; }

  @media (forced-colors: active) {
    .omx-assignboard__cell[data-omx-heat="over"] { outline: 2px solid CanvasText; }
    .omx-assignboard__block { border: 1px solid CanvasText; }
    /* ⚠️ Forced colours strip the hatching and the muted tone, so without this
     * a ghost would be one more box beside the bookings. The DASHED edge — and
     * the word "advisory" — are what survive. */
    .omx-assignboard__ghost { border: 1px dashed CanvasText; background: none; }
  }

}

/* --- DataDisplay/Board/Board.css ----------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Board — one grouping dimension, as columns.
 * ========================================================================== */

@layer omx.component {

  .omx-board { min-inline-size: 0; }
  .omx-board__caption { margin-block-end: var(--omx-space-4); }

  .omx-board__scroll { max-inline-size: 100%; }

  .omx-board__columns {
    display: grid;
    grid-auto-flow: column;
    /* A board scrolls sideways on purpose: squeezing eight columns into the
     * viewport makes every card a sliver. 260px is the narrowest a card with a
     * title, two meta facts and a badge stays readable at. */
    grid-auto-columns: minmax(260px, 1fr);
    gap: var(--omx-space-5);
    align-items: start;
    padding-block-end: var(--omx-space-3);
  }

  .omx-board__column {
    display: grid;
    gap: var(--omx-space-4);
    padding: var(--omx-space-4);
    background: var(--omx-color-surface-sunken);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    border-radius: var(--omx-radius-md);
    min-inline-size: 0;
  }

  .omx-board__head { display: grid; gap: var(--omx-space-1); }

  .omx-board__title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--omx-space-3);
    margin: 0;
    color: var(--omx-color-text);
  }

  .omx-board__count {
    font-variant-numeric: tabular-nums;
    font-weight: var(--omx-font-weight-regular);
    color: var(--omx-color-text-muted);
  }

  .omx-board__total { color: var(--omx-color-text-muted); font-variant-numeric: tabular-nums; margin: 0; }

  .omx-board__column--over { border-color: var(--omx-color-warning-text); }
  .omx-board__limit { color: var(--omx-color-warning-text); margin: 0; }

  .omx-board__empty {
    color: var(--omx-color-text-subtle);
    padding: var(--omx-space-5) 0;
    text-align: center;
    margin: 0;
  }

  .omx-board__cards { display: grid; gap: var(--omx-space-3); min-inline-size: 0; }

  .omx-board__card {
    display: grid;
    gap: var(--omx-space-2);
    padding: var(--omx-space-4);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-sm);
    box-shadow: var(--omx-shadow-xs);
    min-inline-size: 0;
  }

  .omx-board__card-title { margin: 0; font-weight: var(--omx-font-weight-medium); }
  .omx-board__card-link { color: inherit; }

  .omx-board__card-meta { color: var(--omx-color-text-muted); margin: 0; }
  .omx-board__sep { margin-inline: var(--omx-space-2); color: var(--omx-color-text-subtle); }

  .omx-board__card-foot {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    flex-wrap: wrap;
  }

  .omx-board__amount {
    font-variant-numeric: tabular-nums;
    font-weight: var(--omx-font-weight-medium);
    margin-inline-start: auto;
  }

  .omx-board__move { display: flex; gap: var(--omx-space-2); align-items: center; }
  .omx-board__move-select { inline-size: 100%; min-inline-size: 0; }

  /* A dragged card must stay findable — it is the thing the pointer is
   * holding, so it dims rather than vanishing. */
  .omx-board__card[data-omx-dragging="1"] { opacity: .45; }

  /* The drop target is OUTLINED, not recoloured: a fill swap alone disappears
   * in forced-colors and is invisible to anyone who cannot see the hue change. */
  .omx-board__column[data-omx-dropzone="1"] {
    outline: var(--omx-border-width-strong) dashed var(--omx-color-accent);
    outline-offset: calc(var(--omx-border-width) * -1);
  }

  @media (prefers-reduced-motion: no-preference) {
    .omx-board__card { transition: box-shadow var(--omx-duration-fast) var(--omx-easing-standard); }
  }

  @media print {
    .omx-board__scroll { overflow: visible; }
    .omx-board__columns { grid-auto-flow: row; grid-auto-columns: auto; }
    .omx-board__move { display: none; }
    .omx-board__card { break-inside: avoid; box-shadow: none; border-color: #000; }
  }
}

/* --- DataDisplay/Calendar/Calendar.css ----------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Calendar — a month, as a real table.
 * ========================================================================== */

@layer omx.component {

  .omx-calendar { min-inline-size: 0; }

  .omx-calendar__grid {
    inline-size: 100%;
    table-layout: fixed;   /* seven equal columns, whatever the content */
    border-collapse: collapse;
  }

  .omx-calendar__caption {
    caption-side: top;
    text-align: start;
    padding-block-end: var(--omx-space-4);
    color: var(--omx-color-text-muted);
  }

  .omx-calendar__weekday {
    padding: var(--omx-space-3);
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    color: var(--omx-color-text-muted);
    text-align: start;
    border-block-end: var(--omx-border-width) solid var(--omx-color-border);
  }

  .omx-calendar__day {
    vertical-align: top;
    block-size: 96px;
    padding: var(--omx-space-3);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    min-inline-size: 0;
  }

  /* Days from the neighbouring months are context, not content. */
  .omx-calendar__day--outside { background: var(--omx-color-surface-sunken); }
  .omx-calendar__day--outside .omx-calendar__number { color: var(--omx-color-text-subtle); }

  /* A closed period is a FACT about the day, and it is also announced in the
   * cell's hidden text — the hatch alone would be colour-and-pattern only. */
  .omx-calendar__day--closed {
    background: repeating-linear-gradient(
      45deg,
      var(--omx-color-surface-sunken) 0 6px,
      var(--omx-color-surface) 6px 12px);
  }

  .omx-calendar__number {
    display: block;
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--omx-color-text-muted);
    margin-block-end: var(--omx-space-2);
  }

  .omx-calendar__day[aria-current="date"] { background: var(--omx-color-accent-subtle); }
  .omx-calendar__day[aria-current="date"] .omx-calendar__number {
    color: var(--omx-color-accent-text);
    background: var(--omx-color-accent);
    inline-size: 20px;
    block-size: 20px;
    display: grid;
    place-items: center;
    border-radius: var(--omx-radius-pill);
  }

  .omx-calendar__events { display: grid; gap: 2px; min-inline-size: 0; }

  .omx-calendar__event {
    font-size: var(--omx-font-size-caption);
    padding: 1px var(--omx-space-2);
    border-radius: var(--omx-radius-xs);
    background: var(--omx-color-neutral-bg);
    color: var(--omx-color-neutral-text);
    border-inline-start: 2px solid currentColor;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-inline-size: 0;
  }

  .omx-calendar__event--info    { background: var(--omx-color-info-bg);    color: var(--omx-color-info-text); }
  .omx-calendar__event--success { background: var(--omx-color-success-bg); color: var(--omx-color-success-text); }
  .omx-calendar__event--warning { background: var(--omx-color-warning-bg); color: var(--omx-color-warning-text); }
  .omx-calendar__event--danger  { background: var(--omx-color-danger-bg);  color: var(--omx-color-danger-text); }
  .omx-calendar__event--accent  { background: var(--omx-color-accent-subtle); color: var(--omx-color-accent-hover); }

  .omx-calendar__event-link { color: inherit; text-decoration: none; }
  .omx-calendar__event-link:hover { text-decoration: underline; }
  .omx-calendar__event-link:active { color: var(--omx-color-accent-active); }
  .omx-calendar__event-time { font-variant-numeric: tabular-nums; opacity: .8; margin-inline-end: 2px; }

  .omx-calendar__more { color: var(--omx-color-text-subtle); margin-block-start: 2px; }

  /* A month grid below about 560px is seven unreadable slivers. The cells
   * become a single column of days, which is the only honest small form. */
  @container omx-slot (max-width: 560px) {
    .omx-calendar__grid, .omx-calendar__grid tbody, .omx-calendar__grid tr { display: block; }
    .omx-calendar__grid thead { display: none; }
    .omx-calendar__day { display: block; block-size: auto; inline-size: auto; }
    /* The weekday header is gone, so each day has to name itself. */
    .omx-calendar__day--outside { display: none; }
  }

  @media print {
    .omx-calendar__day { break-inside: avoid; border-color: #000; }
    .omx-calendar__day--closed { background: none; }
  }
}

/* --- DataDisplay/Chart/Chart.css ----------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Chart — SVG, no library.
 * ========================================================================== */

@layer omx.component {

  /* The unit, under the plot. Not on the axis: an axis label rotated 90°
   * is unreadable at 11px and untranslatable in a right-to-left locale. */
  .omx-chart__unit { color: var(--omx-color-text-muted); margin: 0; }

  .omx-chart {
    display: grid;
    gap: var(--omx-space-4);
    margin: 0;
    min-inline-size: 0;
  }

  .omx-chart__title { color: var(--omx-color-text-muted); }

  .omx-chart__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-4) var(--omx-space-6);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
  }

  .omx-chart__legend-item { display: flex; align-items: center; gap: var(--omx-space-3); }

  .omx-chart__swatch {
    inline-size: 10px;
    block-size: 10px;
    border-radius: var(--omx-radius-xs);
    flex: 0 0 auto;
  }

  .omx-chart__svg {
    inline-size: 100%;
    block-size: auto;
    /* The viewBox scales by width; height follows. overflow visible so a label
     * at the extreme left is not clipped by a half character. */
    overflow: visible;
  }

  .omx-chart__grid { stroke: var(--omx-chart-grid); stroke-width: 1; }

  .omx-chart__axis {
    font-size: 10px;
    fill: var(--omx-color-text-subtle);
    font-variant-numeric: tabular-nums;
  }

  .omx-chart__bar { rx: 2; }

  .omx-chart__line {
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .omx-chart--with-table .omx-chart__data {
    padding-block-start: var(--omx-space-4);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  @media (forced-colors: active) {
    /* HCM replaces fills, so every series becomes the same colour and the chart
     * stops distinguishing anything. The TABLE is what still works — which is
     * why it is always in the DOM rather than behind a toggle. */
    .omx-chart__svg { display: none; }
    .omx-chart__data { position: static !important; inline-size: auto !important;
                       block-size: auto !important; clip-path: none !important;
                       overflow: visible !important; white-space: normal !important; }
  }

  @media print {
    /* Same reasoning: a monochrome print of a multi-series chart is one colour.
     * Print the numbers. */
    .omx-chart__svg, .omx-chart__legend { display: none; }
    .omx-chart__data { position: static !important; inline-size: auto !important;
                       block-size: auto !important; clip-path: none !important;
                       overflow: visible !important; white-space: normal !important; }
  }
}

/* --- DataDisplay/EditGrid/EditGrid.css ----------------------------- */
/* =============================================================================
 * L4 · DataDisplay · EditGrid — the line-item entry grid.
 * Inherits the Table's geometry; only the editing affordances live here.
 * ========================================================================== */

@layer omx.component {

  /* A row-level error. It is TEXT under the row it belongs to, not a red tint:
   * the tint alone said "something here is wrong" and made a person hunt across
   * five columns for what. */
  .omx-editgrid__errorrow > * { border-block-start: 0; padding-block-start: 0; }

  .omx-editgrid__rowerr {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    color: var(--omx-color-danger-text);
    font-size: var(--omx-font-size-caption);
  }
  .omx-editgrid__rowerr svg { inline-size: 13px; block-size: 13px; flex: none; }


  .omx-editgrid { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-editgrid__scroll {
    /* overflow comes from [data-omx-scroll] in Foundation/a11y.css, together
     * with overscroll containment and a stable gutter. Setting it HERE as
     * well meant the class could scroll without the containment if the
     * attribute were ever omitted — `check-omx` C9 named it. */
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
  }

  /* The line number: fixed, narrow, quiet. It exists so someone can say
   * "line 7 is wrong" out loud. */
  .omx-editgrid__rownum {
    inline-size: 1px;
    padding-inline: var(--omx-space-4);
    text-align: end;
    font-variant-numeric: tabular-nums;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-subtle);
    user-select: none;
    background: var(--omx-color-surface-sunken);
  }

  /* The cell IS the input. A border per input inside a bordered table produces a
   * double grid that is exhausting to read down; the table's own rules are the
   * grid, and the input only shows its edges when it is being used. */
  .omx-editgrid__cell { padding: 0; position: relative; }

  .omx-editgrid__input {
    inline-size: 100%;
    block-size: var(--omx-row-height);
    border: 0;
    border-radius: 0;
    background: transparent;
    padding-inline: var(--omx-cell-pad-x);
  }
  .omx-table__cell--numeric .omx-editgrid__input { text-align: end; font-variant-numeric: tabular-nums; }

  /* omx-no-press: a text field. Hover says EDITABLE; pressing one places a caret and selects text — a background flash would fight the selection */
  .omx-editgrid__input:hover:not(:disabled) { background: var(--omx-color-control-hover); }

  /* Focus shows the cell's edges. `outline-offset: -2px` keeps the ring INSIDE
   * the cell so it is not clipped by the scroll container — an outline drawn
   * outside the first or last cell is half invisible. */
  .omx-editgrid__input:focus-visible {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: -2px;
    background: var(--omx-color-surface);
    box-shadow: none;
  }

  .omx-editgrid__input[aria-invalid="true"] {
    background: var(--omx-color-danger-bg);
    box-shadow: inset 2px 0 0 var(--omx-color-danger-solid);
  }

  .omx-editgrid__ro {
    display: block;
    padding: 0 var(--omx-cell-pad-x);
    line-height: var(--omx-row-height);
    color: var(--omx-color-text-muted);
  }

  .omx-editgrid__check { margin-inline: var(--omx-cell-pad-x); }

  .omx-editgrid__req { color: var(--omx-color-danger-text); margin-inline-start: 2px; }

  /* ⭐ The lock sits where a per-cell error sits, and reads QUIETER than one.
   * An error is something the reader must fix; a lock is something they
   * cannot. Muted text, no tone colour, no border — the padlock carries the
   * signal and the sentence carries the reason. */
  .omx-editgrid__lock {
    /* 🔴 A TABLE CELL SIZES TO ITS CONTENT, so an unconstrained sentence in a
     * <td> WIDENS THE WHOLE COLUMN. Measured: the grid started spilling its
     * scroller the moment a lock reason went in — the explanation pushing the
     * numbers it explains off the screen. Capped so it wraps instead; a locked
     * line is rare enough that three short lines cost nothing. */
    max-inline-size: 20ch;
    overflow-wrap: break-word;
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-1);
    flex-wrap: wrap;
    margin-block-start: var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-tight);
    text-align: start;
  }

  .omx-editgrid__lock-icon {
    inline-size: 11px;
    block-size: 11px;
    flex: none;
    align-self: center;
    fill: currentColor;
  }

  /* ⭐ The route is the half that turns a refusal into an instruction, so it
   * is not dimmed further — it just reads as a second sentence. */
  .omx-editgrid__lock-route { display: block; flex-basis: 100%; }

    display: block;
    padding: 0 var(--omx-cell-pad-x) var(--omx-space-2);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-danger-text);
  }

  .omx-editgrid__row--invalid .omx-table__cell { background: var(--omx-color-danger-bg); }

  .omx-editgrid__foot {
    display: flex;
    align-items: center;
    gap: var(--omx-space-6);
    flex-wrap: wrap;
  }

  .omx-editgrid__keys { margin: 0; color: var(--omx-color-text-subtle); }
  .omx-editgrid__keys kbd {
    display: inline-block;
    padding: 0 var(--omx-space-2);
    font-family: var(--omx-font-mono);
    font-size: 10px;
    color: var(--omx-color-text-muted);
    background: var(--omx-color-surface-sunken);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-xs);
  }

  @media print {
    /* A printed grid is a document, not a form. */
    .omx-editgrid__foot { display: none; }
    .omx-editgrid__input { border: 0; background: transparent; }
  }
}

/* --- DataDisplay/Freshness/Freshness.css --------------------------- */
/* ==========================================================================
 * L4 · DataDisplay · Freshness — how much to trust the numbers beside it.
 *
 * 🔴 THE STALE STATE IS NOT A COLOUR CHANGE. It adds a WORD. The PROD spec is
 * explicit about why, for the display board in particular: "a floor screen is
 * read at an angle, in poor light, by people who may not distinguish red from
 * green." The same reasoning is WCAG 1.4.1 — colour is never the only carrier.
 * ========================================================================== */

@layer omx.component {

  .omx-freshness {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-2);
    margin: 0;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
    font-variant-numeric: tabular-nums;
  }

  .omx-freshness__icon {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
  }

  .omx-freshness__text time { font-weight: var(--omx-font-weight-medium); }

  /* Hidden when fresh -- present in the DOM for every state so the script only
   * toggles a class and never builds markup. */
  .omx-freshness__stale { display: none; }

  /* -- stale ---------------------------------------------------------------
   * ⭐ The WORD appears, the weight lifts, and the colour changes. Any ONE of
   * the three would be enough for someone; all three together is the point. */
  .omx-freshness--stale {
    color: var(--omx-color-warning-text);
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-freshness--stale .omx-freshness__stale {
    display: inline;
    padding-inline: var(--omx-space-2);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-warning-bg);
    color: var(--omx-color-warning-text);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
  }

  /* -- unknown -------------------------------------------------------------
   * ⚠️ Deliberately NOT the stale styling. "We do not know" and "it is old"
   * demand different responses, so they must not look alike. */
  .omx-freshness--unknown {
    color: var(--omx-color-text-subtle);
    font-style: italic;
  }

  /* -- large ---------------------------------------------------------------
   * For a screen read from across a room. Sized in em so it tracks whatever
   * type scale the board sets, rather than pinning a second scale here. */
  .omx-freshness--large {
    font-size: var(--omx-font-size-body);
    gap: var(--omx-space-3);
  }

  /* A printed page is a snapshot: the age at the moment of printing is part of
   * the record, so nothing is hidden and nothing is toned down. */
  @media print {
    .omx-freshness { color: inherit; }
  }
}

/* --- DataDisplay/KpiCard/KpiCard.css ------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · KpiCard
 * ========================================================================== */

@layer omx.component {

  .omx-kpi {
    position: relative;
    display: grid;
    gap: var(--omx-space-3);
    align-content: start;
    padding: var(--omx-panel-pad);
    min-inline-size: 0;

    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
  }

  .omx-kpi__head {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-kpi__icon {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    flex: 0 0 auto;
    color: var(--omx-color-text-subtle);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
  }

  .omx-kpi__label {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-kpi__value {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-kpi__figure {
    font-size: var(--omx-font-size-metric);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    /* Tabular, always. A column of KPI cards whose digits are proportionally
     * spaced cannot be compared down the page, which is the only reason they
     * are in a row. */
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--omx-letter-spacing-tight);
  }

  .omx-kpi__unit {
    font-size: var(--omx-font-size-body);
    color: var(--omx-color-text-muted);
  }

  /* -- delta ---------------------------------------------------------------
   * THREE signals, because one is never enough: the arrow (shape), the colour,
   * and the sign inside the caller's own text. The shape is what survives a
   * colour-blind reader; the sign is what survives a monochrome print. */
  .omx-kpi__delta {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    font-size: var(--omx-font-size-caption);
    font-variant-numeric: tabular-nums;
    min-inline-size: 0;
  }

  .omx-kpi__arrow { inline-size: 12px; block-size: 12px; flex: 0 0 auto; }

  .omx-kpi__delta--good    { color: var(--omx-color-success-text); }
  .omx-kpi__delta--bad     { color: var(--omx-color-danger-text); }
  .omx-kpi__delta--neutral { color: var(--omx-color-text-muted); }

  .omx-kpi__delta-caption {
    color: var(--omx-color-text-subtle);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-kpi__status { margin-block-start: var(--omx-space-1); }

  /* -- linked --------------------------------------------------------------
   * The label is the link; the overlay makes the whole card its hit area. Same
   * construction as Surfaces/Card, and for the same three reasons. */
  .omx-kpi__link { color: inherit; text-decoration: none; }
  .omx-kpi--linked .omx-kpi__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
  .omx-kpi--linked:hover { border-color: var(--omx-color-border-strong); }
  .omx-kpi--linked:hover .omx-kpi__link { text-decoration: underline; }
  .omx-kpi--linked:has(.omx-kpi__link:active) { border-color: var(--omx-color-border-control); }
  .omx-kpi--linked:has(.omx-kpi__link:focus-visible) {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
  }
  .omx-kpi__link:focus-visible { outline: none; }

  /* A narrow slot loses the caption before it loses the figure. */
  @container omx-slot (max-width: 220px) {
    .omx-kpi__delta-caption { display: none; }
    .omx-kpi__figure { font-size: var(--omx-font-size-display); }
  }

  @media (forced-colors: active) {
    .omx-kpi { border-color: CanvasText; }
  }

  @media print {
    .omx-kpi { border-color: #000; break-inside: avoid; }
  }
}

/* --- DataDisplay/List/List.css ------------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · List — records without columns.
 * ========================================================================== */

@layer omx.component {

  .omx-list { display: grid; gap: var(--omx-space-4); min-inline-size: 0; }

  .omx-list__caption { color: var(--omx-color-text-muted); }

  .omx-list__items { display: grid; min-inline-size: 0; }

  .omx-list__item {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--omx-space-5);
    padding: var(--omx-space-5) var(--omx-space-2);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
    min-inline-size: 0;
  }
  .omx-list__item:last-child { border-block-end: 0; }

  .omx-list--compact .omx-list__item { padding-block: var(--omx-space-4); gap: var(--omx-space-4); }

  .omx-list__lead { flex: 0 0 auto; }

  .omx-list__icon {
    inline-size: var(--omx-size-icon-action);
    block-size: var(--omx-size-icon-action);
    flex: 0 0 auto;
    color: var(--omx-color-text-subtle);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
  }

  .omx-list__body { flex: 1; min-inline-size: 0; display: grid; gap: var(--omx-space-1); }

  .omx-list__title {
    font-size: var(--omx-font-size-body);
    color: var(--omx-color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-list__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-muted);
    min-inline-size: 0;
  }
  .omx-list__sep { color: var(--omx-color-text-subtle); }

  .omx-list__badge  { flex: 0 0 auto; }
  .omx-list__amount { flex: 0 0 auto; font-weight: var(--omx-font-weight-medium); }

  /* The overlay pattern, third use. Title is the link; the row is its hit area. */
  .omx-list__link { text-decoration: none; color: inherit; }
  .omx-list__item--linked .omx-list__link::after { content: ""; position: absolute; inset: 0; }
  .omx-list__item--linked:hover { background: var(--omx-color-control-hover); }
  .omx-list__item--linked:hover .omx-list__link { text-decoration: underline; }
  .omx-list__item--linked:has(.omx-list__link:active) { background: var(--omx-color-control-active); }
  .omx-list__item--linked:has(.omx-list__link:focus-visible) {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: -2px;
  }
  .omx-list__link:focus-visible { outline: none; }

  .omx-list__more { justify-self: start; font-size: var(--omx-font-size-caption); }

  /* In a narrow slot the amount drops below the title rather than squeezing it
   * to nothing — the identity matters more than the figure's alignment. */
  @container omx-slot (max-width: 380px) {
    .omx-list__item { flex-wrap: wrap; }
    .omx-list__amount { margin-inline-start: auto; }
  }

  @media print {
    .omx-list__item { break-inside: avoid; border-block-end-color: #000; }
  }
}

/* --- DataDisplay/Meter/Meter.css ----------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Meter — a level, not progress.
 * ========================================================================== */

@layer omx.component {

  .omx-meter {
    --omx-meter-fill: var(--omx-color-success-text);

    display: grid;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-meter__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  .omx-meter__label {
    font-size: var(--omx-font-size-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-meter__value {
    flex: 0 0 auto;
    font-weight: var(--omx-font-weight-semibold);
    font-variant-numeric: tabular-nums;
  }

  .omx-meter__track {
    block-size: 8px;
    background: var(--omx-color-surface-sunken);
    border-radius: var(--omx-radius-pill);
    overflow: hidden;
  }

  .omx-meter__fill {
    block-size: 100%;
    background: var(--omx-meter-fill);
    border-radius: inherit;
    transition: inline-size var(--omx-duration-normal) var(--omx-easing-out);
  }

  .omx-meter--ok     { --omx-meter-fill: var(--omx-color-success-text); }
  .omx-meter--warn   { --omx-meter-fill: var(--omx-color-warning-text); }
  .omx-meter--danger { --omx-meter-fill: var(--omx-color-danger-solid); }

  .omx-meter__band { color: var(--omx-color-text-muted); }
  .omx-meter--warn   .omx-meter__band { color: var(--omx-color-warning-text); }
  .omx-meter--danger .omx-meter__band { color: var(--omx-color-danger-text); }

  .omx-meter--compact { grid-template-columns: 1fr auto; align-items: center; gap: var(--omx-space-5); }
  .omx-meter--compact .omx-meter__head  { display: contents; }
  .omx-meter--compact .omx-meter__track { grid-column: 1; }
  .omx-meter--compact .omx-meter__label { grid-column: 1; grid-row: 1; }
  .omx-meter--compact .omx-meter__value { grid-column: 2; grid-row: 1 / span 2; }

  @media (forced-colors: active) {
    /* HCM removes the fill colour, so the bands become indistinguishable — the
     * BAND LABEL is what still works, which is why it is text and not a tint. */
    .omx-meter__track { border: 1px solid CanvasText; }
    .omx-meter__fill  { background: Highlight; }
  }

  @media print {
    /* The figure and the band print; the bar is decoration without colour. */
    .omx-meter__track { display: none; }
  }
}

/* --- DataDisplay/Pivot/Pivot.css ----------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Pivot — a crosstab. Inherits Table's geometry.
 * ========================================================================== */

@layer omx.component {

  .omx-pivot { min-inline-size: 0; }

  .omx-pivot__scroll {
    /* overflow + containment + gutter come from [data-omx-scroll]. A twelve-month
     * crosstab scrolls HERE, not on the page. */
    max-inline-size: 100%;
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
  }

  /* The row-header column is frozen: scrolling sideways through twelve months
   * with the account name gone is scrolling through anonymous numbers. */
  .omx-pivot__row-header,
  .omx-pivot__corner {
    position: sticky;
    inset-inline-start: 0;
    z-index: var(--omx-z-raised);
    background: var(--omx-color-surface);
    text-align: start;
    font-weight: var(--omx-font-weight-regular);
    white-space: nowrap;
  }
  /* The corner is sticky on BOTH axes, so it must out-rank each of them. */
  .omx-pivot__corner { z-index: calc(var(--omx-z-raised) + 2); }
  .omx-table--sticky .omx-table__head .omx-pivot__corner { inset-block-start: 0; }

  .omx-pivot__cell--empty { color: var(--omx-color-text-subtle); }

  .omx-pivot__row--subtotal > * {
    font-weight: var(--omx-font-weight-semibold);
    background: var(--omx-color-surface-sunken);
  }

  .omx-pivot__total-col {
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-border);
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-pivot__grand {
    font-weight: var(--omx-font-weight-bold);
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-border);
  }

  @media print {
    .omx-pivot__scroll { overflow: visible; border-color: #000; }
    .omx-pivot__row-header, .omx-pivot__corner { position: static; }
  }
}

/* --- DataDisplay/Reconciliation/Reconciliation.css ----------------- */
@layer omx.component {

  /* L4 · DataDisplay · Reconciliation — several sources for one figure.
   *
   * ⭐ It IS a table and looks like one: it reuses the table's own classes,
   * as Permissions and EditGrid do. What is added is only what a
   * reconciliation says that a table does not. */

  .omx-recon__caption,
  .omx-recon__cannot,
  .omx-recon__empty { margin: 0; }

  /* The sign convention and the omitted periods sit IN the caption — the
   * table's accessible name — so a screen reader hears both before any figure. */
  .omx-recon__convention,
  .omx-recon__omitted {
    display: block;
    margin-block-start: var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-regular);
  }

  .omx-recon__tag,
  .omx-recon__nosplit {
    display: block;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-regular);
  }

  /* 🔴 The half-width leg's reason is allowed to wrap: a one-column group with a
   * sentence in its header must not force that column wide. */
  .omx-recon__nosplit { max-inline-size: 18ch; }

  /* ⭐ Residual groups are set apart from the legs by a structure-register rule,
   * so the eye does not read a difference as one more source's figure. */
  .omx-recon__resid-head,
  .omx-recon__resid {
    border-inline-start: var(--omx-border-width) solid var(--omx-color-border-strong);
  }

  /* ⛔ THE SIGN IS TEXT, NOT COLOUR. Neither direction is "wrong", so a residual
   * is never painted red or green. A non-zero residual reads a little stronger —
   * it is what someone has to explain — and zero reads plain, never hidden. */
  .omx-recon__resid[data-omx-residual="positive"],
  .omx-recon__resid[data-omx-residual="negative"] {
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-recon__missing { text-align: start; }
}

/* --- DataDisplay/Revision/Revision.css ----------------------------- */
/* ==========================================================================
 * L4 · DataDisplay · Revision — what is proposed, against what stands.
 *
 * ⛔ THE PROPOSED COLUMN IS NOT PAINTED GREEN AND THE ORIGINAL IS NOT PAINTED
 * RED. This is not a code diff. A proposal is not an improvement until someone
 * says so, and colouring it as one pre-answers the question the screen exists
 * to ask.
 *
 * ⭐ THE STRUCTURE REGISTER under the head, and again above the summary: the
 * column boundary is a fact about the table's shape, the same class as the
 * worksheet's total rule.
 *
 * ⚠️ THE NARROW CASE IS HANDLED UP FRONT, not discovered later. The worksheet
 * shipped a 63px prose column before this was measured; a four-column revision
 * table is worse, so it stacks at 640 rather than 560.
 * ========================================================================== */

@layer omx.component {

  .omx-revision__table {
    inline-size: 100%;
    border-collapse: collapse;
    font-size: var(--omx-font-size-body);
  }

  .omx-revision__caption {
    text-align: start;
    color: var(--omx-color-text-muted);
    padding-block-end: var(--omx-space-3);
  }

  .omx-revision__table thead th {
    text-align: start;
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text-muted);
    padding-block-end: var(--omx-space-2);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-strong);
  }

  .omx-revision__row > * {
    padding-block: var(--omx-space-3);
    padding-inline-end: var(--omx-space-5);
    vertical-align: baseline;
    border-block-end: var(--omx-border-width) solid var(--omx-color-border);
  }

  .omx-revision__field {
    text-align: start;
    font-weight: var(--omx-font-weight-regular);
    color: var(--omx-color-text);
  }

  .omx-revision__value {
    overflow-wrap: anywhere;
  }

  /* Hidden while the columns exist to name themselves.
     ⚠️ TWO RULES, ONE ELEMENT EACH, and deliberately not one rule on the shared
     class. C17 guards the defect that removed sign-out from every desktop --
     `omx-topbar__menu` was worn by two elements and a rule written for one hid
     both. These two are a deliberate pair rather than a collision, but "the
     gate cannot tell the difference" is not a reason to widen the gate. */
  .omx-revision__side-label--original { display: none; }
  .omx-revision__side-label--proposed { display: none; }

  /* ⚠️ A MARK, NOT A CLASSIFICATION. It says the two strings differ, which is a
     mechanical fact — a Slipped line carries none of this and is still the most
     serious change on the page. ⛔ Never colour: colour would read as a verdict,
     and the verdict lives one column to the right, in words. */
  .omx-revision__row[data-omx-line="classified"] .omx-revision__side--proposed .omx-revision__value,
  .omx-revision__row[data-omx-line="unjudged"] .omx-revision__side--proposed .omx-revision__value {
    font-weight: var(--omx-font-weight-medium);
  }

  /* 🔴 It differs and nobody has judged it. Stated in words, because "unlabelled"
     and "labelled harmless" must not look alike to someone about to approve. */
  .omx-revision__unjudged {
    font-style: italic;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
  }

  .omx-revision__verdict {
    inline-size: 1%;
    white-space: nowrap;
  }

  .omx-revision__summary {
    margin: var(--omx-space-3) 0 0;
    color: var(--omx-color-text-muted);
  }

  /* ⚠️ Four columns do not survive a phone. Stacking at 640, not 560: the
     worksheet needed 560 for two columns, and this has four. */
  @container omx-content (max-width: 640px) {
    /* ⚠️ The caption belongs in this list too. Left out of it, a <caption>
       inside a display:block table shrink-wraps and renders one word per line --
       the same failure the worksheet's figure column had, in a different place. */
    .omx-revision__caption,
    .omx-revision__table,
    .omx-revision__row,
    .omx-revision__field,
    .omx-revision__side,
    .omx-revision__verdict {
      display: block;
      inline-size: auto;
      white-space: normal;
    }

    /* 🔴 CLIPPED, NOT display:none. The head is the only thing that says which
       column is which, and `display: none` would take it out of the
       ACCESSIBILITY TREE as well -- so a screen reader would lose the column
       association at the same moment a sighted reader loses the heading. It
       stays in the tree; each side also labels itself visibly below. */
    .omx-revision__table thead {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    .omx-revision__row {
      padding-block: var(--omx-space-4);
      border-block-end: var(--omx-border-width) solid var(--omx-color-border);
    }

    .omx-revision__row > * {
      border-block-end: 0;
      padding-block: 0;
      padding-inline-end: 0;
    }

    .omx-revision__field {
      font-weight: var(--omx-font-weight-medium);
      margin-block-end: var(--omx-space-1);
    }

    .omx-revision__side {
      margin-block-start: var(--omx-space-1);
    }

    /* The side's own label, shown only now that the column heading is not
       visible. A real element rather than generated content, so it can be
       translated with everything else. */
    .omx-revision__side-label--original,
    .omx-revision__side-label--proposed {
      display: inline;
      margin-inline-end: var(--omx-space-2);
      font-size: var(--omx-font-size-caption);
      color: var(--omx-color-text-muted);
    }

    .omx-revision__verdict {
      margin-block-start: var(--omx-space-2);
    }
  }

  @media (forced-colors: active) {
    .omx-revision__table thead th,
    .omx-revision__row > * { border-block-end-color: CanvasText; }
  }

}

/* --- DataDisplay/SequenceLane/SequenceLane.css --------------------- */
@layer omx.component {

  /* L4 · DataDisplay · SequenceLane — the order within a lane, and what it costs.
   *
   * ⭐ A vertical list, because sequence reads top to bottom and a screen reader
   * reads an <ol> as "3 of 5" — which is the question the component answers. */
  .omx-lane {
    display: grid;
    gap: var(--omx-space-3);
    max-inline-size: 48rem;
  }

  .omx-lane__caption,
  .omx-lane__aside,
  .omx-lane__sort,
  .omx-lane__summary { margin: 0; }

  .omx-lane__aside {
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  /* 🔴 THE PENDING BAR — the same shape as the assign board's, on purpose: two
   * components with one contract should look like one contract. */
  .omx-lane__pending {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-2) var(--omx-space-4);
    padding: var(--omx-space-2) var(--omx-space-3);
    border: var(--omx-border-width) dashed var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
  }

  .omx-lane__pending-count,
  .omx-lane__pending-unsaved,
  .omx-lane__pending-readonly { margin: 0; }

  .omx-lane__pending-effect {
    margin-inline-start: var(--omx-space-2);
    color: var(--omx-color-text-muted);
  }

  .omx-lane__pending-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-2);
    align-items: end;
    margin-inline-start: auto;
  }

  .omx-lane__why-label {
    flex-basis: 100%;
    font-size: var(--omx-font-size-caption);
  }

  .omx-lane__refused {
    flex-basis: 100%;
    margin: 0;
    padding-inline-start: var(--omx-space-4);
    color: var(--omx-color-danger-text);
  }

  .omx-lane__empty { margin: 0; }

  .omx-lane__list {
    display: grid;
    gap: var(--omx-space-2);
    margin: 0;
    padding-inline-start: var(--omx-space-5);
  }

  .omx-lane__item {
    padding: var(--omx-space-2) var(--omx-space-3);
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-surface);
  }

  /* ⛔ NOT BY COLOUR ALONE. A moved item gets a DASHED edge, a shape difference
   * that survives greyscale, and a line of text. */
  .omx-lane__item[data-omx-tentative] {
    border-style: dashed;
  }

  .omx-lane__changeover {
    margin: 0 0 var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  /* ⛔ FLAGGED, NEVER BLOCKED — and flagged in WORDS first. The warning tone is
   * the second signal, never the only one. */
  .omx-lane__reversal {
    margin-inline-end: var(--omx-space-2);
    color: var(--omx-color-warning-text);
  }

  .omx-lane__body {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--omx-space-1) var(--omx-space-3);
  }

  .omx-lane__label { font-weight: var(--omx-font-weight-medium); }

  .omx-lane__detail,
  .omx-lane__rank,
  .omx-lane__mins,
  .omx-lane__tentative {
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

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

  .omx-lane__nudge {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-3);
    margin-block-start: var(--omx-space-1);
    font-size: var(--omx-font-size-caption);
  }

  /* The drop cue while dragging — drag is the enhancement, the links above are
   * the mechanism. */
  .omx-lane__item[data-omx-lane-over] {
    outline: var(--omx-border-width-strong) dashed var(--omx-color-accent);
    outline-offset: 2px;
  }
}

/* --- DataDisplay/Suggestions/Suggestions.css ----------------------- */
@layer omx.component {

  /* L4 · DataDisplay · Suggestions — options ranked for one exact question.
   *
   * ⭐ Every rank carries its arithmetic in plain text — the weight is a
   * number and a word, never a colour. ⛔ No tone for confidence: a green
   * "high" would make a claim the number already makes, and a red "low"
   * would read as a warning about an option that may still be the best one. */

  .omx-suggest {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-suggest__caption {
    margin: 0;
    font-weight: var(--omx-font-weight-semibold);
  }

  /* The question, part by part. */
  .omx-suggest__ask {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-5);
    margin: 0;
  }

  .omx-suggest__ask-part {
    display: flex;
    gap: var(--omx-space-2);
  }

  .omx-suggest__ask dt { color: var(--omx-color-text-muted); }
  .omx-suggest__ask dd { margin: 0; }

  /* ⛔ A refusal is SEEN, where the ranking would have been. */
  .omx-suggest__refused {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    padding: var(--omx-space-3);
    border: var(--omx-border-width) dashed var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
  }

  .omx-suggest__refused p { margin: 0; }

  .omx-suggest__missing {
    margin: 0;
    padding-inline-start: var(--omx-space-5);
  }

  .omx-suggest__not-weighed {
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--omx-font-size-caption);
  }

  .omx-suggest__counts,
  .omx-suggest__none,
  .omx-suggest__note {
    margin: 0;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  /* The ranking. The position is written out, so the list keeps no marker. */
  .omx-suggest__list {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .omx-suggest__item {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-1);
    padding: var(--omx-space-2) var(--omx-space-3);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    border-radius: var(--omx-radius-sm);
  }

  .omx-suggest__head {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-suggest__pos,
  .omx-suggest__score {
    font-weight: var(--omx-font-weight-semibold);
    font-variant-numeric: tabular-nums;
  }

  .omx-suggest__label { overflow-wrap: anywhere; }

  .omx-suggest__head > .omx-suggest__label {
    flex: 1 1 auto;
    min-inline-size: 0;
  }

  .omx-suggest__tied,
  .omx-suggest__math,
  .omx-suggest__basis,
  .omx-suggest__because,
  .omx-suggest__pick {
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-suggest__math,
  .omx-suggest__basis { margin: 0; }

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

  .omx-suggest__because {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .omx-suggest__pick { align-self: flex-start; }

  /* ⛔ The excluded are NOT in the list: their own disclosure, each naming the
   * dimension it fails on. */
  .omx-suggest__excluded-list {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-1);
    margin: var(--omx-space-2) 0 0;
    padding-inline-start: var(--omx-space-5);
  }

  .omx-suggest__excluded-list > li {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--omx-space-3);
  }

  .omx-suggest__reason { color: var(--omx-color-text-muted); }
}

/* --- DataDisplay/Table/Table.css ----------------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Table
 *
 * Row height, cell padding and type all come from the density tokens, so the
 * same table is 36px comfortable / 30px cozy / 26px compact and 44px on a phone
 * without this file knowing any of those numbers.
 * ========================================================================== */

@layer omx.component {

  /* ⭐ Quieter than the caption it sits under — it is a footnote to the table's
   * name, not a second title. ⛔ No tone colour: a column that was never
   * measured is a fact about the data, not an error the reader caused. */
  .omx-table__withheld {
    display: block;
    margin-block-start: var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-tight);
    font-weight: var(--omx-font-weight-regular);
  }

  .omx-table__withheld-label { margin-inline-end: var(--omx-space-1); }

  /* Each withheld column reads as one unit, so two of them cannot run together
   * into a sentence that looks like one column with a long reason. */
  .omx-table__withheld-col { display: inline-block; margin-inline-end: var(--omx-space-3); }

  .omx-table__withheld-why { font-style: italic; }

  /* -- hierarchy ------------------------------------------------------------
   * ⭐ INDENT IS A SPACER ELEMENT, not padding on the cell. A frozen first
   * column and a numeric alignment both already own that cell's padding, and
   * stacking a depth multiplier on top of them breaks whichever one loses.
   * A spacer composes with both and cannot fight them. */
  .omx-table__indent {
    display: inline-block;
    inline-size: calc(var(--omx-depth, 0) * var(--omx-space-5));
    flex: none;
  }

  /* The control is a link, so it must not look like the row's own link. */
  .omx-table__twisty {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--omx-space-6);
    block-size: var(--omx-space-6);
    margin-inline-end: var(--omx-space-1);
    vertical-align: middle;
    border-radius: var(--omx-radius-sm);
    color: var(--omx-color-text-muted);
    text-decoration: none;
  }

  .omx-table__twisty:hover {
    background: var(--omx-color-control-hover);
    color: var(--omx-color-text);
    text-decoration: none;
  }
  .omx-table__twisty:active { background: var(--omx-color-control-active); }

  /* Keeps a leaf's text on the same line as its siblings' text rather than
   * against their control. */
  .omx-table__twisty-spacer {
    display: inline-block;
    inline-size: calc(var(--omx-space-6) + var(--omx-space-1));
    vertical-align: middle;
  }

  .omx-table__twisty-icon {
    inline-size: 0.85em;
    block-size: 0.85em;
    /* ⚠️ A rotation, not a second icon: one glyph cannot get out of step with
     * the state, and `prefers-reduced-motion` only has one thing to disable. */
    transition: transform var(--omx-duration-fast) var(--omx-easing-standard);
  }

  /* 🔴 A DESCENDANT SELECTOR, not a child one. `aria-expanded` sits on the
   * <tr>; the twisty is inside a <td>, so it is a GRANDCHILD. The first version
   * used `>` and never matched -- every expanded row kept a collapsed chevron,
   * which is the one thing the chevron exists to say. Nothing errored, the rows
   * were expanded, and the control simply lied about it.
   * ⭐ Found by looking at the rendered table, not by reading the selector. */
  [aria-expanded="true"] .omx-table__twisty-icon {
    transform: rotate(90deg);
  }

  @media (prefers-reduced-motion: reduce) {
    .omx-table__twisty-icon { transition: none; }
  }

  /* ⭐ A child row is tinted, not just indented. On a wide monitor the first
   * column can be far from the eye tracking a number on the right, and indent
   * alone is invisible out there. */
  .omx-table__row--child > .omx-table__cell {
    background: var(--omx-color-surface-sunken);
  }


  .omx-table__wrap {
    /* overscroll containment + a stable gutter come from [data-omx-scroll].
     * A wide ledger scrolls sideways HERE, not on the page — a table that makes
     * the whole page scroll horizontally takes the shell with it. */
    /* overflow comes from [data-omx-scroll] in Foundation/a11y.css, together
     * with overscroll containment and a stable gutter. Setting it HERE as
     * well meant the class could scroll without the containment if the
     * attribute were ever omitted — `check-omx` C9 named it. */
    max-inline-size: 100%;
  }

  .omx-table {
    inline-size: 100%;
    border-collapse: separate;   /* separate, or sticky headers lose their border */
    border-spacing: 0;
    font-size: var(--omx-font-size-body);
  }

  .omx-table__caption {
    padding: var(--omx-cell-pad-y) var(--omx-cell-pad-x);
    text-align: start;
    font-size: var(--omx-font-size-label);
    color: var(--omx-color-text-muted);
  }

  /* -- cells ---------------------------------------------------------------- */
  .omx-table__cell {
    padding: var(--omx-cell-pad-y) var(--omx-cell-pad-x);
    text-align: start;
    vertical-align: middle;
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-table__cell--end    { text-align: end; }
  .omx-table__cell--center { text-align: center; }

  /* Figures must be tabular AND end-aligned. Either alone leaves a column that
   * still cannot be scanned down. */
  .omx-table__cell--numeric { font-variant-numeric: tabular-nums; }

  /* 🔴 AN ABSENT CELL LEAVES THE NUMERIC RHYTHM. End-aligned tabular figures
   * are what make a money column scannable; a REASON sitting in that same
   * rhythm reads as one more figure, which is the exact confusion the Absence
   * type exists to stop.
   * ⚠️ Must come AFTER `--end`: both are one class, so this is decided by
   * source order, not specificity. Moving it up silently stops it working. */
  .omx-table__cell--absent { text-align: start; }

  .omx-table__cell--select  { inline-size: 1px; padding-inline-end: 0; }
  .omx-table__cell--actions { inline-size: 1px; white-space: nowrap; }

  /* -- header --------------------------------------------------------------- */
  .omx-table__th {
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    color: var(--omx-color-text-muted);
    white-space: nowrap;
    background: var(--omx-color-surface);
    border-block-end-color: var(--omx-color-border);
  }

  .omx-table--sticky .omx-table__head th {
    position: sticky;
    inset-block-start: 0;
    /* Above the rows, below everything in the shell. */
    z-index: var(--omx-z-raised);
  }

  .omx-table__sort {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-2);
    color: inherit;
    text-decoration: none;
  }
  .omx-table__sort:hover { color: var(--omx-color-text); text-decoration: none; }
  .omx-table__sort:active { background: var(--omx-color-control-active); }

  .omx-table__sort-icon {
    inline-size: 10px; block-size: 10px; flex: 0 0 auto;
    opacity: 0.32;
  }
  /* The state is read off aria-sort, never off a class — so an unstyled sorted
   * column and an unannounced one are the same bug. */
  .omx-table__th[aria-sort="ascending"]  .omx-table__sort-icon,
  .omx-table__th[aria-sort="descending"] .omx-table__sort-icon { opacity: 1; }
  .omx-table__th[aria-sort="ascending"]  .omx-table__sort-icon path:last-child,
  .omx-table__th[aria-sort="descending"] .omx-table__sort-icon path:first-child { opacity: 0.25; }
  .omx-table__th[aria-sort="ascending"],
  .omx-table__th[aria-sort="descending"] { color: var(--omx-color-text); }

  /* -- rows ----------------------------------------------------------------- */
  /* omx-no-press: the row is a plain <tr> with no href, no button and no row-click. This highlight is a READING AID for tracking one record across columns, not an affordance */
  .omx-table__row:hover .omx-table__cell { background: var(--omx-color-control-hover); }

  .omx-table__row[aria-selected="true"] .omx-table__cell {
    background: var(--omx-color-accent-subtle);
  }

  .omx-table__link { color: var(--omx-color-text-link); }

  /* Actions appear on hover or focus — but focus matters more: hover-only
   * actions are invisible to a keyboard and absent on touch, so they are also
   * revealed whenever anything inside the row has focus. */
  .omx-table__actions { display: flex; gap: var(--omx-space-2); opacity: 0; }
  /* omx-no-press: an opacity REVEAL, not a state. The buttons it uncovers have their own press */
  .omx-table__row:hover .omx-table__actions,
  .omx-table__row:focus-within .omx-table__actions { opacity: 1; }
  @media (pointer: coarse) {
    /* No hover on touch. Always visible. */
    .omx-table__actions { opacity: 1; }
  }

  /* -- frozen column -------------------------------------------------------- */
  .omx-table__cell--frozen {
    position: sticky;
    inset-inline-start: 0;
    z-index: var(--omx-z-raised);
    background: var(--omx-color-surface);
  }
  .omx-table--sticky .omx-table__head .omx-table__cell--frozen { z-index: calc(var(--omx-z-raised) + 1); }
  /* omx-no-press: same reading aid, repeated for the frozen column so the band does not break at the freeze line */
  .omx-table__row:hover .omx-table__cell--frozen { background: var(--omx-color-control-hover); }

  /* -- totals --------------------------------------------------------------- */
  .omx-table__foot .omx-table__cell {
    font-weight: var(--omx-font-weight-semibold);
    background: var(--omx-color-surface-sunken);
    border-block-start: var(--omx-border-width-strong) solid var(--omx-color-border);
    border-block-end: 0;
    position: sticky;
    inset-block-end: 0;
  }

  /* -- empty ---------------------------------------------------------------- */
  .omx-table__empty {
    padding: var(--omx-space-11) var(--omx-cell-pad-x);
    text-align: center;
    color: var(--omx-color-text-muted);
    border-block-end: 0;
  }

  /* ===========================================================================
   * RESPONSIVE — columns drop by PRIORITY, on the SLOT's width.
   *
   * ⭐ Hidden, not removed: `visibility` would still reserve the cell, and
   * `display:none` on a <td> is the only thing that actually collapses a table
   * column — but the cell stays in the DOM, so print and the accessibility tree
   * keep the whole row. Priority 0 never drops.
   * ======================================================================== */
  @container omx-slot (max-width: 900px) {
    .omx-table__cell[data-omx-priority="3"] { display: none; }
  }
  @container omx-slot (max-width: 700px) {
    .omx-table__cell[data-omx-priority="2"],
    .omx-table__cell[data-omx-priority="3"] { display: none; }
  }
  @container omx-slot (max-width: 520px) {
    .omx-table__cell[data-omx-priority="1"],
    .omx-table__cell[data-omx-priority="2"],
    .omx-table__cell[data-omx-priority="3"] { display: none; }
  }

  @media print {
    .omx-table__wrap { overflow: visible; }
    /* Every column prints, whatever the screen dropped. */
    .omx-table__cell[data-omx-priority] { display: table-cell !important; }
    .omx-table__cell--actions { display: none !important; }
    .omx-table__head th { position: static; background: transparent; }
    .omx-table__row { break-inside: avoid; }
    /* Repeat the header on every printed page — a ledger that spans four sheets
     * has three sheets of unlabelled numbers otherwise. */
    .omx-table__head { display: table-header-group; }
    .omx-table__foot { display: table-footer-group; }
  }

  @media (forced-colors: active) {
    .omx-table__cell { border-block-end-color: CanvasText; }
    .omx-table__row[aria-selected="true"] .omx-table__cell { background: Highlight; color: HighlightText; }
  }
}

/* --- DataDisplay/Timeline/Timeline.css ----------------------------- */
/* =============================================================================
 * L4 · DataDisplay · Timeline — the order IS the information.
 * ========================================================================== */

@layer omx.component {

  .omx-timeline { min-inline-size: 0; }

  .omx-timeline__list { display: grid; gap: var(--omx-space-7); min-inline-size: 0; }
  .omx-timeline--compact .omx-timeline__list { gap: var(--omx-space-5); }

  .omx-timeline__event {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  /* The spine. A pseudo-element on the event rather than a separate element, so
   * the event count and the spine count cannot drift apart. */
  .omx-timeline__event:not(:last-child)::before {
    content: "";
    position: absolute;
    inset-block-start: 18px;
    inset-inline-start: 8px;
    inline-size: 2px;
    block-size: calc(100% + var(--omx-space-7) - 18px);
    background: var(--omx-color-border);
  }
  .omx-timeline--compact .omx-timeline__event:not(:last-child)::before {
    block-size: calc(100% + var(--omx-space-5) - 18px);
  }

  .omx-timeline__marker {
    position: relative;
    z-index: var(--omx-z-raised);
    display: grid;
    place-items: center;
    inline-size: 18px;
    block-size: 18px;
    margin-block-start: 1px;
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-surface);
    border: var(--omx-border-width-strong) solid var(--omx-color-border-strong);
    color: var(--omx-color-text-muted);
  }
  .omx-timeline__icon { inline-size: 10px; block-size: 10px; fill: none; stroke: currentColor; stroke-width: 1.6; }

  .omx-timeline__body { display: grid; gap: var(--omx-space-2); min-inline-size: 0; }

  .omx-timeline__head {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    flex-wrap: wrap;
    min-inline-size: 0;
  }

  .omx-timeline__title {
    font-size: var(--omx-font-size-body);
    color: var(--omx-color-text);
    min-inline-size: 0;
  }

  .omx-timeline__detail { color: var(--omx-color-text-muted); max-inline-size: var(--omx-measure); }

  .omx-timeline__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-subtle);
  }
  .omx-timeline__actor { color: var(--omx-color-text-muted); }

  @media print {
    /* 🔴 An audit trail is EVIDENCE. It prints whole, and every entry stays on
     * one page so a timestamp is never separated from what it timestamps. */
    .omx-timeline__event { break-inside: avoid; }
    .omx-timeline__event::before { background: #000; }
  }
}

/* --- DataDisplay/Worksheet/Worksheet.css --------------------------- */
/* ==========================================================================
 * L4 · DataDisplay · Worksheet — how a number was arrived at.
 *
 * 🔴 THE TOTAL ROW IS SEPARATED BY THE STRUCTURE REGISTER, not by a heavier
 * font alone. A total that differs only in weight is a total people skim past,
 * and this is the one row on the sheet that is either an answer or a refusal.
 *
 * ⛔ A WITHHELD TOTAL IS NOT PAINTED AS AN ERROR. Nothing has gone wrong —
 * something is not known — and a red row trains people to dismiss it. It is
 * the same italic muted treatment every other absence in this package wears.
 * ========================================================================== */

@layer omx.component {

  .omx-worksheet {
    inline-size: 100%;
    border-collapse: collapse;
    font-size: var(--omx-font-size-body);
  }

  .omx-worksheet__caption {
    text-align: start;
    color: var(--omx-color-text-muted);
    padding-block-end: var(--omx-space-3);
  }

  .omx-worksheet__row > * {
    padding-block: var(--omx-space-2);
    vertical-align: baseline;
  }

  .omx-worksheet__label {
    text-align: start;
    font-weight: var(--omx-font-weight-regular);
    color: var(--omx-color-text);
    padding-inline-end: var(--omx-space-5);
  }

  /* An aside about where the figure came from — a rate, a basis. Quieter than
     the line it explains, and on its own row so a long one does not push the
     figure off. */
  .omx-worksheet__note {
    display: block;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-subtle);
  }

  /* ⭐ FIGURES ARE A COLUMN. End-aligned and tabular, so digit 3 of one row
     sits above digit 3 of the next and the sheet can be checked by eye — which
     is the only reason anyone opens a worksheet. */
  .omx-worksheet__figure {
    text-align: end;
    font-variant-numeric: tabular-nums;
    inline-size: 1%;
    white-space: nowrap;
  }

  /* ⚠️ A missing term's reason is PROSE in a numeric column, so it gets the
     column's alignment but not its figures — absence.css already turns tabular
     numerals off, and this stops the sentence being forced onto one line. */
  .omx-worksheet__figure:has(.omx-absent) {
    white-space: normal;
    text-align: end;
  }

  .omx-worksheet__row--total > * {
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-strong);
    padding-block-start: var(--omx-space-3);
  }

  .omx-worksheet__row--total .omx-worksheet__label,
  .omx-worksheet__row--total .omx-worksheet__amount {
    font-weight: var(--omx-font-weight-semibold);
  }

  /* ⭐ The named terms. This is the half that turns a refusal into a route, so
     it is not hidden behind a tooltip or a hover. */
  .omx-worksheet__why {
    display: block;
    margin-block-start: var(--omx-space-1);
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-regular);
    color: var(--omx-color-text-muted);
    white-space: normal;
    text-align: end;
  }

  /* \U0001f534 MEASURED AT 420px: the figure column was 63px against a 257px label
     column, because `inline-size: 1%` shrink-wraps it to the widest NUMBER. A
     missing term's reason then wrapped to ONE WORD PER LINE, and the total's
     refusal rendered 183px tall -- the most important sentence on the sheet,
     rendered worst.
     ⭐ So the row STACKS. Column alignment is what a worksheet is for, but only
     while there is room for a column; below that, being readable wins.
     ⚠️ The roles are written out in the markup because `display: block` strips
     the implicit ones from table elements in several browsers -- the fix would
     otherwise quietly cost the screen-reader reading the <table> was chosen for. */
  @container omx-content (max-width: 560px) {
    .omx-worksheet,
    .omx-worksheet__row,
    .omx-worksheet__label,
    .omx-worksheet__figure {
      display: block;
      inline-size: auto;
    }

    .omx-worksheet__row {
      padding-block: var(--omx-space-2);
    }

    .omx-worksheet__label,
    .omx-worksheet__figure {
      text-align: start;
      white-space: normal;
      padding-inline: 0;
      padding-block: 0;
    }

    .omx-worksheet__figure {
      margin-block-start: var(--omx-space-1);
    }

    /* The figure carries the weight now that it is no longer in a column of
       its own -- otherwise a stacked label and value read as two stray texts. */
    .omx-worksheet__amount {
      font-weight: var(--omx-font-weight-medium);
    }

    /* ⚠️ The :has() rule above is (0,2,0) and beats a bare class even from
       inside this container query, so the reason stayed end-aligned and read as
       a stray indent once the row stacked. Matched here on purpose. */
    .omx-worksheet__figure:has(.omx-absent),
    .omx-worksheet__why {
      text-align: start;
    }
  }

  @media (forced-colors: active) {
    .omx-worksheet__row--total > * { border-block-start-color: CanvasText; }
  }

  @media print {
    /* A withheld total is exactly the line that must survive a photocopy — it
       is the difference between "the cost is X" and "the cost is not known". */
    .omx-worksheet__why { color: #000; }
  }

}

/* --- DataEntry/ColorPicker/ColorPicker.css ------------------------- */
/* ==========================================================================
 * L5 · DataEntry · ColorPicker.
 *
 * ⭐⭐ THE FIELD IS A <canvas> AND THE CONTROLS ARE REAL INPUTS. Everything
 * coloured here is painted by ColorPicker.js, which is why this file declares
 * almost no colour of its own — the gradient, the out-of-gamut hatching and the
 * shaded "we will darken this" band are pixels, not rules.
 *
 * ⚠️ The two swatches and the preset dots take their colour from a custom
 * property set inline (`--omx-cp-preset`, and the swatches from JS). That is a
 * VALUE arriving as data, not a literal in this file — which is the distinction
 * C6 is drawing.
 * ========================================================================== */

@layer omx.component {

  .omx-cp {
    display: grid;
    gap: var(--omx-space-4);
    max-inline-size: 42rem;
  }

  .omx-cp__label {
    display: block;
    color: var(--omx-color-text);
  }

  /* The stage and the sliders sit side by side when there is room, and stack
   * when there is not. A container query, not a media query: this control can
   * be dropped in a narrow drawer on a wide screen. */
  .omx-cp__body {
    display: grid;
    gap: var(--omx-space-5);
    grid-template-columns: 1fr;
  }

  @container (min-width: 34rem) {
    .omx-cp__body { grid-template-columns: auto 1fr; align-items: start; }
  }

  .omx-cp__stage {
    position: relative;
    display: grid;
    gap: var(--omx-space-2);
    inline-size: fit-content;
  }

  .omx-cp__field,
  .omx-cp__hue {
    display: block;
    inline-size: 280px;
    max-inline-size: 100%;
    border-radius: var(--omx-radius-md);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    cursor: crosshair;
    touch-action: none;   /* the drag is ours; do not scroll the page under it */
  }

  .omx-cp__hue { block-size: 14px; cursor: ew-resize; }

  /* ⚠️ The thumb is a RING, never a filled dot: a filled marker hides the one
   * pixel the person is trying to judge. Two rings so it stays visible on both
   * a pale and a dark field. */
  .omx-cp__thumb {
    position: absolute;
    inline-size: 14px;
    block-size: 14px;
    border-radius: var(--omx-radius-pill);
    border: 2px solid var(--omx-color-surface);
    box-shadow: 0 0 0 1px var(--omx-color-text), inset 0 0 0 1px var(--omx-color-text);
    transform: translate(-50%, -50%);
    pointer-events: none;
    inset-block-start: 0;
    inset-inline-start: 0;
  }

  .omx-cp__controls { display: grid; gap: var(--omx-space-3); align-content: start; }

  .omx-cp__row {
    display: grid;
    grid-template-columns: 5.5rem 1fr;
    align-items: center;
    gap: var(--omx-space-3);
  }

  .omx-cp__slider-label { color: var(--omx-color-text-muted); }

  .omx-cp__range {
    inline-size: 100%;
    min-block-size: var(--omx-size-touch);   /* WCAG 2.5.8 — the target, not the track */
    accent-color: var(--omx-color-accent);
    background: none;
  }

  .omx-cp__hex { font-family: var(--omx-font-mono); text-transform: lowercase; }

  /* ---- picked vs shipped ------------------------------------------------- */
  .omx-cp__verdict {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    flex-wrap: wrap;
    padding: var(--omx-space-3) var(--omx-space-4);
    background: var(--omx-color-surface-sunken);
    border-radius: var(--omx-radius-md);
  }

  .omx-cp__chip { display: flex; align-items: center; gap: var(--omx-space-2); }

  .omx-cp__swatch {
    inline-size: var(--omx-size-icon-feature);
    block-size: var(--omx-size-icon-feature);
    border-radius: var(--omx-radius-sm);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    background: var(--omx-cp-swatch, transparent);
    flex: 0 0 auto;
  }

  .omx-cp__arrow {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    color: var(--omx-color-text-subtle);
    flex: 0 0 auto;
  }

  .omx-cp__grade {
    margin-inline-start: auto;
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    color: var(--omx-color-text-muted);
  }

  /* ---- presets ------------------------------------------------------------ */
  .omx-cp__presets {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    flex-wrap: wrap;
  }

  .omx-cp__presets-label { color: var(--omx-color-text-muted); margin-inline-end: var(--omx-space-2); }

  .omx-cp__preset {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-2);
    min-block-size: var(--omx-size-touch);
    padding-inline: var(--omx-space-3);
    font: inherit;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border-control);
    border-radius: var(--omx-radius-md);
    cursor: pointer;
  }

  .omx-cp__preset:hover { background: var(--omx-color-control-hover); }
  .omx-cp__preset:active { background: var(--omx-color-control-active); }

  .omx-cp__preset-dot {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-cp-preset, transparent);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-cp__hint { color: var(--omx-color-text-muted); }

  /* ---- disabled ----------------------------------------------------------- */
  .omx-cp--disabled { opacity: 0.55; pointer-events: none; }

  /* ⚠️ The field is information, and in forced-colors it is information that
   * cannot survive. Say so rather than showing a grey rectangle. */
  @media (forced-colors: active) {
    .omx-cp__field, .omx-cp__hue { display: none; }
  }
}

/* --- DataEntry/Field/Field.css ------------------------------------- */
/* =============================================================================
 * L5 · DataEntry · Field — label, control, hint, error.
 * ========================================================================== */

@layer omx.component {

  .omx-field {
    display: grid;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-field__label {
    font-size: var(--omx-font-size-label);
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
    /* A label is clickable, so it must say so on hover — otherwise the larger
     * hit target it provides is a secret. */
    cursor: pointer;
    width: fit-content;
  }

  .omx-field__req {
    color: var(--omx-color-danger-text);
    margin-inline-start: 2px;
  }

  /* The hint sits ABOVE the control, not below it. Guidance the person needs
   * before typing is guidance they have to read before typing — under the field
   * it is read after the mistake. */
  .omx-field__hint {
    margin-block-start: calc(var(--omx-space-1) * -1);
    color: var(--omx-color-text-muted);
    max-inline-size: var(--omx-measure);
  }

  .omx-field__control { min-inline-size: 0; }

  /* The error sits BELOW, next to the control it belongs to, where the eye lands
   * after trying and failing. */
  .omx-field__error {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-2);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-danger-text);
    max-inline-size: var(--omx-measure);
  }

  /* A shape as well as a colour: WCAG 1.4.1 — colour must not be the only
   * signal, and the control's own red border is also just colour. */
  .omx-field__error::before {
    content: "";
    flex: 0 0 auto;
    inline-size: 12px;
    block-size: 12px;
    margin-block-start: 2px;
    background-color: currentColor;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Ccircle cx='6' cy='6' r='5' fill='none' stroke='black' stroke-width='1.2'/%3E%3Cpath d='M6 3.4v3.2M6 8.3h.01' stroke='black' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / contain;
  }

  /* ===========================================================================
   * HORIZONTAL — label beside the control.
   * ⚠️ Only for a dense settings list. It ties the name to the control by
   * proximity alone, which breaks the moment the row wraps — so it un-wraps
   * itself below the tablet breakpoint rather than becoming ambiguous.
   * ======================================================================== */
  .omx-field--horizontal {
    grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
    align-items: start;
    gap: var(--omx-space-4) var(--omx-space-6);
  }
  .omx-field--horizontal > .omx-field__label   { grid-column: 1; padding-block-start: var(--omx-space-3); }
  .omx-field--horizontal > .omx-field__hint    { grid-column: 1; margin: 0; }
  .omx-field--horizontal > .omx-field__control { grid-column: 2; grid-row: 1 / span 2; }
  .omx-field--horizontal > .omx-field__error   { grid-column: 2; }

  @media (max-width: 640px) {
    .omx-field--horizontal { grid-template-columns: minmax(0, 1fr); }
    .omx-field--horizontal > * { grid-column: 1 !important; grid-row: auto !important; }
  }

  @media (forced-colors: active) {
    /* The mask icon is a background behind a mask, and HCM drops both — so the
     * error would lose its non-colour signal exactly where colour is least
     * reliable. A border puts a shape back. */
    .omx-field__error::before { display: none; }
    .omx-field__error { border-inline-start: 2px solid CanvasText; padding-inline-start: var(--omx-space-3); }
  }

  @media print {
    /* A printed form shows its values and its problems; the hint is guidance
     * for filling it in, which has already happened. */
    .omx-field__hint { display: none; }
  }
}

/* --- DataEntry/Fieldset/Fieldset.css ------------------------------- */
/* =============================================================================
 * L5 · DataEntry · Fieldset — a named group of related controls.
 * ========================================================================== */

@layer omx.component {

  .omx-fieldset {
    display: grid;
    gap: var(--omx-space-4);
    /* A <fieldset> carries browser default padding, margin and a border that
     * nothing in this design uses. */
    margin: 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
  }

  .omx-fieldset__legend {
    /* A <legend> is not laid out like other elements; float:none + padding:0
     * puts it back into normal flow so the grid can place it. */
    float: none;
    padding: 0;
    color: var(--omx-color-text);
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-fieldset__hint {
    color: var(--omx-color-text-muted);
    max-inline-size: var(--omx-measure);
  }

  .omx-fieldset__body {
    display: grid;
    gap: var(--omx-field-gap);
    min-inline-size: 0;
  }

  .omx-fieldset__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--omx-space-4);
    cursor: pointer;
    list-style: none;
  }
  .omx-fieldset__summary::-webkit-details-marker { display: none; }

  .omx-fieldset__chevron {
    inline-size: 12px;
    block-size: 12px;
    color: var(--omx-color-text-subtle);
    transition: rotate var(--omx-duration-fast) var(--omx-easing-standard);
  }
  .omx-fieldset--collapsible[open] > .omx-fieldset__summary .omx-fieldset__chevron { rotate: 90deg; }

  @media print {
    /* A collapsed group prints empty and nobody notices until it is on paper. */
    .omx-fieldset--collapsible > .omx-fieldset__body { display: grid !important; }
    .omx-fieldset__chevron { display: none; }
  }
}

/* --- DataEntry/FilterBar/FilterBar.css ----------------------------- */
/* =============================================================================
 * L5 · DataEntry · FilterBar — the controls, and what is currently on.
 * ========================================================================== */

@layer omx.component {

  .omx-filter-bar {
    display: grid;
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  .omx-filter-bar__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  /* Each control keeps a sensible minimum so a filter row degrades into stacked
   * rows rather than into a line of unusably narrow inputs. */
  .omx-filter-bar__controls > * { flex: 1 1 12rem; min-inline-size: 0; }
  .omx-filter-bar__controls > button,
  .omx-filter-bar__controls > .omx-button { flex: 0 0 auto; }

  .omx-filter-bar__applied {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-filter-bar__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-filter-bar__clear { flex: 0 0 auto; }

  /* The count sits at the end of the row, away from the chips, because it is a
   * RESULT and they are INPUTS. */
  .omx-filter-bar__count {
    margin-inline-start: auto;
    flex: 0 0 auto;
    color: var(--omx-color-text-muted);
    font-variant-numeric: tabular-nums;
  }

  @container omx-slot (max-width: 560px) {
    .omx-filter-bar__count { margin-inline-start: 0; }
  }

  @media print {
    /* A printed list must say what was filtered, or the numbers cannot be
     * reconciled against anything. The chips stay; the controls go. */
    .omx-filter-bar__controls,
    .omx-filter-bar__clear { display: none; }
    .omx-chip__remove { display: none; }
  }
}

/* --- DataEntry/Form/Form.css --------------------------------------- */
/* =============================================================================
 * L5 · DataEntry · Form
 * ========================================================================== */

@layer omx.component {

  .omx-form {
    display: grid;
    gap: var(--omx-field-gap);
    /* ONE column, with a measure. A form field wider than about 40 characters
     * is harder to scan, not easier — and a two-column form makes the tab order
     * ambiguous the moment it wraps. */
    max-inline-size: 34rem;
    min-inline-size: 0;
  }

  .omx-form--wide { max-inline-size: none; }

  /* The action row. `space-between` puts the destructive action away from the
   * confirming one — a Cancel adjacent to a Post is a mis-click waiting. */
  .omx-form__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-5);
    padding-block-start: var(--omx-space-5);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }
  .omx-form__actions--end { justify-content: flex-end; }

  /* A submit that is in flight keeps its width, so the row does not reflow the
   * moment it is pressed. */
  .omx-form button[aria-busy="true"] { cursor: progress; }

  @media print {
    .omx-form { max-inline-size: none; }
    .omx-form__actions { display: none; }
  }
}

/* --- DataEntry/Import/Import.css ----------------------------------- */
/* =============================================================================
 * L5 · DataEntry · Import — the dropzone is a <label> around a real input.
 * ========================================================================== */

@layer omx.component {

  .omx-import {
    display: grid;
    gap: var(--omx-space-4);
    justify-items: start;
    min-inline-size: 0;
  }

  .omx-import__zone {
    display: grid;
    justify-items: center;
    gap: var(--omx-space-3);
    inline-size: 100%;
    padding: var(--omx-space-10) var(--omx-space-8);
    text-align: center;

    background: var(--omx-color-surface-sunken);
    border: var(--omx-border-width-strong) dashed var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    cursor: pointer;

    transition:
      border-color var(--omx-duration-fast) var(--omx-easing-standard),
      background-color var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-import__zone:hover { border-color: var(--omx-color-border-strong); }
  .omx-import__zone:active { border-color: var(--omx-color-border-control); background: var(--omx-color-control-active); }

  .omx-import__zone[data-omx-dragging] {
    border-color: var(--omx-color-accent);
    background: var(--omx-color-accent-subtle);
  }

  /* 🔴 The input is visually hidden (it is the real control, and it is ugly),
   * so its focus ring lands on something 1px wide. The zone shows it instead —
   * the same forwarding Switch does, and for the same reason. Without this, a
   * keyboard user tabbing to the file input sees nothing at all. */
  .omx-import__zone[data-omx-focused] {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
    border-color: var(--omx-color-accent);
  }

  .omx-import__icon {
    inline-size: 28px;
    block-size: 28px;
    color: var(--omx-color-text-subtle);
  }

  .omx-import__label {
    font-size: var(--omx-font-size-body);
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
  }

  .omx-import__hint { color: var(--omx-color-text-muted); }

  .omx-import__files {
    display: grid;
    gap: var(--omx-space-2);
    inline-size: 100%;
    min-inline-size: 0;
  }

  .omx-import__file {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-4);
    padding: var(--omx-space-3) var(--omx-space-5);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border-subtle);
    border-radius: var(--omx-radius-md);
    min-inline-size: 0;
  }

  .omx-import__file-name {
    flex: 1;
    min-inline-size: 0;
    /* A filename is often long and its END is the informative part — the
     * extension and the date suffix. Break rather than truncate the tail. */
    overflow-wrap: anywhere;
  }

  .omx-import__file-size { flex: 0 0 auto; color: var(--omx-color-text-muted); font-variant-numeric: tabular-nums; }

  .omx-import__file--invalid {
    border-color: var(--omx-color-danger-solid);
    background: var(--omx-color-danger-bg);
  }
  .omx-import__file-error { flex: 0 0 auto; color: var(--omx-color-danger-text); }

  @media (forced-colors: active) {
    .omx-import__zone { border-color: CanvasText; }
    .omx-import__zone[data-omx-dragging] { border-color: Highlight; }
  }

  @media print { .omx-import__zone { display: none; } }
}

/* --- DataEntry/ImportReview/ImportReview.css ----------------------- */
/* ==========================================================================
 * L5 · DataEntry · ImportReview — judged, and not yet written.
 *
 * 🔴 THE "NOTHING HAS BEEN IMPORTED YET" SENTENCE IS THE LOUDEST THING HERE,
 * and deliberately louder than the counts. The counts are what make a person
 * believe the work is done; the sentence is the only thing that says it is not.
 *
 * ⛔ IT IS NOT AN ALERT. Nothing has gone wrong — the import is working exactly
 * as designed — so it carries no danger colour. Red here would train people to
 * dismiss the one line that stops them closing the tab too early.
 * ========================================================================== */

@layer omx.component {

  .omx-import-review {
    display: grid;
    gap: var(--omx-space-4);
    padding: var(--omx-space-5);
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    border-radius: var(--omx-radius-md);
    background: var(--omx-color-surface-raised);
  }

  .omx-import-review__title {
    margin: 0;
    overflow-wrap: anywhere;
  }

  .omx-import-review__counts {
    margin: 0;
    font-variant-numeric: tabular-nums;
    color: var(--omx-color-text);
  }

  /* 🔴 The sentence. Full ink and medium weight — heavier than the counts above
     it, because the counts are the thing that misleads. */
  .omx-import-review__nothing-yet {
    margin: 0;
    padding-inline-start: var(--omx-space-4);
    border-inline-start: 2px solid var(--omx-brand-instrument);
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
  }

  .omx-import-review__rejects {
    display: grid;
    gap: var(--omx-space-2);
  }

  .omx-import-review__rejects-title { margin: 0; }

  .omx-import-review__reject-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--omx-space-2);
  }

  .omx-import-review__reject {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-3);
    align-items: baseline;
    padding-block: var(--omx-space-1);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border);
  }

  .omx-import-review__reject:last-child { border-block-end: 0; }

  /* ⛔ The line number is the actionable part, so it is not the quiet part. */
  .omx-import-review__line {
    font-variant-numeric: tabular-nums;
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
  }

  .omx-import-review__reason { color: var(--omx-color-text-muted); }

  .omx-import-review__excerpt {
    color: var(--omx-color-text-subtle);
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
  }

  .omx-import-review__more,
  .omx-import-review__download,
  .omx-import-review__why {
    margin: 0;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
  }

  .omx-import-review__refusal {
    margin: 0;
    display: grid;
    gap: var(--omx-space-1);
  }

  @media (forced-colors: active) {
    .omx-import-review { border-color: CanvasText; }
    .omx-import-review__nothing-yet { border-inline-start-color: CanvasText; }
  }

  @media print {
    /* "Nothing has been imported yet" is exactly the line that must survive a
       printout handed to someone else. */
    .omx-import-review__nothing-yet { color: #000; }
  }

}

/* --- DataEntry/ScanField/ScanField.css ----------------------------- */
/* ==========================================================================
 * L5 · DataEntry · ScanField.
 *
 * ⭐ SIZED FOR A FACTORY FLOOR. The PROD terminal is used standing, often in
 * gloves, on a wall-mounted tablet — so the target is well above the 24px WCAG
 * 2.5.8 minimum rather than at it. This is the one input in the package that
 * assumes its user is not sitting down.
 * ========================================================================== */

@layer omx.component {

  .omx-scan {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
  }

  .omx-scan__box {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    position: relative;
  }

  .omx-scan__icon {
    position: absolute;
    inset-inline-start: var(--omx-space-3);
    inline-size: 1.25rem;
    block-size: 1.25rem;
    color: var(--omx-color-text-subtle);
    pointer-events: none;
  }

  .omx-scan__input {
    inline-size: 100%;
    /* Room for the glyph, on whichever side the writing direction puts it. */
    padding-inline-start: calc(var(--omx-space-3) * 2 + 1.25rem);
    min-block-size: var(--omx-space-10);
    font-size: var(--omx-font-size-lead);
    /* A code is read character by character when something is wrong, and a
     * proportional face makes 0/O and 1/l/I a guess. */
    font-family: var(--omx-font-mono);
    letter-spacing: var(--omx-letter-spacing-wide);
  }

  .omx-scan__hint { color: var(--omx-color-text-muted); margin: 0; }

  .omx-scan__error {
    margin: 0;
    color: var(--omx-color-danger-text);
    font-size: var(--omx-font-size-caption);
  }

  .omx-scan--invalid .omx-scan__input { border-color: var(--omx-color-danger-solid); }

  /* -- the scan acknowledgement --------------------------------------------
   * ⭐ A brief, visible confirmation that the machine accepted the read. On a
   * noisy floor the scanner's beep is often inaudible, and without this the
   * operator cannot tell a successful scan from a dead trigger — so they scan
   * again, and the duplicate lands in the sync queue. */
  .omx-scan--scanned .omx-scan__input {
    border-color: var(--omx-color-success-text);
    background: var(--omx-color-success-bg);
    transition: background var(--omx-duration-fast) var(--omx-easing-standard);
  }

  @media (prefers-reduced-motion: reduce) {
    .omx-scan--scanned .omx-scan__input { transition: none; }
  }
}

/* --- DataEntry/TransitionMatrix/TransitionMatrix.css --------------- */
@layer omx.component {

  /* L5 · DataEntry · TransitionMatrix — from each state to each other one.
   *
   * ⭐ It IS a table and looks like one: it reuses the table's own classes, as
   * Reconciliation and EditGrid do. What is added is the corner, the row
   * headers, the narrow inputs — and the mark that tells blank from zero. */

  .omx-tmatrix {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-tmatrix__coverage {
    margin: 0;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-tmatrix__caption {
    caption-side: top;
    padding-block-end: var(--omx-space-2);
    text-align: start;
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-tmatrix__corner {
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-tmatrix__from {
    text-align: start;
    font-weight: var(--omx-font-weight-medium);
    white-space: nowrap;
  }

  .omx-tmatrix__input {
    inline-size: 6ch;
    min-inline-size: 6ch;
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  /* 🔴🔴 BLANK IS NOT ZERO — marked by a SHAPE, not a colour, and live: an
   * empty input shows the hatch, a typed 0 does not. */
  .omx-tmatrix__input:placeholder-shown {
    background-image: repeating-linear-gradient(135deg, transparent 0 4px,
                      var(--omx-color-border-subtle) 4px 5px);
  }
}

/* --- DataEntry/Wizard/Wizard.css ----------------------------------- */
/* =============================================================================
 * L5 · DataEntry · Wizard — the step list.
 * ========================================================================== */

@layer omx.component {

  .omx-wizard { min-inline-size: 0; }

  .omx-wizard__steps {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-wizard__step {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-4);
    flex: 1 1 0;
    min-inline-size: 0;
    position: relative;
  }

  /* The connector between markers. A pseudo-element on the step rather than a
   * separate element, so the step count and the connector count cannot drift. */
  .omx-wizard__step:not(:last-child)::after {
    content: "";
    position: absolute;
    inset-block-start: 11px;
    inset-inline-start: calc(24px + var(--omx-space-4));
    inline-size: calc(100% - 24px - var(--omx-space-4));
    block-size: 2px;
    background: var(--omx-color-border);
  }
  .omx-wizard__step[data-omx-step-state="done"]::after { background: var(--omx-color-accent); }

  .omx-wizard__marker {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    inline-size: 24px;
    block-size: 24px;
    border-radius: var(--omx-radius-pill);

    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-semibold);
    font-variant-numeric: tabular-nums;

    color: var(--omx-color-text-muted);
    background: var(--omx-color-surface);
    border: var(--omx-border-width-strong) solid var(--omx-color-border);
  }
  .omx-wizard__marker svg { inline-size: 12px; block-size: 12px; }

  .omx-wizard__step[data-omx-step-state="current"] .omx-wizard__marker {
    color: var(--omx-color-accent-text);
    background: var(--omx-color-accent);
    border-color: var(--omx-color-accent);
  }
  .omx-wizard__step[data-omx-step-state="done"] .omx-wizard__marker {
    color: var(--omx-color-accent);
    border-color: var(--omx-color-accent);
  }
  .omx-wizard__step[data-omx-step-state="invalid"] .omx-wizard__marker {
    color: var(--omx-color-danger-on);
    background: var(--omx-color-danger-solid);
    border-color: var(--omx-color-danger-solid);
  }

  .omx-wizard__label {
    display: grid;
    gap: var(--omx-space-1);
    min-inline-size: 0;
    padding-block-start: 2px;
    color: var(--omx-color-text-muted);
    text-decoration: none;
  }
  a.omx-wizard__label:hover { color: var(--omx-color-text); }
  a.omx-wizard__label:active { color: var(--omx-color-accent-active); }

  .omx-wizard__name {
    font-size: var(--omx-font-size-body);
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The state hooks off aria-current, never a class — the same rule as the
   * sidebar: an unstyled current step and an unannounced one are one bug. */
  .omx-wizard__label[aria-current="step"] {
    color: var(--omx-color-text);
    font-weight: var(--omx-font-weight-semibold);
  }

  /* ===========================================================================
   * VERTICAL — better past about five steps, where a horizontal row either
   * wraps or shrinks every label to nothing.
   * ======================================================================== */
  .omx-wizard--vertical .omx-wizard__steps { flex-direction: column; gap: var(--omx-space-6); }
  .omx-wizard--vertical .omx-wizard__step  { flex: 0 0 auto; inline-size: 100%; }
  .omx-wizard--vertical .omx-wizard__step:not(:last-child)::after {
    inset-block-start: calc(24px + var(--omx-space-2));
    inset-inline-start: 11px;
    inline-size: 2px;
    block-size: calc(100% - 24px + var(--omx-space-4));
  }

  /* On a narrow slot the horizontal form is unreadable, so it becomes the
   * vertical one rather than shrinking further. */
  @container omx-slot (max-width: 640px) {
    .omx-wizard .omx-wizard__steps { flex-direction: column; gap: var(--omx-space-6); }
    .omx-wizard .omx-wizard__step  { flex: 0 0 auto; inline-size: 100%; }
    .omx-wizard .omx-wizard__step:not(:last-child)::after {
      inset-block-start: calc(24px + var(--omx-space-2));
      inset-inline-start: 11px;
      inline-size: 2px;
      block-size: calc(100% - 24px + var(--omx-space-4));
    }
  }

  @media (forced-colors: active) {
    .omx-wizard__marker { border-color: CanvasText; }
    .omx-wizard__step[data-omx-step-state="current"] .omx-wizard__marker {
      background: Highlight; color: HighlightText;
    }
  }

  @media print {
    /* On paper the step list is history, not navigation — but it says which
     * step the printed page came from, so it stays. */
    .omx-wizard__step:not(:last-child)::after { background: #000; }
  }
}

/* --- Feedback/Banner/Banner.css ------------------------------------ */
/* =============================================================================
 * L6 · Feedback · Banner — a condition that is still true.
 * ========================================================================== */

@layer omx.component {

  .omx-banner {
    --omx-banner-bg: var(--omx-color-info-bg);
    --omx-banner-fg: var(--omx-color-info-text);

    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-5);
    padding: var(--omx-space-5) var(--omx-space-6);

    color: var(--omx-color-text);
    background: var(--omx-banner-bg);
    border: var(--omx-border-width) solid transparent;
    /* A 3px inline bar in the tone colour. The wash alone is too quiet at the
     * top of a dense page, and a full-strength fill is too loud to leave up. */
    border-inline-start: 3px solid var(--omx-banner-fg);
    border-radius: var(--omx-radius-md);
    min-inline-size: 0;
  }

  .omx-banner__icon {
    inline-size: var(--omx-size-icon-action);
    block-size: var(--omx-size-icon-action);
    flex: 0 0 auto;
    margin-block-start: 1px;
    color: var(--omx-banner-fg);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
  }

  .omx-banner__body { flex: 1; display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-banner__title { font-weight: var(--omx-font-weight-medium); }
  .omx-banner__desc { color: var(--omx-color-text-muted); }

  .omx-banner__action,
  .omx-banner__close { flex: 0 0 auto; }

  /* Each tone sets the PAIR. Never one half — that is how the disabled button
   * ended up white-on-grey. */
  .omx-banner--info    { --omx-banner-bg: var(--omx-color-info-bg);    --omx-banner-fg: var(--omx-color-info-text); }
  .omx-banner--success { --omx-banner-bg: var(--omx-color-success-bg); --omx-banner-fg: var(--omx-color-success-text); }
  .omx-banner--warning { --omx-banner-bg: var(--omx-color-warning-bg); --omx-banner-fg: var(--omx-color-warning-text); }
  .omx-banner--danger  { --omx-banner-bg: var(--omx-color-danger-bg);  --omx-banner-fg: var(--omx-color-danger-text); }
  .omx-banner--neutral { --omx-banner-bg: var(--omx-color-neutral-bg); --omx-banner-fg: var(--omx-color-neutral-text); }
  .omx-banner--accent  { --omx-banner-bg: var(--omx-color-accent-subtle); --omx-banner-fg: var(--omx-color-accent-hover); }

  @media (forced-colors: active) {
    /* HCM strips the wash, so the tone disappears and only the WORDS remain —
     * which is the reason a banner never signals by colour alone. */
    .omx-banner { border-color: CanvasText; }
  }

  @media print {
    .omx-banner { break-inside: avoid; border: 1px solid #000; }
    .omx-banner__close, .omx-banner__action { display: none; }
  }
}

/* --- Feedback/Empty/Empty.css -------------------------------------- */
/* =============================================================================
 * L6 · Feedback · Empty
 * ========================================================================== */

@layer omx.component {

  .omx-empty {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: var(--omx-space-4);
    padding: var(--omx-space-11) var(--omx-space-8);
    text-align: center;
    /* Wide enough to read, narrow enough that the eye does not travel. */
    max-inline-size: 44ch;
    margin-inline: auto;
    color: var(--omx-color-text-muted);
  }

  .omx-empty--compact { padding: var(--omx-space-8) var(--omx-space-6); gap: var(--omx-space-3); }

    /* ⭐ BRAND, NOT GREY (owner ruling 2026-09-19). This glyph is `aria-hidden`
     * — the words carry the whole message — so it is decoration, which is
     * exactly what makes it safe to brand. It read at `text-subtle`, the same
     * grey as a DISABLED control: the one screen a person meets when there is
     * nothing to look at was painted in the colour of something switched off.
     * MEASURED 4.23 light / 6.02 dark against the surface, over 1.4.11's 3.0.
     * ⛔ The status variants below override this and MUST keep overriding it —
     * denied and failed are verdicts, and a verdict is never decorated. */
  .omx-empty__icon {
    inline-size: var(--omx-size-icon-feature);
    block-size: var(--omx-size-icon-feature);
    color: var(--omx-color-brand-mark);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.2;
  }

  .omx-empty__title { color: var(--omx-color-text); }
  .omx-empty__action { margin-block-start: var(--omx-space-3); }

  /* Denied and failed are NOT decorated like errors — a red panel for
   * "you do not have access" reads as a malfunction and sends people to
   * support. The words carry the difference; only the icon is tinted. */
  .omx-empty--denied .omx-empty__icon { color: var(--omx-color-warning-text); }
  .omx-empty--failed .omx-empty__icon { color: var(--omx-color-danger-text); }

  @media print { .omx-empty__action { display: none; } }
}

/* --- Feedback/Progress/Progress.css -------------------------------- */
/* =============================================================================
 * L6 · Feedback · Progress — determinate only.
 * ========================================================================== */

@layer omx.component {

  .omx-progress {
    /* ⭐ THE SWEEP (owner ruling 2026-09-19). This component is determinate
     * ONLY — the bar always means "this much of a known total", never a status
     * — so the fill is the one progress surface with no meaning to lose.
     * ⛔ The three status variants below are untouched and MUST stay untouched:
     * success/warning/danger carry a verdict, and a verdict is never decorated. */
    --omx-progress-fill: var(--omx-brand-sweep);

    display: grid;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-progress__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--omx-space-5);
    min-inline-size: 0;
  }

  .omx-progress__label {
    font-size: var(--omx-font-size-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-progress__value {
    flex: 0 0 auto;
    /* Tabular, or the figure jumps sideways on every tick and the eye chases it
     * instead of reading the bar. */
    font-variant-numeric: tabular-nums;
    color: var(--omx-color-text-muted);
  }

  .omx-progress__track {
    block-size: 6px;
    background: var(--omx-color-surface-sunken);
    border-radius: var(--omx-radius-pill);
    overflow: hidden;
  }

  .omx-progress__fill {
    block-size: 100%;
    background: var(--omx-progress-fill);
    border-radius: inherit;
    /* Animate the WIDTH only as a courtesy; under reduced motion the duration
     * token collapses to 0 and the bar simply jumps, which is honest. */
    transition: inline-size var(--omx-duration-normal) var(--omx-easing-out);
  }

  .omx-progress--success { --omx-progress-fill: var(--omx-color-success-text); }
  .omx-progress--warning { --omx-progress-fill: var(--omx-color-warning-text); }
  .omx-progress--danger  { --omx-progress-fill: var(--omx-color-danger-solid); }

  @media (forced-colors: active) {
    /* HCM removes the fill colour, so the bar becomes one flat block. A border
     * on the track keeps the two apart. */
    .omx-progress__track { border: 1px solid CanvasText; }
    .omx-progress__fill  { background: Highlight; }
  }

  @media print {
    /* On paper the bar is meaningless without its figure, and the figure is
     * already in the head — so only the chrome goes. */
    .omx-progress__track { display: none; }
  }
}

/* --- Feedback/Skeleton/Skeleton.css -------------------------------- */
/* =============================================================================
 * L6 · Feedback · Skeleton — a placeholder the shape of the thing.
 *
 * ⭐ Why this and not a spinner: a skeleton HOLDS THE LAYOUT. Content arrives
 * into space already reserved for it, so nothing jumps — and it says what is
 * coming, which a ring cannot. A spinner is for a short wait whose result has
 * an unknown shape; everything else is this.
 * ========================================================================== */

@layer omx.component {

  .omx-skeleton {
    display: block;
    border-radius: var(--omx-radius-sm);
    background-color: var(--omx-color-surface-sunken);
    /* A moving highlight, not a pulse: a pulsing block reads as a warning
     * indicator, which is the wrong message for "loading". */
    background-image: linear-gradient(
      90deg,
      transparent 0%,
      color-mix(in srgb, var(--omx-color-surface-raised) 70%, transparent) 50%,
      transparent 100%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    animation: omx-skeleton-sweep 1400ms ease-in-out infinite;
  }

  @keyframes omx-skeleton-sweep {
    from { background-position: -100% 0; }
    to   { background-position: 200% 0; }
  }

  /* Under reduced motion, a11y.css clamps the animation to a single 1ms
   * iteration, leaving a flat block — which still holds the layout and still
   * reads as "not yet". That is the correct degradation, not a loss. */

  .omx-skeleton--text {
    block-size: 1em;
    margin-block: 0.25em;
    border-radius: var(--omx-radius-xs);
  }

  /* A paragraph's last line is short. A stack of identical full-width bars does
   * not read as text, and the illusion is the entire point. */
  .omx-skeleton--text:last-child { inline-size: 62%; }

  .omx-skeleton--title  { block-size: 1.4em; inline-size: 42%; }
  .omx-skeleton--circle { border-radius: var(--omx-radius-pill); }
  .omx-skeleton--row    { block-size: var(--omx-row-height); border-radius: 0; }

  /* The GROUP announces once, not once per bar — twenty bars inside a live
   * region is twenty announcements of the same fact. */
  .omx-skeleton-group { display: grid; gap: var(--omx-space-4); }

  @media print {
    .omx-skeleton,
    .omx-skeleton-group { display: none; }
  }
}

/* --- Feedback/StatePage/StatePage.css ------------------------------ */
/* =============================================================================
 * L6 · Feedback · StatePage — the whole page is the message.
 * ========================================================================== */

@layer omx.component {

  .omx-statepage {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: var(--omx-space-6);
    padding: var(--omx-space-12) var(--omx-space-8);
    text-align: center;
    /* Narrow enough that the eye does not travel across a wide monitor to read
     * one sentence. */
    max-inline-size: 52ch;
    margin-inline: auto;
    min-block-size: 100%;
  }

    /* ⭐ BRAND, NOT GREY (owner ruling 2026-09-19). This glyph is `aria-hidden`
     * — the words carry the whole message — so it is decoration, which is
     * exactly what makes it safe to brand. It read at `text-subtle`, the same
     * grey as a DISABLED control: the one screen a person meets when there is
     * nothing to look at was painted in the colour of something switched off.
     * MEASURED 4.23 light / 6.02 dark against the surface, over 1.4.11's 3.0.
     * ⛔ The status variants below override this and MUST keep overriding it —
     * denied and failed are verdicts, and a verdict is never decorated. */
  .omx-statepage__icon {
    inline-size: 40px;
    block-size: 40px;
    color: var(--omx-color-brand-mark);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.1;
  }

  .omx-statepage__desc { color: var(--omx-color-text-muted); }

  .omx-statepage--failed .omx-statepage__icon { color: var(--omx-color-danger-text); }
  .omx-statepage--warn   .omx-statepage__icon { color: var(--omx-color-warning-text); }

  .omx-statepage__ref {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-4);
    padding: var(--omx-space-3) var(--omx-space-3) var(--omx-space-3) var(--omx-space-5);
    background: var(--omx-color-surface-sunken);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
  }

  .omx-statepage__ref-value {
    font-family: var(--omx-font-mono);
    font-size: var(--omx-font-size-body);
    /* Select the whole reference on one click — it is read aloud to support,
     * and a half-selected GUID is the wrong log line. */
    user-select: all;
    overflow-wrap: anywhere;
  }

  .omx-statepage__actions { margin-block-start: var(--omx-space-3); }

  @media print {
    /* A printed error page is what someone attaches to a ticket, so the
     * reference must survive and the buttons must not. */
    .omx-statepage__actions,
    .omx-statepage__ref button { display: none; }
  }
}

/* --- Feedback/Toast/Toast.css -------------------------------------- */
/* =============================================================================
 * L6 · Feedback · Toast — something happened, and nothing is lost when it goes.
 *
 * ⛔ Never for a standing condition; that is a Banner. The test: if missing the
 * message would cost the user something, it must not be a toast.
 * ========================================================================== */

@layer omx.component {

  .omx-toast-region {
    position: fixed;
    inset-block-end: var(--omx-space-8);
    inset-inline-end: var(--omx-space-8);
    z-index: var(--omx-z-toast);

    display: grid;
    gap: var(--omx-space-4);
    justify-items: end;
    max-inline-size: min(380px, calc(100vw - var(--omx-space-11)));

    /* The REGION must not swallow clicks on the page behind it; each toast
     * takes pointer events back for itself. */
    pointer-events: none;
  }

  /* On a phone the bottom-right corner is both under the thumb and over the
   * content. Full width at the bottom is the only place that is neither. */
  @media (max-width: 640px) {
    .omx-toast-region {
      inset-inline: var(--omx-space-5);
      inset-block-end: calc(var(--omx-space-5) + env(safe-area-inset-bottom));
      max-inline-size: none;
      justify-items: stretch;
    }
  }

  .omx-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-4);
    padding: var(--omx-space-5) var(--omx-space-6);

    color: var(--omx-color-text);
    background: var(--omx-color-surface-raised);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-inline-start: 3px solid var(--omx-color-accent);
    border-radius: var(--omx-radius-md);
    /* A toast is announced over whatever is on screen, modals included. */
    box-shadow: var(--omx-shadow-xl);

    animation: omx-toast-in var(--omx-duration-normal) var(--omx-easing-standard);
  }

  @keyframes omx-toast-in {
    from { opacity: 0; translate: 0 8px; }
    to   { opacity: 1; translate: 0 0; }
  }

  .omx-toast--success { border-inline-start-color: var(--omx-color-success-text); }
  .omx-toast--warning { border-inline-start-color: var(--omx-color-warning-text); }
  .omx-toast--danger  { border-inline-start-color: var(--omx-color-danger-text); }

  .omx-toast__body  { flex: 1; min-inline-size: 0; display: grid; gap: var(--omx-space-1); }
  .omx-toast__title { font-weight: var(--omx-font-weight-medium); }
  .omx-toast__desc  { color: var(--omx-color-text-muted); }
  .omx-toast__close { flex: 0 0 auto; }

  @media print { .omx-toast-region { display: none; } }
}

/* --- Feedback/Validation/Validation.css ---------------------------- */
/* =============================================================================
 * L6 · Feedback · Validation
 * ========================================================================== */

@layer omx.component {

  .omx-validation {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-5);
    padding: var(--omx-space-6);

    color: var(--omx-color-text);
    background: var(--omx-color-danger-bg);
    border: var(--omx-border-width) solid transparent;
    border-inline-start: 3px solid var(--omx-color-danger-solid);
    border-radius: var(--omx-radius-md);
  }

  /* It receives focus programmatically, so the ring would fire on every failed
   * submit for mouse users too. :focus-visible keeps it for the keyboard. */
  .omx-validation:focus { outline: none; }
  .omx-validation:focus-visible {
    outline: var(--omx-border-width-strong) solid var(--omx-color-danger-solid);
    outline-offset: 2px;
  }

  .omx-validation__icon {
    inline-size: var(--omx-size-icon-action);
    block-size: var(--omx-size-icon-action);
    flex: 0 0 auto;
    margin-block-start: 1px;
    color: var(--omx-color-danger-text);
  }

  .omx-validation__body  { flex: 1; min-inline-size: 0; display: grid; gap: var(--omx-space-4); }
  .omx-validation__title { font-weight: var(--omx-font-weight-semibold); }

  .omx-validation__list { display: grid; gap: var(--omx-space-2); }

  .omx-validation__item {
    /* A real list marker: a screen reader announces "list, 6 items" before the
     * first one, which is the count the person most needs. */
    list-style: disc;
    margin-inline-start: var(--omx-space-6);
    font-size: var(--omx-font-size-body);
  }

  .omx-validation__link { color: var(--omx-color-danger-text); text-decoration: underline; }
  .omx-validation__link:hover { color: var(--omx-color-text); }
  .omx-validation__link:active { color: var(--omx-color-accent-active); }

  .omx-validation__field { font-weight: var(--omx-font-weight-medium); }

  @media (forced-colors: active) { .omx-validation { border-color: CanvasText; } }

  @media print {
    /* A printed form that failed validation should still show what failed. */
    .omx-validation { border: 1px solid #000; break-inside: avoid; }
  }
}

/* --- Navigation/Appearance/Appearance.css -------------------------- */
/* =============================================================================
 * L7 · Navigation · Appearance — theme, density, text size.
 * ========================================================================== */

@layer omx.component {

  .omx-appearance { display: grid; gap: var(--omx-space-6); min-inline-size: 0; }
  .omx-appearance__caption { margin: 0; }

  .omx-appearance__sample {
    display: grid;
    gap: var(--omx-space-2);
    padding: var(--omx-space-5);
    border: var(--omx-border-width) dashed var(--omx-color-border);
    border-radius: var(--omx-radius-md);
    background: var(--omx-color-surface);
  }
  .omx-appearance__sample-label { color: var(--omx-color-text-subtle); margin: 0; }

  .omx-appearance__sample-row {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    /* row-height, so DENSITY visibly moves the sample — the point of the
     * preview is that it responds to the control above it. */
    min-block-size: var(--omx-row-height);
  }
  .omx-appearance__sample-amount { margin-inline-start: auto; }
  .omx-appearance__sample-caption { color: var(--omx-color-text-muted); margin: 0; }

  .omx-appearance__note { color: var(--omx-color-text-subtle); margin: 0; }

  @media print { .omx-appearance { display: none; } }
}

/* --- Navigation/Breadcrumb/Breadcrumb.css -------------------------- */
/* =============================================================================
 * L7 · Navigation · Breadcrumb
 *
 * 🔴 MOVED OUT OF Topbar.css 2026-09-19, unchanged. It lived there because the
 * trail was markup inside the Topbar, which meant a page drawing its own header
 * could not have a breadcrumb without copying both.
 * ========================================================================== */

@layer omx.component {

  .omx-breadcrumb__list {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }
  .omx-breadcrumb__item {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }
  .omx-breadcrumb__link {
    font-size: var(--omx-font-size-body);
    color: var(--omx-color-text-muted);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .omx-breadcrumb__link:hover { color: var(--omx-color-text); text-decoration: underline; }
  .omx-breadcrumb__link:active { color: var(--omx-color-accent-active); }
  .omx-breadcrumb__sep {
    inline-size: 12px; block-size: 12px; flex: 0 0 auto;
    color: var(--omx-color-text-subtle);
  }
  .omx-breadcrumb__current {
    font-size: var(--omx-font-size-body);
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* On a narrow content area only the last two steps survive. A trail that
   * wraps to two lines has stopped being a trail. Container query, not media:
   * it is the CONTENT area's width that decides, not the window's.
   * ⚠️ This depends on an ancestor declaring `container-name: omx-content`. The
   * shell does; a caller rendering the breadcrumb somewhere else gets the full
   * trail, which is the safe direction to fail in. */
  @container omx-content (max-width: 640px) {
    .omx-breadcrumb__item:not(:nth-last-child(-n+2)) { display: none; }
  }
}

/* --- Navigation/Catalog/Catalog.css -------------------------------- */
/* =============================================================================
 * L7 · Navigation · Catalog — the module panel on the landing page.
 * ========================================================================== */

@layer omx.component {

  .omx-catalog { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }
  .omx-catalog__caption { margin: 0; }

  .omx-catalog__group {
    margin: var(--omx-space-4) 0 0;
    color: var(--omx-color-text-muted);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-catalog__grid {
    display: grid;
    /* Reacts to the SLOT, not the window — the launcher's content area is a
     * container, so this is four across on a desktop and one on a phone with no
     * media query and no caller involvement. */
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--omx-space-5);
    list-style: none;
    padding: 0;
    margin: 0;
    min-inline-size: 0;
  }

  .omx-catalog__item { display: flex; min-inline-size: 0; }

  .omx-catalog__tile {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-4);
    inline-size: 100%;
    padding: var(--omx-space-6);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
    color: var(--omx-color-text);
    text-decoration: none;
    min-block-size: 88px;
  }

  a.omx-catalog__tile:hover {
    border-color: var(--omx-color-accent);
    background: var(--omx-color-accent-subtle);
  }
  a.omx-catalog__tile:active { background: var(--omx-color-control-active); border-color: var(--omx-color-accent-active); }

  .omx-catalog__icon {
    display: grid;
    place-items: center;
    inline-size: 36px;
    block-size: 36px;
    flex: none;
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-surface-sunken);
    color: var(--omx-color-accent-hover);
  }
  .omx-catalog__icon svg { inline-size: var(--omx-size-icon-feature); block-size: var(--omx-size-icon-feature); }

  .omx-catalog__text { display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-catalog__name { font-weight: var(--omx-font-weight-semibold); }
  .omx-catalog__desc { color: var(--omx-color-text-muted); }

  /* 🔴 Dimmed AND explained. The tile is quieter, and the sentence beside it is
   * what actually tells a person what to do — a grey tile alone is a support
   * call, and "unavailable" with no reason is a longer one. */
  .omx-catalog__tile--unavailable {
    background: var(--omx-color-surface-sunken);
    border-style: dashed;
    cursor: default;
  }
  .omx-catalog__tile--unavailable .omx-catalog__name { color: var(--omx-color-text-muted); }
  .omx-catalog__tile--unavailable .omx-catalog__icon { color: var(--omx-color-text-subtle); }

  .omx-catalog__why { color: var(--omx-color-text-muted); }
  /* Licensing is the administrator's problem; no-access is a role request. The
   * tone differs because the ACTION differs. */
  .omx-catalog__tile[data-omx-availability="noaccess"] .omx-catalog__why { color: var(--omx-color-warning-text); }

  @media print {
    /* A printed list of modules is a useful thing — an access review. So it
     * prints, unavailable ones included, with their reasons. */
    .omx-catalog__tile { break-inside: avoid; border-color: #000; background: none; }
  }
}

/* --- Navigation/CommandPalette/CommandPalette.css ------------------ */
/* =============================================================================
 * L7 · Navigation · CommandPalette.
 * ========================================================================== */

@layer omx.component {

  .omx-palette {
    /* A dialog is centred by the UA; a palette belongs near the top, where the
     * eye already is and where the results grow downward into empty space. */
    margin-block-start: 10vh;
    margin-inline: auto;
    inline-size: min(620px, calc(100vw - var(--omx-space-8)));
    max-block-size: min(70vh, 560px);
    padding: 0;
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    background: var(--omx-color-surface);
    color: var(--omx-color-text);
    box-shadow: var(--omx-shadow-lg);
    overflow: hidden;
  }

  .omx-palette::backdrop {
    background: var(--omx-color-surface-overlay);
    /* ⚠️ ::backdrop does NOT inherit from the page, so a var() here resolves
     * against the ROOT, not against .omx-ui — the same trap Modal documents.
     * The literal is the fallback that actually paints. */
    background: rgb(16 25 40 / 44%);
    backdrop-filter: blur(1px);
  }

  /* 🔴 FIXED 2026-09-19 — the system branch was NOT inside the media query,
   * so this dark rule also matched the DEFAULT, UNSTAMPED state and applied
   * on a light page. Three states, not two: explicit dark, explicit light,
   * and system — only `@media (prefers-color-scheme: dark)` separates the
   * third. Five files had this; tokens.css and Brand.css did not. */
  .omx-ui[data-theme="dark"] .omx-palette::backdrop {
    background: rgb(4 8 14 / 62%);
  }

  @media (prefers-color-scheme: dark) {
    .omx-ui:not([data-theme="light"]) .omx-palette::backdrop{
      background: rgb(4 8 14 / 62%);
    }
  }

  .omx-palette__box {
    display: grid;
    grid-template-rows: auto 1fr auto;
    max-block-size: inherit;
    min-block-size: 0;
  }

  .omx-palette__search {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    padding: var(--omx-space-4) var(--omx-space-5);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-palette__search-icon { inline-size: 16px; block-size: 16px; flex: none; color: var(--omx-color-text-muted); }

  .omx-palette__input {
    flex: 1 1 auto;
    min-inline-size: 0;
    appearance: none;
    border: 0;
    background: none;
    padding: var(--omx-space-2) 0;
    font: inherit;
    font-size: var(--omx-font-size-lead);
    color: var(--omx-color-text);
  }
  /* The ring goes on the ROW, because the thing a person perceives as focused
   * is the whole search control, not the bare <input> inside it. That is not a
   * bespoke rule here: the row carries `data-omx-focus-group`, and
   * Foundation/a11y.css already owns both halves of the pattern — the ring on
   * the wrapper AND the suppression of the inner input's own ring. Writing it
   * again locally would have been a second copy to keep in step. */
  .omx-palette__input::placeholder { color: var(--omx-color-text-subtle); }

  .omx-palette__close {
    appearance: none;
    background: none;
    border: 0;
    padding: var(--omx-space-2);
    min-inline-size: var(--omx-control-height);
    min-block-size: var(--omx-control-height);
    display: grid;
    place-items: center;
    border-radius: var(--omx-radius-sm);
    color: var(--omx-color-text-muted);
    cursor: pointer;
  }
  .omx-palette__close svg { inline-size: 14px; block-size: 14px; }
  .omx-palette__close:hover { background: var(--omx-color-control-hover); color: var(--omx-color-text); }
  .omx-palette__close:active { background: var(--omx-color-control-active); }

  .omx-palette__results {
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--omx-space-3);
    min-block-size: 0;
  }

  .omx-palette__group + .omx-palette__group { margin-block-start: var(--omx-space-3); }

  .omx-palette__group-label {
    padding: var(--omx-space-2) var(--omx-space-3);
    color: var(--omx-color-text-muted);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    font-weight: var(--omx-font-weight-semibold);
    margin: 0;
  }

  .omx-palette__option {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    padding: var(--omx-space-3) var(--omx-space-3);
    border-radius: var(--omx-radius-sm);
    color: var(--omx-color-text);
    text-decoration: none;
    min-block-size: var(--omx-control-height);
  }

  /* 🔴 The highlight is aria-selected, NOT :hover and not a class — so the
   * thing the eye sees and the thing aria-activedescendant points at are the
   * same DOM state, and cannot disagree. */
  .omx-palette__option[aria-selected="true"] {
    background: var(--omx-color-accent-subtle);
    box-shadow: inset 2px 0 0 var(--omx-color-accent);
  }
  .omx-palette__option:hover { background: var(--omx-color-control-hover); }
  .omx-palette__option:active { background: var(--omx-color-control-active); }

  .omx-palette__icon { inline-size: 16px; block-size: 16px; flex: none; color: var(--omx-color-text-muted); }

  .omx-palette__text { display: grid; min-inline-size: 0; }
  .omx-palette__label { font-weight: var(--omx-font-weight-medium); }
  .omx-palette__hint {
    color: var(--omx-color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-palette__kbd, .omx-palette__legend kbd {
    font: inherit;
    font-size: var(--omx-font-size-caption);
    font-family: var(--omx-font-mono);
    padding: 1px var(--omx-space-2);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-block-end-width: 2px;
    border-radius: var(--omx-radius-xs);
    background: var(--omx-color-surface-sunken);
    color: var(--omx-color-text-muted);
    margin-inline-start: auto;
    white-space: nowrap;
  }

  .omx-palette__empty {
    padding: var(--omx-space-8) var(--omx-space-5);
    text-align: center;
    color: var(--omx-color-text-muted);
    margin: 0;
  }

  .omx-palette__legend {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    padding: var(--omx-space-3) var(--omx-space-5);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
    color: var(--omx-color-text-subtle);
    margin: 0;
  }
  .omx-palette__legend kbd { margin-inline-start: 0; }

  @media (max-width: 640px) {
    /* On a phone the palette is the screen. A 10vh inset wastes the space the
     * results need once the keyboard is up. */
    .omx-palette {
      margin: 0;
      inline-size: 100vw;
      max-block-size: 100dvh;
      block-size: 100dvh;
      border: 0;
      border-radius: 0;
    }
    .omx-palette__legend { display: none; }
  }

  @media print { .omx-palette { display: none; } }
}

/* --- Navigation/Pager/Pager.css ------------------------------------ */
/* =============================================================================
 * L7 · Navigation · Pager.
 * ========================================================================== */

@layer omx.component {

  .omx-pager {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--omx-space-4) var(--omx-space-5);
    padding-block: var(--omx-space-4);
  }

  .omx-pager__status { color: var(--omx-color-text-muted); margin: 0; font-variant-numeric: tabular-nums; }

  /* 🔴 THE ONE PLACE IN THE PACKAGE THAT PUSHED A PAGE SIDEWAYS.
   * A nowrap flex row of page links cannot shrink below its content, so at
   * 320px the list measured 347px and ran 44px past the viewport -- and a
   * page that scrolls horizontally is the single layout fault a person
   * always notices. Measured on /gallery/print, the only route of 186
   * page-renders (31 routes x 6 widths) that did it.
   * ⇒ Wrap onto a second line instead. A pager is a list, and a list wraps. */
  .omx-pager__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-1);
    margin-inline-start: auto;
    list-style: none;
    padding: 0;
  }

  .omx-pager__item { display: flex; }

  .omx-pager__link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--omx-space-2);
    /* WCAG 2.5.8 — 24px minimum target, and page numbers are the control most
     * often shrunk below it. */
    min-inline-size: var(--omx-control-height);
    min-block-size: var(--omx-control-height);
    padding-inline: var(--omx-space-3);
    border-radius: var(--omx-radius-sm);
    color: var(--omx-color-text-muted);
    text-decoration: none;
    font-variant-numeric: tabular-nums;
  }

  .omx-pager__link:hover { background: var(--omx-color-control-hover); color: var(--omx-color-text); }
  .omx-pager__link:active:not(.omx-pager__link--inert) { background: var(--omx-color-control-active); }

  /* The current page is marked by FILL and by WEIGHT, not by colour alone. */
  .omx-pager__link[aria-current="page"] {
    background: var(--omx-color-accent);
    color: var(--omx-color-accent-text);
    font-weight: var(--omx-font-weight-semibold);
  }
  .omx-pager__link[aria-current="page"]:hover {
    background: var(--omx-color-accent-hover);
    color: var(--omx-color-accent-text);
  }

  /* Not a link at all — it has no href, so it cannot be focused or followed. */
  .omx-pager__link--inert { color: var(--omx-color-text-subtle); cursor: default; }
  /* omx-no-press: explicitly inert — the ellipsis between page ranges. It exists to NOT respond */
  .omx-pager__link--inert:hover { background: none; color: var(--omx-color-text-subtle); }

  .omx-pager__icon { inline-size: 14px; block-size: 14px; flex: none; }
  .omx-pager__gap { padding-inline: var(--omx-space-2); color: var(--omx-color-text-subtle); }

  .omx-pager__size { display: flex; align-items: center; gap: var(--omx-space-3); }
  .omx-pager__size-label { color: var(--omx-color-text-muted); white-space: nowrap; }
  .omx-pager__size-select { inline-size: auto; }

  /* Below the narrow breakpoint the numbers are the first thing to go: on a
   * phone, Previous/Next plus the count is the whole useful control. */
  @container omx-slot (max-width: 480px) {
    .omx-pager__number, .omx-pager__gap { display: none; }
    .omx-pager__list { margin-inline-start: 0; }
    .omx-pager { justify-content: space-between; }
  }

  @media print {
    /* The controls do nothing on paper; the count is the fact worth printing. */
    .omx-pager__list, .omx-pager__size { display: none; }
  }
}

/* --- Navigation/Shell/Shell.css ------------------------------------ */
/* =============================================================================
 * L7 · Navigation · Shell — the application frame.
 *
 * DESIGN DIRECTION: "quiet instrument panel". Dieter Rams, applied to a dense
 * ERP rather than to a marketing page:
 *
 *   · Navigation RECEDES. The sidebar sits on the sunken surface and is
 *     separated by a hairline; the content area is the bright plane. The first
 *     RCL did the opposite — a saturated three-stop navy gradient that competes
 *     with the data all day.
 *   · A rule, not a shadow. Every division in this frame is 1px.
 *   · The active item is marked PRECISELY — a 2px inline bar and a weight
 *     change — rather than with a large filled pill.
 *   · Nothing animates except what the user caused.
 *
 * ⭐ It is also measurably simpler: the quiet sidebar needs ZERO sidebar-specific
 * colour tokens. The old palette carried **19** `--color-sidebar-*` entries,
 * each needing a light value and a dark value, and each one a chance for the two
 * to drift. The brand variant below is opt-in and confined to four lines.
 * ========================================================================== */

@layer omx.layout {

  .omx-shell {
    display: grid;
    grid-template-columns: var(--omx-sidebar-width) minmax(0, 1fr);
    grid-template-rows: 100dvh;
    /* dvh, not vh: on mobile, vh is the TALLEST possible viewport, so a shell
     * sized in vh is taller than the visible area while the URL bar shows and
     * the topbar scrolls off. */
    min-block-size: 100dvh;
  }

  .omx-shell[data-omx-collapsed="true"] { --omx-sidebar-width: var(--omx-sidebar-rail); }
  /* the collapsed width is a LOCAL override of the token, which is the
   * supported pattern — the token supplies the default. */

  .omx-shell__main {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    min-inline-size: 0;
    min-block-size: 0;
    background: var(--omx-color-canvas);

    /* 🔴🔴 THE TOPBAR'S FOUR RESPONSIVE RULES MATCHED NOTHING, AT ANY WIDTH.
     * They are `@container omx-content (max-width: …)`, and `omx-content` was
     * declared on `.omx-shell__content` — which is the topbar's SIBLING, not its
     * ancestor. A container query matches DESCENDANTS of the container only.
     *
     * Measured at 375px: `.omx-topbar__actions` stayed 455px wide and pushed the
     * document to 637px, so EVERY PAGE IN THE ESTATE scrolled horizontally below
     * 640px. The rules that would have hidden the search label, the "Switch
     * module" text and the user's name were all there and all inert.
     *
     * ⭐ This is the third container-query-against-the-wrong-element bug in this
     * package — the same shape `check-omx` C13 was written for. C13 catches a
     * query against its OWN container; it does not catch a query against a
     * container that is not an ancestor. That gap is now C17.
     *
     * The column that holds BOTH the topbar and the content is the right
     * container, and it is this element. */
    container-type: inline-size;
    container-name: omx-content;
  }

  .omx-shell__content {
    min-inline-size: 0;
    overflow: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;

    /* The content area is also named `omx-content`, and that is deliberate:
     * two containers may share a name and the NEAREST ancestor wins. A page
     * inside the content area resolves to this one (which excludes the topbar's
     * own height and any future gutter); the topbar itself, which is a sibling,
     * resolves to `.omx-shell__main` above. Before that outer container existed
     * the topbar's queries resolved to NOTHING. */
    container-type: inline-size;
    container-name: omx-content;
  }

  /* The skip target must be focusable, and must not show a ring when focus was
   * moved by SCRIPT rather than by the keyboard. `:focus-visible` is exactly
   * that distinction, so the negation is what to write — a bare `:focus
   * { outline: none }` removes the ring for keyboard users too, which
   * `check-omx` C8 named. */
  .omx-shell__content:focus:not(:focus-visible) { outline: none; }

  /* ===========================================================================
   * MOBILE — the rail becomes a drawer.
   * Below 768px the sidebar leaves the grid entirely rather than shrinking: a
   * 56px rail on a phone is 15% of the screen spent on icons nobody can read.
   * ======================================================================== */
  @media (max-width: 768px) {
    .omx-shell { grid-template-columns: minmax(0, 1fr); }

    /* ⭐ STAGE 2 — THE DRAWER RULES MOVED TO `Navigation/Sidebar/Sidebar.css`,
     * with the markup they style. What is left here is the SHELL's business:
     * the grid loses its first column because the rail has left the flow. A
     * component that owns its element and not its own responsive behaviour is
     * the same half-a-component split this stage closed in the markup. */
  }

  /* No complementary `min-width: 769px` rule: 769 is off the four-step scale,
   * and a complement is only needed because the rule was written desktop-first.
   * The scrim is hidden by DEFAULT and revealed inside the mobile block above,
   * which is the same behaviour with one breakpoint instead of two. */

  /* ===========================================================================
   * BRAND TONE — opt-in, four lines.
   * The sidebar is where a tenant sees the product's identity, and that is the
   * owner's call rather than this package's. `Tone = SidebarTone.Brand`.
   * ======================================================================== */
  .omx-shell--brand .omx-sidebar {
    --omx-sidebar-bg: linear-gradient(180deg,
      var(--omx-brand-nav-1) 0%, var(--omx-brand-nav-2) 56%, var(--omx-brand-nav-3) 100%);
    --omx-sidebar-fg: var(--omx-brand-nav-fg);
    --omx-sidebar-fg-muted: var(--omx-brand-nav-fg-dim);
    --omx-sidebar-rule: var(--omx-brand-nav-rule);
  }

  @media print {
    .omx-shell { display: block; min-block-size: 0; }
    .omx-sidebar, .omx-sidebar__scrim { display: none; }
    .omx-shell__content { overflow: visible; }
  }
}

/* --- Navigation/Sidebar/Sidebar.css -------------------------------- */
/* =============================================================================
 * L7 · Navigation · Sidebar — the left menu.
 *
 * Four local variables carry the whole tone, so the brand variant in Shell.css
 * re-points four lines instead of maintaining a parallel 19-token palette.
 * ========================================================================== */

@layer omx.component {

  /* 🔴 RENAMED FROM `.omx-shell__sidebar` IN STAGE 2, and the old name was the
   * tell. This file has always styled this element — `Sidebar.css` owned a
   * `shell__` class — because the markup lived in the shell layout while the
   * styling lived with the component. One of the two had to be wrong, and the
   * element belongs to the component that now renders it.
   * ⚠️ Safe to rename: a consuming module ships ZERO CSS (`check-module-ui` M1),
   * so nothing outside this package can be selecting it. */
  .omx-sidebar {
    /* 🔴🔴 THE RAIL IS A STEP **UP** FROM THE CANVAS, NOT DOWN — ruled
     * 2026-09-20, after the measurement refused the other direction.
     *
     * It was `surface-sunken`, and MEASURED that put the rail **deltaE2000
     * 0.61** from the page canvas against a just-noticeable difference of about
     * 2.3: not a faint plane — no plane at all. The obvious repair, darkening
     * it, is the one thing this palette cannot do. `surface-sunken` clears the
     * text floor by **+0.03** and the non-text floor by **+0.06**; one step
     * darker (#e2e8f2) puts `text-subtle` at 4.21 against 4.5 and
     * `border-control` at 2.84 against 3.0. That is the plane ceiling, and the
     * rail was already standing on it.
     *
     * ⭐ Going UP costs nothing and buys everything: worst text on the rail
     * improves 4.53 -> 5.18, worst graphic 3.06 -> 3.50, and the separation
     * from the canvas goes 0.61 -> **3.89 deltaE**, well past the threshold.
     * The rail and the topbar now share one surface, so the chrome reads as a
     * single L around a tinted content canvas rather than as two rectangles
     * that happen to touch.
     * ⚠️ `Shell.css`'s brand variant re-points this local to its gradient and is
     * unaffected — quiet in a module, brand on the launcher, as ruled. */
    /* 🔧 2026-09-20 -- these four now resolve to the CHROME plane rather than
     * straight to the palette. Byte-identical today (chrome defaults to exactly
     * these tokens); the indirection is what lets a tenant colour reach a
     * component-local variable it could not otherwise override. */
    --omx-sidebar-bg: var(--omx-color-chrome);
    --omx-sidebar-fg: var(--omx-color-chrome-text);
    --omx-sidebar-fg-muted: var(--omx-color-chrome-muted);
    --omx-sidebar-rule: var(--omx-color-chrome-rule);

    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    min-block-size: 0;
    background: var(--omx-sidebar-bg);
    color: var(--omx-sidebar-fg);
    /* A rule, not a shadow — the frame's only division. */
    border-inline-end: var(--omx-border-width) solid var(--omx-sidebar-rule);
  }

  /* ===========================================================================
   * HEAD — brand, then module, then workspace. Three lines of identity in
   * decreasing permanence, which is also decreasing visual weight.
   * ======================================================================== */
  /* The head is now only a grouper — the rule and the padding belong to the two
   * rows inside it. */
  .omx-sidebar__head { display: grid; }

  /* ⭐⭐ THE ONE ROW THAT HAS TO MATCH THE TOPBAR.
   * `--omx-topbar-height` rather than a literal 52px: the two heights are the
   * same FACT, and a second copy of a fact is a future disagreement. Its rule is
   * the continuation of the topbar's, which is what makes the chrome one L
   * instead of two rectangles that touch.
   * ⚠️ `min-block-size`, not `block-size` — the same reason the topbar uses it:
   * a hard height clips the brand at the larger text scales instead of growing,
   * and the two have to grow together or the rules part company again. */
  .omx-sidebar__identity {
    position: relative;
    display: flex;
    align-items: center;
    /* 🔴 NO LONGER THE TOPBAR'S HEIGHT, and that is costed, not drift.
     * A 52px mark cannot live in a 52px row, and the full dimensioned cut needs
     * that size to carry its ring, crossing and lattice. So the head stops
     * pretending to be a continuation of the topbar and becomes its own PLATE
     * ⭐ which is exactly what the corner ticks below are for: a framed
     * object reads as a decision, an unframed taller box reads as a mistake. */
    min-block-size: var(--omx-sidebar-plate-height, 80px);
    padding-inline: var(--omx-space-6);
    border-block-end: var(--omx-border-width) solid var(--omx-sidebar-rule);

    /* ⭐⭐ THE PLINTH. Measured before it was written: this zone's
     * background computed to `rgba(0, 0, 0, 0)` -- the logo floated on blank
     * wall with a hairline under it, and that is what read as ordinary.
     * A wash of about 5% brand, fading out before the rule, makes the top of
     * the rail a SURFACE the identity sits on rather than the absence of one.
     * ⭐ Keyed to `--omx-color-accent`, which OMX-15 put on the tenant
     * allow-list -- so a tenant's own brand colour flows through here for free
     * and the plinth is never off-brand. ⛔ Not a hardcoded blue. */
    background-image: linear-gradient(
      to bottom,
      color-mix(in oklab, var(--omx-color-accent) 6%, transparent),
      transparent 78%);
  }

  /* ⭐ THE SEAM. A 2px brand edge down the leading side of the identity
   * zone, masked so it fades out before the rule rather than stopping dead.
   * This is the one machined detail: it says the chrome was engineered, and it
   * costs no space because the rail's padding already left the edge empty.
   * ⚠️ `currentColor` in the mask, not a literal -- only the ALPHA of a mask
   * image is read, so an opaque keyword is exactly right AND it keeps the file
   * clear of C6, which forbids a raw colour outside tokens.css. */
  .omx-sidebar__identity::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: 2px;
    background: var(--omx-brand-sweep);
    -webkit-mask-image: linear-gradient(to bottom, currentColor 10%, transparent 88%);
    mask-image: linear-gradient(to bottom, currentColor 10%, transparent 88%);
    pointer-events: none;
  }

  /* ⭐⭐ THE RULE AS A COMPOSITIONAL ELEMENT, which is the Swiss move.
   * Every other hairline in this package is one flat colour across its whole
   * width. This one carries the brand for the width of the lockup and then
   * releases into the ordinary rule -- so the 143px of empty rail to the right
   * of the logo becomes deliberate emptiness instead of leftover space.
   * ⚠️ It sits ON the existing border rather than replacing it: the border is
   * the structural rule that has to line up with the topbar's, and losing it
   * would break the one-L chrome that `--omx-topbar-height` exists to keep.
   * ⭐⭐ AND IT IS DERIVED, NOT A MAGIC NUMBER. It was 132px, guessed from a
   * screenshot. It now hangs off `.omx-sidebar__brand`, which IS the lockup --
   * so it ends exactly where the wordmark ends at every text scale and at any
   * brand size, instead of being right at one of them. */
  .omx-sidebar__brand {
    position: relative;
  }

  .omx-sidebar__brand::after {
    content: "";
    position: absolute;
    /* Pulled back to the rail's own edge so it starts flush with the seam,
     * then run to the end of the lockup. */
    inset-inline-start: calc(-1 * var(--omx-space-6));
    inline-size: calc(100% + var(--omx-space-6));
    /* Down onto the identity zone's border. The brand box is centred in a
     * 52px row, so this reaches from the lockup to the rule below it. */
    /* ⚠️ Derived from the PLATE's height now, not the topbar's. The brand
     * box is centred in the plate, so this reaches from the lockup down onto
     * the plate's own rule. Pointing it at the topbar would have left it
     * floating mid-air the moment the plate grew. */
    inset-block-start: calc(100% + (var(--omx-sidebar-plate-height, 80px) - 100%) / 2);
    block-size: var(--omx-border-width-strong);
    background: var(--omx-brand-sweep);
    -webkit-mask-image: linear-gradient(to right, currentColor 55%, transparent);
    mask-image: linear-gradient(to right, currentColor 55%, transparent);
    pointer-events: none;
  }

  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__brand::after {
    display: none;
  }

  /* ⭐⭐ ONE LINE CROSSES THE WHOLE PLATE, IN TWO REGISTERS.
   * 🔴 THE DEFECT THIS FIXES, measured: the accent rule stopped at 205.3 and
   * the datum began at 211.2, so a 25.9px band ran from the lockup to the
   * plate's edge holding one faint graph and a corner tick that was on neither
   * the 16px padding grid (it sat at 6) nor the shared centre line at y=39.6
   * (it sat at 12). The plate had two halves that did not acknowledge each
   * other, and THAT is what reads as a gap rather than as a composition.
   * ⭐ So the rule continues past the lockup as DASHES, on exactly the same
   * line: brand gradient under the brand, instrument ticks under the
   * instrument. One line, two registers, no gap.
   * ⛔ The corner tick is REMOVED rather than realigned. With a divider and a
   * baseline framing the datum column it was a third mark saying what two were
   * already saying, and it was the only thing in the plate off the grid. */
  .omx-sidebar__identity::after {
    content: "";
    position: absolute;
    inset-inline-end: var(--omx-space-6);
    inline-size: 20px;
    inset-block-end: calc(-1 * var(--omx-border-width));
    block-size: var(--omx-border-width-strong);
    /* ⚠️ `currentColor` so the dashes take the colour set here rather than a
     * literal — C6 forbids a raw colour outside tokens.css. */
    /* 🔴 MEASURED AT 1.14 AGAINST THE RAIL when this used the rail's own
     * hairline token — present in the DOM and invisible on screen, while the
     * datum beside it sat at 2.5. One register, two weights.
     * ⭐ The instrument tone is the fix: the same value the datum and the
     * mark's ring already use, so the technical marks finally agree. */
    color: var(--omx-brand-instrument);
    opacity: 0.75;
    background-image: repeating-linear-gradient(
      to right, currentColor 0 2px, transparent 2px 5px);
    pointer-events: none;
  }

  /* ⭐⭐ THE DATUM ⛔ a lattice, not an ornament. The mark's own
   * caption on the company site reads "RESOLVES INTO INTELLIGENCE", and the
   * thing that resolves is a field of squares. This is that field, echoed at
   * the far side of the plate so the lockup and the empty edge belong to one
   * composition instead of the logo sitting next to nothing.
   * ⭐ Drawn with a repeating gradient and a mask, so it costs no markup, no
   * flow width and no request. The mask fades it away UP AND RIGHT, the same
   * direction the mark's own lattice dissolves.
   * ⚠️ `currentColor` in the mask rather than a literal: only the mask's
   * ALPHA is read, so an opaque keyword is exactly right, and C6 forbids a raw
   * colour outside tokens.css. */
  .omx-sidebar__datum {
    position: absolute;
    inset-inline-end: var(--omx-space-6);
    inset-block-start: 50%;
    translate: 0 -50%;
    /* ⚠️ 26, not 34. The lockup owns 181px of the rail's 216 usable and the
     * plate keeps 16px of padding, so 26 is what is actually left. A 34px field
     * was measured sitting 17.5px ON TOP of the wordmark. */
    /* ⚠️ 20, not 30. Every pixel here is taken from the title, and the
     * node graph still reads all five nodes at 20. Measured trade: 30px of
     * datum caps the title at 14.5px of letter; 20px lifts it to 16.2. */
    inline-size: 20px;
    block-size: 20px;
    pointer-events: none;
    /* 🔴 A NODE GRAPH, not the lattice that was here. The lattice repeated
     * the mark's own "resolves into a lattice" idea at the other end of the same
     * plate, which says nothing the mark had not already said ⛔ that is why
     * it read as filler rather than as a component.
     * ⭐ A graph says what the plate is about: nodes and edges are connected
     * systems, the left-to-right path is a pipeline, the feeder edges are work
     * arriving without a hand, and the one solid node is the resolved answer. */
    background-image: url("../brand/omnixa-datum.svg");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    opacity: 0.85;
  }

  /* ⭐⭐ THE DIVIDER — the single mark that closes the gap. A short
   * vertical hairline between the wordmark and the datum says these are two
   * REGISTERS OF ONE PANEL rather than a logo with something dropped in beside
   * it. It is the oldest instrument-panel move there is, and it costs 1px.
   * ⚠️ Short and centred, not full height: a full-height rule would divide
   * the plate into two boxes, which is the opposite of the intent. */
  .omx-sidebar__datum::before {
    content: "";
    position: absolute;
    /* 🔴 MEASURED, after the first attempt put it UNDER the wordmark. At
     * -12px the divider landed at x=199.2 while the lockup runs to 205.3, so it
     * was drawn and invisible. The real gap is 205.3..211.2 — 5.9px — so
     * -4 puts it inside that gap instead of behind the title. */
    inset-inline-start: calc(-1 * var(--omx-space-2));
    inset-block-start: 50%;
    translate: 0 -50%;
    inline-size: var(--omx-border-width);
    block-size: 22px;
    /* Same tone and same opacity as the baseline and the datum: the
     * instrument register is now ONE weight, not three. */
    background: var(--omx-brand-instrument);
    opacity: 0.75;
    pointer-events: none;
  }

  /* ⛔ The collapsed rail is one icon wide. The datum would sit on top of
   * the mark, and the plate has no spare edge to decorate. */
  /* ⛔ The collapsed rail is one icon wide: no room for a second register,
   * and the datum would sit on the mark. The whole instrument side goes. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__datum,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__module-rule,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__context::before,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__context::after,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__identity::after {
    display: none;
  }

  /* 🔴 Forced colors discards author backgrounds and gradients, so the
   * plinth, the seam and the accent all vanish -- correct, because the system
   * palette is the user's explicit instruction. Nothing here carries meaning
   * that is lost with them: this is all ground, and the logo is the figure. */

  /* Everything that is not the brand. ⛔ No rule of its own — a second horizontal
   * here would re-create the defect this split exists to remove. It separates by
   * whitespace, which is what the first section label below it does too. */
  .omx-sidebar__context {
    position: relative;
    display: grid;
    gap: var(--omx-space-3);
    padding: var(--omx-space-5) var(--omx-space-6);
  }

  /* ⭐⭐ ONE LINE DOWN THE LEADING EDGE, IN TWO REGISTERS — the move the
   * brand plate makes horizontally, turned through 90°. Plate 1 carries a 2px
   * brand-gradient seam (`__identity::before`); this is its instrument-register
   * continuation: 1px, `--omx-brand-instrument`, the tone the datum, the dashed
   * baseline and the mark's own ring already share. ⇒ the two plates read as
   * ONE PANEL WITH TWO REGISTERS rather than as a logo with text under it.
   * ⚠️ Masked to fade out before the nav instead of stopping dead, exactly as
   * the seam above does — a hairline that ends on nothing reads as a crop.
   * ⚠️ `currentColor` in the mask: only a mask's ALPHA is read, so an opaque
   * keyword is exactly right AND it keeps the file clear of C6. */
  .omx-sidebar__context::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: var(--omx-border-width);
    background: var(--omx-brand-instrument);
    opacity: 0.75;
    /* 🔧 THE FADE CAME OFF. It used to dissolve at 94% because it ended on
     * nothing; it now runs to the corner and TURNS, so it is the spine of a
     * bracket rather than a stroke that stops. See `::after`. */
    pointer-events: none;
  }

  /* ⭐⭐ THE TERMINATOR — what says the identity block ENDS.
   * 🔴 MEASURED, and it is the owner's complaint stated as numbers: the only
   * thing between "Ispahani Group" and the first menu label was **24px of
   * whitespace**, and those two lines are BOTH 12px at contrast 6.50 — same
   * size, same ink, differing only in weight and tracking. The identity block
   * and the menu ran into each other because nothing said otherwise.
   * ⛔ NOT A RULE ACROSS THE RAIL. The note on `__context` above is still
   * right: a second full-width horizontal re-creates the two-parallel-rules
   * defect that splitting the head existed to remove.
   * ⭐ So it is the same line one register down, RUN BACKWARDS. The plate rule
   * at the top is solid-under-the-brand then dashes at the instrument end;
   * this is dashes from the leading edge, then nothing. Same mark, mirrored,
   * carrying no brand gradient and spanning nothing — so it reads as the foot
   * of the bracket, not as a divider.
   * ⭐ With the plate rule above and the spine at the start, the identity block
   * is now FRAMED on three sides. That is the company site's own `plateframe`
   * idea, which the datum already borrows.
   * ⚠️ 96px = six units of the plate's own 16px grid, not a number read off a
   * screenshot. It deliberately stops short of the company name so it
   * underlines the COLUMN rather than the words. */
  .omx-sidebar__context::after {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    inset-block-end: 0;
    inline-size: calc(var(--omx-space-6) * 6);
    block-size: var(--omx-border-width);
    /* ⚠️ `currentColor` so the dashes take the colour set here — C6 forbids a
     * raw colour outside tokens.css, and a mask reads only ALPHA anyway. */
    color: var(--omx-brand-instrument);
    opacity: 0.75;
    background-image: repeating-linear-gradient(
      to right, currentColor 0 2px, transparent 2px 5px);
    -webkit-mask-image: linear-gradient(to right, currentColor 0 45%, transparent 100%);
    mask-image: linear-gradient(to right, currentColor 0 45%, transparent 100%);
    pointer-events: none;
  }

  .omx-sidebar__brand {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    font-family: var(--omx-font-display);
    font-size: var(--omx-font-size-title);
    font-weight: var(--omx-font-weight-semibold);
    letter-spacing: var(--omx-letter-spacing-tight);
    color: var(--omx-sidebar-fg);
    text-decoration: none;
    min-inline-size: 0;
  }
  /* 🔧 `omx-sidebar__brand-mark` -- the gradient square that stood in for a logo
   * until 2026-09-16 -- is gone. The sidebar now renders `<vc:brand>`
   * (Foundation/Brand) and the real mark comes with it. The rule is kept ONLY as
   * a fallback for a consumer still emitting the old markup from a cached view;
   * ⛔ do not use it in new markup. */
  .omx-sidebar__brand-mark {
    inline-size: 22px; block-size: 22px; flex: 0 0 auto;
    border-radius: var(--omx-radius-sm);
    background: linear-gradient(135deg, var(--omx-brand-blue), var(--omx-brand-cyan));
  }

  /* ⭐ COLLAPSED RAIL: the mark stays, the wordmark goes.
   * The old markup relied on `omx-sidebar__label` being hidden when collapsed
   * (see the collapse block below); the lockup's wordmark is not that class, so
   * it needs saying explicitly or a 24px rail would try to hold an 86px image.
   * ⚠️ Descendant + attribute qualified, so this is a state rule, not a blanket
   * hide -- which is what keeps it clear of C17. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__brand .omx-brand__word {
    display: none;
  }
  /* 🔴🔴 MEASURED BEFORE ANY OF THIS WAS WRITTEN, AND IT IS THE WHOLE
   * DEFECT: the module name (13px / 400), the company name (12px / 400) and
   * the "OVERVIEW" group heading below them (12px / 600) all computed to the
   * SAME INK — `rgb(80, 95, 116)`. Three lines, one tone: the module you are
   * standing in was styled exactly like a nav group heading, so the rail's
   * identity block had no hierarchy at all. ⛔ No ornament fixes that.
   * ⛔ AND THIS RULE COULD NOT FIX IT EITHER. The markup carried
   * `omx-text-label`, and `.omx-text-*` live in the omx.utility layer, which
   * beats omx.component — both declarations below were dead. The class came
   * off the markup; see Sidebar.cshtml.
   * ⭐ THE LADDER, now that one exists: the wordmark renders ~15.7px at 600,
   * this is 14 at 600 in the same display face and the same tight tracking,
   * and the company under it is 12 at 400. Brand, module, company. */
  .omx-sidebar__module {
    font-family: var(--omx-font-display);
    font-size: var(--omx-font-size-body);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    letter-spacing: var(--omx-letter-spacing-tight);
    color: var(--omx-sidebar-fg);
  }
  /* ---- the module line, and the rule that closes it ----------------------
   * ⛔ THE DOOR LEFT THIS ROW on 2026-09-22 (owner). `.omx-sidebar__switch`
   * and its three state rules went with it — they are `.omx-topbar__switch`
   * now, same glyph, same `--omx-module-spread` mechanism. */
  .omx-sidebar__module-row {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-sidebar__module-row .omx-sidebar__module {
    flex: 0 1 auto;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ⭐⭐ THE LEADER LINE — and it CLOSES the row rather than decorating it.
   * 🔴 MEASURED: the module name is 93.2px of a 216px row, so taking the
   * switcher out left ~120px of dead rail on the plate's most important line.
   * That is the same shape as the brand plate's 25.9px dead band, which is
   * what the owner felt there as *"have gap"* — and it would have been the
   * first thing to feel here too.
   * ⭐ A label with a rule running out to the frame is the drawing language
   * this plate already speaks, and it is DERIVED, not a magic width: `flex: 1`
   * closes the row to the 16px padding grid at EVERY name length and every
   * text scale, so the gap cannot re-open the way a fixed width would.
   * ⚠️ It fades IN from the title rather than butting against it: a hairline
   * touching a letter reads as a strikethrough that missed. */
  .omx-sidebar__module-rule {
    flex: 1 1 auto;
    min-inline-size: 0;
    block-size: var(--omx-border-width);
    background: var(--omx-brand-instrument);
    opacity: 0.75;
    -webkit-mask-image: linear-gradient(to right, transparent, currentColor 34%);
    mask-image: linear-gradient(to right, transparent, currentColor 34%);
  }

  /* 🔴 THIS LINE HAS ALWAYS BEEN A LINK AND NEVER LOOKED LIKE ONE — and its
   * press state was worse than nothing. `inline-flex` inside a grid STRETCHES,
   * so the anchor measured the full 215px row while its content was 96: the
   * `:active` background painted a hard-edged bar the whole width of the rail,
   * nowhere near the words being pressed.
   * ⭐ `justify-self: start` makes the box the CONTROL rather than the row, and
   * the negative margin buys the padding back so the node still sits on the
   * plate's 16px grid with the title above it. MEASURED, not assumed. */
  .omx-sidebar__workspace {
    justify-self: start;
    display: inline-flex; align-items: center; gap: var(--omx-space-4);
    max-inline-size: 100%;
    padding: var(--omx-space-1) var(--omx-space-3);
    margin-inline-start: calc(-1 * var(--omx-space-3));
    border-radius: var(--omx-radius-sm);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-sidebar-fg-muted);
    text-decoration: none;
    min-inline-size: 0;
    transition: background-color var(--omx-duration-fast) var(--omx-easing-standard),
                color var(--omx-duration-fast) var(--omx-easing-standard);
  }
  /* ⭐ A NODE, NOT A STATUS LIGHT.
   * 🔴 This dot was `--omx-color-success-text` — a SEMANTIC token meaning
   * "healthy" spent on decoration. Nothing in `ShellModel` carries a tenant
   * health state, so the rail was asserting green about something it cannot
   * know, and a real outage would have had no colour left to say so with.
   * ⭐ The instrument tone instead, in the shape the plate already uses: the
   * datum's one SOLID node is "the resolved answer", and which company you
   * are in is exactly that answer for this block. The halo is the node's
   * field, the same device the datum draws its edges into.
   * ⚠️ `box-shadow`, not a bigger box: the halo must not take flow width or
   * the name beside it loses 6px of truncation budget. */
  .omx-sidebar__workspace-dot {
    inline-size: 6px; block-size: 6px; flex: 0 0 auto;
    border-radius: var(--omx-radius-pill);
    background: var(--omx-brand-instrument);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--omx-brand-instrument) 20%, transparent);
  }
  /* ⚠️ Hover and press as a PAIR — C20 exists because a hover with no press
   * partner leaves a control that lights up and then does not acknowledge the
   * click. Only the anchor form gets them: the plain <span> fallback is not
   * pressable and must not pretend to be. */
  a.omx-sidebar__workspace:hover {
    color: var(--omx-sidebar-fg);
    background: var(--omx-color-control-hover);
  }
  a.omx-sidebar__workspace:active { background: var(--omx-color-control-active); }

  /* ===========================================================================
   * NAV
   * ======================================================================== */
  .omx-sidebar__nav {
    overflow: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
    /* ⭐ 20 at the top, 12 at the bottom. The menu is the rail's second half
     * and it now starts under a terminator, so it needs to begin rather than
     * merely continue: 12px put the first group label the same distance below
     * the identity block as the labels sit above their own items. */
    padding-block: var(--omx-space-7) var(--omx-space-5);
    min-block-size: 0;
  }

  .omx-sidebar__section + .omx-sidebar__section { margin-block-start: var(--omx-space-7); }

  /* ⭐⭐ EVERY HEADING IN THIS RAIL IS NOW A LABEL WITH A RULE RUNNING OUT TO
   * THE FRAME — the module title does it, and so does each group.
   * 🔴 THE DEFECT: each label sat alone in a 216px box with about **106px of
   * dead space** to its right. That is the brand plate's dead-band shape,
   * three times over, and it is why the menu read as loose text rather than
   * as structure.
   * ⭐⭐ A THIRD REGISTER, AND IT IS WHAT KEEPS THE RAIL FROM FLATTENING. If
   * these rules wore the instrument tone they would tie with the module
   * title's leader and the identity block would stop being senior. MEASURED
   * across both grounds:
   *     brand       2px, the brand gradient      the plate seam and rule
   *     instrument  1px, --omx-brand-instrument  3.01 light / 2.54 dark
   *     structure   1px, --omx-color-border-strong  1.95 / 1.90
   * ⛔ `border-subtle` (1.22 / 1.14) and `border` (1.33 / 1.38) are BOTH
   * invisible on this plane — 1.22 is the exact value that made the plate's
   * first instrument marks present in the DOM and absent on screen. And
   * `border-control` measures 3.50 / 3.53, which ties with the instrument.
   * `border-strong` is the only rung that is both visible and junior.
   * ⚠️ `padding-inline: <start> 0`, not the shorthand. The 8px was on BOTH
   * sides, so a rule filling the content box would have stopped at 224 while
   * the pills below it end at 232 — an 8px near-miss, which is the exact shape
   * the "two optical lines" note below was written about. */
  .omx-sidebar__section-label {
    /* ⭐ TWO OPTICAL LINES IN THIS RAIL, and everything sits on one of them:
     *   16px - the container line: the brand, the pill's outer edge, the foot
     *   24px - the content line:   the item icons, and this label
     * The label used to sit on 16 while the icons sat on 18.4, which is the
     * kind of near-miss that reads as sloppy without being nameable. */
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    margin-inline-start: var(--omx-space-6);
    padding-inline: var(--omx-space-4) 0;
    margin-block-end: var(--omx-space-3);
    color: var(--omx-sidebar-fg-muted);
    /* An index, not a heading — overline weight keeps it from competing with
     * the items it labels. */
  }

  /* ⚠️ It fades IN from the words rather than butting against them, the same
   * as the module title's leader: a hairline touching a letter reads as a
   * strikethrough that missed. */
  .omx-sidebar__section-label::after {
    content: "";
    flex: 1 1 auto;
    min-inline-size: 0;
    block-size: var(--omx-border-width);
    background: var(--omx-color-border-strong);
    -webkit-mask-image: linear-gradient(to right, transparent, currentColor 30%);
    mask-image: linear-gradient(to right, transparent, currentColor 30%);
  }

  .omx-sidebar__list { display: grid; }

  .omx-sidebar__link {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    position: relative;
    min-block-size: var(--omx-control-height);

    /* ⭐⭐ THE ITEM IS A PILL INSIDE THE RAIL, NOT A BAND ACROSS IT.
     * Owner ruling 2026-09-20. A full-bleed, square-cornered selection reads as
     * a table row; an inset rounded one reads as a chosen thing. That is the
     * single biggest difference between this rail and the ones it is measured
     * against.
     *
     * 🔴 AND IT FIXED A DEFECT NOBODY HAD NAMED. `.omx-sidebar__nav` carries
     * `scrollbar-gutter: stable`, which reserves 15px on the inline-END side
     * and nothing on the start. MEASURED: the active band ran 0 -> 232 inside a
     * 248px rail — flush to the left edge and stopping 16px short of the right.
     * Invisible while the band was full-bleed, glaring the moment it is a pill.
     * ⇒ `margin-inline: <16px> 0` puts the pill's own margin on the start and
     * lets the reserved gutter be the end margin: 16 and 16, symmetric, without
     * hard-coding a scrollbar width that differs per platform.
     *
     * ⚠️ The 3px indicator moved from `border-inline-start` to a `::before` for
     * this: a border would land INSIDE the rounded corner and push the content
     * line to 19px, which is on no scale step. The pseudo-element is clipped by
     * the same radius and leaves the padding box alone. */
    margin-inline: var(--omx-space-6) 0;
    padding-block: var(--omx-space-2);
    padding-inline: var(--omx-space-4);
    border-radius: var(--omx-radius-md);

    font-size: var(--omx-font-size-body);
    /* 🔴 WAS `--omx-sidebar-fg-muted` — THE SAME TOKEN AS THE GROUP LABEL ABOVE
     * IT. Measured at render: both resolved to rgb(80, 95, 116) — a contrast
     * ratio of 1.00 between a heading and the items it heads. Size and weight
     * were doing the whole job of saying which was the parent, and an uppercase
     * 12px label in the identical ink reads as a peer, not as an index.
     * ⭐ The item is what a person reads and clicks, so the item takes the full
     * ink and the label keeps the muted one. That is the hierarchy the two
     * tokens were named for; the link had simply taken the wrong one. */
    color: var(--omx-sidebar-fg);
    text-decoration: none;
    cursor: pointer;
    min-inline-size: 0;

    transition:
      background-color var(--omx-duration-fast) var(--omx-easing-standard),
      color var(--omx-duration-fast) var(--omx-easing-standard);
  }

  .omx-sidebar__label {
    flex: 1;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ⭐ AN ICON IS A LOCATOR, NOT CONTENT. At rest it sat on
   * `--omx-sidebar-fg` — the IDENTICAL ink as the label beside it — so fourteen
   * glyphs competed with the fourteen words they were there to help find.
   * One step back at rest; full ink the moment the row is hovered or current.
   * ⚠️ Non-text, so it is gated at 3.0 not 4.5: `text-muted` on the rail plane
   * measures 5.48, which clears it with room. */
  .omx-sidebar__icon {
    inline-size: var(--omx-size-icon-inline);
    block-size: var(--omx-size-icon-inline);
    flex: 0 0 auto;
    color: var(--omx-sidebar-fg-muted);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
  }

  .omx-sidebar__link:hover {
    background: var(--omx-color-control-hover);
    color: var(--omx-sidebar-fg);
    text-decoration: none;
  }
  .omx-sidebar__link:active:not([aria-disabled="true"]) { background: var(--omx-color-control-active); }
  /* The icon rejoins the label whenever the row is live. */
  .omx-sidebar__link:hover .omx-sidebar__icon,
  .omx-sidebar__link[aria-current="page"] .omx-sidebar__icon { color: inherit; }

  /* 🔴 THE ACTIVE STATE HOOKS OFF aria-current, NOT A CLASS.
   * The first RCL computed active state server-side and painted it with a
   * class; aria-current appeared ZERO times in the whole shell, so a screen
   * reader announced every item identically. Styling from the attribute means
   * an unstyled active item and an unannounced one are the SAME bug — one fix
   * closes both, and neither can regress alone. */
  .omx-sidebar__link[aria-current="page"] {
    background: var(--omx-color-accent-subtle);
    color: var(--omx-color-accent-hover);
    font-weight: var(--omx-font-weight-semibold);
  }

  /* 🔴🔴 ONE HUE PER SELECTED ROW — OWNER RULING 2026-09-20, AND IT REVERSES
   * THE RULING OF 2026-09-19 that made this bar `--omx-color-brand-mark`.
   * That earlier note called it "the one brand-coloured pixel on a working
   * page". MEASURED at render on 2026-09-20: the bar was #8b5cf6 violet while
   * the label and icon of the same row were #2059cf blue — two hues on one
   * object, which reads as an accident rather than as identity.
   * ⇒ The bar joins the accent. The brand now lives in the rail head and
   *   nowhere else on a working page; that is the cost, and it was accepted.
   * ⚠️ `check-omx` CT carried a dedicated pair for the OLD relationship
   * (`brand-mark` on `accent-subtle`). It was repointed in the same change —
   * a gate that measures a relationship the package no longer has is a gate
   * measuring nothing. */
  .omx-sidebar__link[aria-current="page"]::before {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    /* Inset from the pill's ends so it reads as part of the pill rather than
     * as a bar the pill was dropped onto. */
    inset-block: var(--omx-space-2);
    inline-size: 3px;
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-accent);
  }

  .omx-sidebar__link[aria-disabled="true"] {
    color: var(--omx-color-text-subtle);
    cursor: not-allowed;
  }
  /* omx-no-press: aria-disabled. It stays focusable so it can explain itself, but it must not look pressable */
  .omx-sidebar__link[aria-disabled="true"]:hover { background: none; }

  .omx-sidebar__badge { flex: 0 0 auto; }

  .omx-sidebar__kbd {
    flex: 0 0 auto;
    font-family: var(--omx-font-mono);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-subtle);
  }

  /* -- expandable groups: native <details> ---------------------------------- */
  .omx-sidebar__link--parent { list-style: none; }
  .omx-sidebar__link--parent::-webkit-details-marker { display: none; }

  .omx-sidebar__chevron {
    inline-size: 12px; block-size: 12px; flex: 0 0 auto;
    color: var(--omx-color-text-subtle);
    transition: rotate var(--omx-duration-fast) var(--omx-easing-standard);
  }
  .omx-sidebar__group[open] > .omx-sidebar__link--parent .omx-sidebar__chevron { rotate: 90deg; }

  .omx-sidebar__link--child {
    /* Aligned under the parent's LABEL, not under its icon — the indent has to
     * read as "belongs to that", and lining up with the icon reads as a
     * sibling. */
    padding-inline-start: calc(var(--omx-space-6) + var(--omx-size-icon-inline) + var(--omx-space-4));
    font-size: var(--omx-font-size-body);
  }

  /* ===========================================================================
   * FOOT — the collapse control.
   * ======================================================================== */
  .omx-sidebar__foot {
    /* 16px inline, so the collapse control starts on the same container line as
     * the brand above it and the pills between them. It was on 12. */
    padding: var(--omx-space-4) var(--omx-space-6);
    border-block-start: var(--omx-border-width) solid var(--omx-sidebar-rule);
  }
  .omx-sidebar__collapse {
    inline-size: 100%;
    justify-content: flex-start;
    color: var(--omx-sidebar-fg-muted);
  }
  .omx-sidebar__collapse-icon { transition: rotate var(--omx-duration-normal) var(--omx-easing-standard); }

  /* ===========================================================================
   * COLLAPSED RAIL — icons only.
   * The labels are HIDDEN, not removed: each link keeps its `title`, so the
   * name is still available on hover and still in the accessibility tree.
   * ======================================================================== */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__label,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__section-label,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__badge,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__kbd,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__chevron,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__module,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__workspace-name,
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__collapse .omx-button__label {
    /* clip, not display:none — the text stays in the accessibility tree, so the
     * collapsed rail still announces "Invoices" rather than "link". */
    position: absolute;
    inline-size: 1px; block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* ---------------------------------------------------------------------------
   * The collapsed rail's tooltip. Shown ONLY while collapsed — `Sidebar.js`
   * refuses to open it otherwise, so an expanded rail cannot sprout tooltips
   * that repeat labels already on screen.
   * ⚠️ WCAG 1.4.13: dismissable (Escape), hoverable (pointer-events stay off so
   * it can never steal the pointer from the item it describes) and persistent
   * (no timer). Same contract Surfaces/Tooltip documents.
   * ⚠️ THIS DOES NOT REACH TOUCH, and no hover tooltip does. It is an aid for
   * mouse and keyboard on top of an accessible name that is always present —
   * never the only place the name lives. Tooltip.css opens with that rule. */
  .omx-sidebar__tip {
    position: fixed;
    z-index: var(--omx-z-tooltip);
    inline-size: max-content;
    max-inline-size: 26ch;
    padding: var(--omx-space-3) var(--omx-space-5);
    font-size: var(--omx-font-size-caption);
    line-height: var(--omx-line-height-body);
    /* Inverted, so it reads as an overlay rather than as more rail. */
    color: var(--omx-color-text-inverse);
    background: var(--omx-color-text);
    border-radius: var(--omx-radius-sm);
    box-shadow: var(--omx-shadow-xl);
    pointer-events: none;
  }

  /* 🔴 THE COLLAPSED ICONS WERE 6.8px OFF-CENTRE, and had been all along.
   * `scrollbar-gutter: stable` reserves 15px at the inline end of the nav
   * whether or not anything ever scrolls. MEASURED collapsed: a 56px rail, a
   * 40px content box, icon centres at 21.2 where the rail's centre is 28.
   * On a 248px rail 15px of reserved gutter is invisible; on a 56px one it is
   * a quarter of the width, and the whole icon column sits visibly left.
   * ⇒ No gutter while collapsed. There is nothing to stabilise: the labels are
   * clipped away, so the collapsed rail's content cannot change width. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__nav { scrollbar-gutter: auto; }

  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__link {
    justify-content: center;
    margin-inline: var(--omx-space-2);
    padding-inline: 0;
    gap: 0;
  }
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__identity {
    padding-inline: 0;
    justify-content: center;
  }
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__context {
    padding-inline: 0;
    justify-items: center;
  }
  /* ⚠️ `justify-self` on the workspace BEATS the `justify-items` above, so
   * without this the collapsed rail would left-align the node against a rail
   * that centres everything else. And the halo goes: a 12px glowing node
   * alone in a 56px rail is louder than the 6px dot that was here before,
   * and it labels nothing once the company name is clipped away.
   * 🔴 REPORTED, ⛔ NOT FIXED: the dot itself has always survived the
   * collapse with no name beside it, which says nothing to anyone. That is a
   * pre-existing call about what the collapsed rail should show, not part of
   * this plate. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__workspace {
    justify-self: center;
    margin-inline: 0;
    padding-inline: 0;
  }
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__workspace-dot {
    box-shadow: none;
  }
  /* 🔴🔴 THE COLLAPSED RAIL HAD NO VISIBLE WAY BACK. Owner, 2026-09-22:
   * "i have minimize the menu but no where showing the expanding sign", in BOTH
   * modes — and it was never a colour problem.
   * ⛔ MEASURED, because "the markup is present" is exactly what the sign-out
   * incident passed on while the control was 0x0. The button rendered, hit-
   * tested and carried `aria-expanded="false"` correctly. Its CHEVRON measured
   * **0 x 16**:
   *     button        25.6 wide
   *     its padding   12 + 12 = 24   (the ghost button's own)
   *     its border    0.8 + 0.8
   *     content box   25.6 - 24 - 1.6 = **0.00px**
   * ⭐ EVERY OTHER COLLAPSED CONTROL ALREADY HAD THE FIX. The rule above gives
   * `.omx-sidebar__link` `padding-inline: 0` and centres it, which is why the
   * nav glyphs survive the 56px rail. The foot was simply never included — the
   * collapsed rail was written as "icons only" and the one control that is not
   * a nav item was missed.
   * ⭐ `--omx-control-height` for the width, not a literal 32: it makes the
   * button SQUARE by construction and it tracks the density scale, so the
   * compact and touch builds cannot drift apart. 32x32 also clears WCAG 2.5.8.
   * ⚠️ CENTRED ON THE ICON COLUMN, NOT ON THE RAIL. Measured: every collapsed
   * nav icon sits at x-centre **20** while the 56px rail's centre is **28**.
   * Centring this on the rail put it 8px right of the column it belongs to, so
   * it matches the links instead — same 32x32 box at x=4, same glyph at x=12.
   * 🔴 That 8px column offset is a SEPARATE, pre-existing defect and is
   * reported, ⛔ not fixed here: moving the whole column is not this change. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__foot {
    padding-inline: var(--omx-space-2);
  }
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__collapse {
    inline-size: var(--omx-control-height);
    justify-content: center;
    padding-inline: 0;
    gap: 0;
  }
  /* The chevron turns to point OUT of the rail — the direction the rail will
   * move, which is the only thing a bare glyph can say about what it does.
   * ⚠️ The accessible NAME still reads "Collapse" while collapsed. `aria-expanded`
   * carries the true state, but the two disagree. Reported, ⛔ not fixed here:
   * it needs a conditional label and a new text key, not a CSS rule. */
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__collapse-icon { rotate: 180deg; }
  .omx-shell[data-omx-collapsed="true"] .omx-sidebar__section + .omx-sidebar__section {
    margin-block-start: var(--omx-space-5);
    border-block-start: var(--omx-border-width) solid var(--omx-sidebar-rule);
    padding-block-start: var(--omx-space-5);
  }

  /* ===========================================================================
   * MOBILE — the rail becomes a drawer. ⭐ MOVED HERE FROM `Shell.css` IN STAGE
   * 2, with the markup. Below 768px the rail leaves the grid entirely rather
   * than shrinking: a 56px rail on a phone is 15% of the screen spent on icons
   * nobody can read.
   * ======================================================================== */
  @media (max-width: 768px) {
    .omx-sidebar {
      position: fixed;
      inset-block: 0;
      inset-inline-start: 0;
      z-index: var(--omx-z-sidebar);
      inline-size: min(var(--omx-sidebar-width), 84vw);
      translate: -100% 0;
      transition: translate var(--omx-duration-normal) var(--omx-easing-standard);
      box-shadow: var(--omx-shadow-lg);
    }
    .omx-shell[data-omx-sidebar-open="true"] .omx-sidebar { translate: 0 0; }

    /* ⛔ `.omx-shell` IS REQUIRED ON THIS SELECTOR, NOT DECORATION. The scrim is
     * a full-viewport fixed element, and the component is now rendered outside a
     * shell too — the gallery samples the real rail. Without the ancestor, the
     * sample would lay a fixed sheet over the page demonstrating it at any
     * width under 768px: invisible (opacity 0) and therefore the worst kind. */
    .omx-shell .omx-sidebar__scrim {
      display: block;
      position: fixed;
      inset: 0;
      z-index: var(--omx-z-scrim);
      background: var(--omx-color-surface-overlay);
      border: 0;
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--omx-duration-fast) var(--omx-easing-standard);
    }
    .omx-shell[data-omx-sidebar-open="true"] .omx-sidebar__scrim { opacity: 1; pointer-events: auto; }
  }

  /* Hidden by default; the mobile block above reveals it. 🔧 This rule sat AFTER
   * that block for one revision and silently won on source order — the scrim
   * never appeared. ⚠️ It is written LAST here and still correct, because the
   * reveal above is more specific (`.omx-shell .omx-sidebar__scrim`), not
   * because of order. Specificity is the durable argument; order is not. */
  .omx-sidebar__scrim { display: none; }

  @media (forced-colors: active) {
    .omx-sidebar { border-inline-end-color: CanvasText; }
    .omx-sidebar__link[aria-current="page"] {
      background: Highlight; color: HighlightText;
    }
    /* ⚠️ Follows the indicator to `::before`. Forced-colors mode discards
     * author backgrounds, so without this the bar simply vanishes for the
     * people who most need a position marker. */
    .omx-sidebar__link[aria-current="page"]::before { background: HighlightText; }
  }
}

/* --- Navigation/SignIn/SignIn.css ---------------------------------- */
/* =============================================================================
 * L7 · Navigation · SignIn — the front door.
 *
 * ⚠️ NO branding here. The organisation name, the mark and the footnote belong
 * to Layouts/_OmxAuth.cshtml, which a tenant configures once. A form that
 * carries its own logo is a form that has to be edited per tenant.
 * ========================================================================== */

@layer omx.component {

  /* The mark sits above the heading, at the card's own alignment. ⭐ Mark-only,
   * not the lockup: the wordmark beside a title that already says the product
   * name is the same word twice, and the card is narrow. */
  .omx-signin__brand { margin-block-end: var(--omx-space-5); }


  .omx-signin { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-signin__heading { margin: 0; text-align: center; }

  .omx-signin__lead {
    color: var(--omx-color-text-muted);
    margin: 0;
    text-align: center;
  }

  .omx-signin__error {
    display: flex;
    align-items: flex-start;
    gap: var(--omx-space-3);
    margin: 0;
    padding: var(--omx-space-4);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-danger-bg);
    color: var(--omx-color-danger-text);
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-danger-text);
  }
  .omx-signin__error svg { inline-size: 15px; block-size: 15px; flex: none; margin-block-start: 1px; }

  .omx-signin__notice {
    margin: 0;
    padding: var(--omx-space-4);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-info-bg);
    color: var(--omx-color-info-text);
  }

  .omx-signin__form { display: grid; gap: var(--omx-space-5); }
  .omx-signin__aside { text-align: center; margin: 0; }

  /* -- SSO ----------------------------------------------------------------- */

  .omx-signin__sso { display: grid; gap: var(--omx-space-3); }
  .omx-signin__sso-button { inline-size: 100%; justify-content: center; }

  /* A rule with the word "or" in it. The pseudo-elements are decoration; the
   * word is real text, so it is read and it survives forced-colors. */
  .omx-signin__or {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    margin: 0;
    color: var(--omx-color-text-subtle);
    font-size: var(--omx-font-size-caption);
  }
  .omx-signin__or::before,
  .omx-signin__or::after {
    content: "";
    flex: 1;
    border-block-start: var(--omx-border-width) solid var(--omx-color-border);
  }

  /* -- workspace choice ---------------------------------------------------- */

  .omx-signin__workspaces { display: grid; gap: var(--omx-space-2); }

  /* Each row IS the submit button: one press, and the whole row is the target. */
  .omx-signin__workspace {
    appearance: none;
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    inline-size: 100%;
    padding: var(--omx-space-4);
    text-align: start;
    font: inherit;
    color: var(--omx-color-text);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-sm);
    cursor: pointer;
    min-block-size: var(--omx-control-height);
  }
  .omx-signin__workspace:hover { background: var(--omx-color-control-hover); }
  .omx-signin__workspace:active { background: var(--omx-color-control-active); }

  .omx-signin__workspace--current { border-color: var(--omx-color-accent); }

  .omx-signin__workspace-text { display: grid; min-inline-size: 0; }
  .omx-signin__workspace-name { font-weight: var(--omx-font-weight-medium); }
  .omx-signin__workspace-hint { color: var(--omx-color-text-muted); }
  .omx-signin__workspace-current { margin-inline-start: auto; color: var(--omx-color-accent-hover); }

  /* -- locked -------------------------------------------------------------- */

  .omx-signin__locked-body { margin: 0; color: var(--omx-color-text); }

  @media print {
    /* Nobody prints a sign-in page on purpose. If it happens, do not print the
     * fields — a printed password box is a form somebody will write in. */
    .omx-signin__form, .omx-signin__sso { display: none; }
  }
}

/* --- Navigation/Tabs/Tabs.css -------------------------------------- */
/* =============================================================================
 * L7 · Navigation · Tabs — one region, several views of it.
 * ========================================================================== */

@layer omx.component {

  .omx-tabs { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-tabs__list {
    display: flex;
    gap: var(--omx-space-1);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border);
    /* Many tabs on a narrow pane scroll rather than wrap: a wrapped tablist
     * changes height as the selection moves and the content jumps. */
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    min-inline-size: 0;
  }

  .omx-tabs__tab {
    appearance: none;
    background: none;
    border: 0;
    /* 44px of target height including the padding (WCAG 2.5.8). */
    padding: var(--omx-space-4) var(--omx-space-5);
    font: inherit;
    font-weight: var(--omx-font-weight-medium);
    color: var(--omx-color-text-muted);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    border-block-end: var(--omx-border-width-strong) solid transparent;
    margin-block-end: calc(var(--omx-border-width) * -1);
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-2);
  }

  .omx-tabs__tab:hover:not([aria-disabled="true"]) { color: var(--omx-color-text); }
  .omx-tabs__tab:active:not([aria-disabled="true"]) { color: var(--omx-color-accent-active); }

  /* 🔴 The selected tab is marked by an UNDERLINE and by WEIGHT, not by colour
   * alone (WCAG 1.4.1) — and the underline survives forced-colors, where a
   * colour-only selection disappears entirely. */
  .omx-tabs__tab[aria-selected="true"],
  .omx-tabs__tab[aria-current="page"] {
    color: var(--omx-color-accent-hover);
    border-block-end-color: var(--omx-color-accent);
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-tabs__tab[aria-disabled="true"] {
    color: var(--omx-color-text-subtle);
    cursor: not-allowed;
  }

  .omx-tabs__badge {
    font-size: var(--omx-font-size-caption);
    font-variant-numeric: tabular-nums;
    padding: 0 var(--omx-space-2);
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-neutral-bg);
    color: var(--omx-color-neutral-text);
  }

  /* The panel is focusable; its ring must not be clipped by the panel's own box. */
  .omx-tabs__panel { min-inline-size: 0; }
  .omx-tabs__panel:focus-visible { outline-offset: var(--omx-space-1); }

  @media (forced-colors: active) {
    .omx-tabs__tab[aria-selected="true"],
    .omx-tabs__tab[aria-current="page"] { border-block-end-color: Highlight; }
  }

  @media print {
    /* Printing one panel of eleven prints a fact and hides ten. */
    .omx-tabs__list { display: none; }
    .omx-tabs__panel[hidden] { display: block !important; }
    .omx-tabs__panel { break-inside: avoid; }
  }
}

/* --- Navigation/Topbar/Topbar.css ---------------------------------- */
/* =============================================================================
 * L7 · Navigation · Topbar — WHERE · WHAT · WHO.
 * ========================================================================== */

@layer omx.component {

  /* The record's identity, under the page title. Quiet — it is context, not
   * the heading, and the real <h1> is in the page itself. */
  .omx-topbar__subtitle {
    color: var(--omx-color-text-muted);
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Sign out is a <button> inside a <form>, because it is a POST — see the view.
   * It has to be indistinguishable from the links above it: a menu where one
   * item is visibly a button reads as a mistake. */

  .omx-topbar {
    display: flex;
    align-items: center;
    gap: var(--omx-space-5);
    /* 🔧 `min-block-size`, NOT `block-size`. A hard height CLIPS whatever the
     * larger text scales put inside it instead of growing; the shell's row track
     * is already `auto`, so letting the bar grow costs nothing downstream. */
    min-block-size: var(--omx-topbar-height, 52px);
    padding-inline: var(--omx-space-6);
    /* 🔧 2026-09-20 -- the CHROME plane, shared with the rail. See the token's
     * note: one tenant colour drives both, so the single-L reading survives. */
    background: var(--omx-color-chrome);
    border-block-end: var(--omx-border-width) solid var(--omx-color-chrome-rule);
    min-inline-size: 0;
  }

  /* 🔴🔴 CLASS COLLISION — AND IT REMOVED SIGN-OUT FROM EVERY DESKTOP.
   * `omx-topbar__menu` was on TWO different elements: the mobile hamburger
   * button AND the account `<details>`. This pair of rules was written for the
   * hamburger — hide on desktop, show under 768px — and it therefore hid the
   * ACCOUNT MENU on every screen wider than 768px. No Profile, no workspace
   * switch, no Appearance, no Change password, and NO WAY TO SIGN OUT. Below
   * 768px the account menu appeared and the hamburger did too.
   *
   * Measured: `.omx-topbar__user-trigger` 0x0 on 45 page-renders.
   *
   * ⚠️ Every render assertion passed throughout, because the sign-out FORM is
   * in the HTML — markup checks cannot see `display: none`. It took an audit
   * that asked "does this component actually occupy space?".
   *
   * ⇒ The hamburger is now `omx-topbar__hamburger`. Two elements, two names. */
  .omx-topbar__hamburger { display: none; }
  @media (max-width: 768px) { .omx-topbar__hamburger { display: inline-flex; } }

  /* -- WHERE ---------------------------------------------------------------- */
  /* 🔧 2026-09-21 — THE MODULE SWITCHER LEFT THIS ZONE. It sits beside the
   * MODULE title in the rail now ("Shared frontend"), which is the thing it
   * actually switches; the page title up here is not. Owner ruling, and it
   * supersedes the 2026-09-20 one that put it in this row.
   * ⚠️ This stays a flex ROW rather than reverting to a block: `__where-text`
   * keeps its own box so the title's overflow/ellipsis still has something to
   * act on — see the note on `.omx-topbar__title`. Reverting the layout would
   * be a second change nobody asked for. */
  .omx-topbar__where {
    flex: 1;
    min-inline-size: 0;
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
  }
  /* `flex: 0 1 auto`, NOT `1 1 auto`: grow 0 so the title takes only what it
   * needs, shrink 1 so a long one ellipsises rather than pushing the search off
   * the bar.
   * 🔧 The original reason was the module switcher, which used to share this
   * row and got shoved ~500px right when the text was allowed to grow. That
   * control has moved to the rail; the constraint is kept because it is still
   * the right behaviour for a title beside a search. */
  .omx-topbar__where-text { flex: 0 1 auto; min-inline-size: 0; display: block; }

  /* ⭐⭐ THE BAR JOINS THE RAIL'S REGISTER SYSTEM — owner, 2026-09-22.
   * The rail ended up with three, and which one a mark wears is the whole
   * hierarchy. MEASURED on both grounds, and carried over unchanged:
   *     brand       2px, the brand gradient       the plate seam and rule
   *     instrument  1px, --omx-brand-instrument   3.01 light / 2.54 dark
   *     structure   1px, --omx-color-border-strong   1.95 / 1.90
   * ⭐ THE PAGE TITLE TAKES THE SENIOR REGISTER, and that is the argument for
   * this rule rather than taste: "where am I" is the same CLASS of fact as
   * "which module, which company", which is what the instrument register
   * already marks in the rail. The grouping marks among controls are junior
   * and take structure, exactly as the nav's group leaders do.
   * ⚠️ It rides `__where`'s `align-items: center`, so it lands on the bar's
   * own horizon — MEASURED at cy 25.5, the line the search, the door, the bell
   * and the caret already share. The title/subtitle stack crosses it; the rule
   * starts after that stack ends, so it never touches either line.
   * ⚠️ It fades IN, like the module title's: a hairline butting against a
   * letter reads as a strikethrough that missed. */
  .omx-topbar__where-rule {
    flex: 1 1 auto;
    min-inline-size: 0;
    block-size: var(--omx-border-width);
    background: var(--omx-brand-instrument);
    opacity: 0.75;
    -webkit-mask-image: linear-gradient(to right, transparent, currentColor 24%);
    mask-image: linear-gradient(to right, transparent, currentColor 24%);
  }

  /* 🔴 THE PAGE TITLE USED TO BE 14px / WEIGHT 500 — THE SAME SIZE AS A SIDEBAR
   * LINK and one step off body text. Nothing on the chrome said which of the two
   * was the heading, so the frame had no hierarchy at all and the loudest thing
   * on a page was a status chip. Now `omx-text-title` (18 / 600).
   *
   * ⛔ IT CANNOT SIMPLY BE MADE BIGGER — MEASURED TWICE. `.omx-topbar__where`
   * shares one flex row with the search and the actions, so a larger title WRAPS
   * rather than widening: at `data-text-scale="large"` an 18px title rendered on
   * TWO LINES, 54px tall inside a 52px bar.
   * ⚠️ AND `white-space: nowrap` ALONE DID NOT FIX IT. `__where` is
   * `display: block` and this span is INLINE, so `overflow` and `text-overflow`
   * have no box to act on — an inline box cannot be clipped. `display: block`
   * is what makes the other three declarations mean anything. */
  .omx-topbar__title {
    /* 🔴 THE PAGE TITLE WAS 14px / WEIGHT 500 — THE SAME SIZE AS A SIDEBAR LINK
     * and one step off body text, so nothing on the chrome said which of the two
     * was the heading. The frame had no hierarchy at all; the loudest thing on a
     * page was a status chip.
     *
     * ⛔⛔ AND IT COULD NOT BE FIXED WITH `class="omx-text-title"`, WHICH IS WHAT
     * I TRIED FIRST. The `.omx-text-*` classes live in `@layer omx.utility`, and
     * the layer order is reset, base, layout, COMPONENT, UTILITY, module — so a
     * utility BEATS a component rule no matter the specificity or source order.
     * ⇒ `.omx-text-title` carries `text-wrap: balance`, and every attempt to make
     * this title a single truncating line lost to it silently: the declaration
     * was in the source, in the bundle, in the served CSS and in the matched
     * rule, and did nothing. `white-space` reported `normal` and `text-wrap`
     * reported `balance`. MEASURED AT 375px: "Dash… / overvi…" on THREE lines.
     * ⭐ THE GENERAL TRAP: a component that puts an `omx-text-*` utility on its
     * OWN element can never tune that element again. Set the step directly.
     *
     * ⚠️ `text-wrap: nowrap`, not `white-space: nowrap` — `white-space` is a
     * shorthand of collapse + mode and cannot express itself once
     * `text-wrap-style` is in play. A topbar title is one line that truncates;
     * `balance` is for a multi-line heading and was never right here. */
    display: block;
    font-size: var(--omx-font-size-title);
    font-weight: var(--omx-font-weight-semibold);
    line-height: var(--omx-line-height-tight);
    color: var(--omx-color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    text-wrap: nowrap;
  }

  /* ⭐ The breadcrumb moved to its own component 2026-09-19 —
   * `Navigation/Breadcrumb/`. It was markup here with styles here, so it
   * existed only where a full Topbar did. */

  /* -- WHAT ----------------------------------------------------------------- */
  .omx-topbar__search {
    display: inline-flex;
    align-items: center;
    gap: var(--omx-space-4);
    block-size: var(--omx-control-height);
    padding-inline: var(--omx-space-5);
    inline-size: min(320px, 34vw);

    font-size: var(--omx-font-size-body);
    color: var(--omx-color-text-subtle);
    background: var(--omx-color-surface-sunken);
    /* 🔴 WAS `--omx-color-border`, WHICH MEASURES 1.332:1 AGAINST THE BAR.
     * WCAG 1.4.11 needs 3.0 for a component boundary, so the edge of the only
     * control in the topbar sat well under the floor and the field read as a
     * smudge rather than as something you can click.
     * ⭐ `--omx-color-border-control` is 3.5:1 and its own comment in tokens.css
     * says why it exists: "an input must read as an input". The right token was
     * already there; this control had simply reached for the generic one.
     * ⚠️ CT COULD NEVER HAVE CAUGHT THIS. It proves a token is contrasty enough,
     * never that a control wears the RIGHT token. That gap is still open, and
     * this is the first confirmed defect it let through.
     * ⛔ The other three `--omx-color-border` uses in this file are deliberate:
     * `__kbd` is a hint chip, and the dropdown and Appearance panels are raised
     * SURFACES carrying their own shadow, not control boundaries. */
    border: var(--omx-border-width) solid var(--omx-color-border-control);
    border-radius: var(--omx-radius-md);
    cursor: pointer;

    transition:
      border-color var(--omx-duration-fast) var(--omx-easing-standard),
      background-color var(--omx-duration-fast) var(--omx-easing-standard);
  }
  .omx-topbar__search:hover {
    border-color: var(--omx-color-border-strong);
    background: var(--omx-color-surface);
  }
  .omx-topbar__search:active { border-color: var(--omx-color-border-control); background: var(--omx-color-control-active); }
  .omx-topbar__search-icon { inline-size: 14px; block-size: 14px; flex: 0 0 auto; }
  .omx-topbar__search-label { flex: 1; text-align: start; }

  /* 🆕 The shortcut is VISIBLE. The old shell shipped a command palette and
   * rendered no hint that it existed — measured, zero <kbd> elements in the
   * whole layout. */
  .omx-topbar__kbd {
    flex: 0 0 auto;
    padding: 1px var(--omx-space-3);
    font-family: var(--omx-font-mono);
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-sm);
  }

  @container omx-content (max-width: 860px) {
    .omx-topbar__search-label, .omx-topbar__kbd { display: none; }
    .omx-topbar__search { inline-size: var(--omx-control-height); padding-inline: 0; justify-content: center; }
  }

  /* -- THE DOOR OUT OF THIS MODULE -----------------------------------------
   * ⭐⭐ Owner ruling 2026-09-22: it sits with the SEARCH, not in the rail's
   * identity plate and not beside the page title. Same glyph, same door.
   * ⚠️ `flex: 0 0 auto` HAS TO BE SAID HERE. The `__actions > *` rule that
   * protects the other icon controls from the shrink chain does not reach
   * this one — it is a child of the BAR, not of `__actions`. Without it the
   * row squashes the glyph exactly the way it once ovalised a 16px button to
   * 9.2px wide while leaving it 16 tall.
   * ⭐ It matches the search's height rather than the icon buttons' — the
   * two are one cluster, and a 32px control beside a 32px field is what says
   * so. 32 also clears WCAG 2.5.8's 24px floor with room. */
  .omx-topbar__switch {
    flex: 0 0 auto;
    display: inline-grid;
    place-items: center;
    inline-size: var(--omx-control-height);
    block-size: var(--omx-control-height);
    border-radius: var(--omx-radius-md);
    color: var(--omx-color-text-subtle);
    text-decoration: none;
    /* The leaves read this; see the note in Topbar.cshtml. */
    --omx-module-spread: 0px;
    transition: background-color var(--omx-duration-fast) var(--omx-easing-standard),
                color var(--omx-duration-fast) var(--omx-easing-standard);
  }

  /* ⭐ THE TIGHTENING IS CONDITIONAL ON THE SEARCH BEING THERE, which is why
   * it is an adjacent-sibling rule and not a margin on the control. The bar's
   * own gap is 12px and `__actions` groups its buttons at 6px; pulling back to
   * 6 here puts the door in the SEARCH's cluster rather than halfway between
   * the search and the account controls, which is what the owner asked for.
   * ⛔ With `ShowSearch = false` there is no field to belong to and the rule
   * simply does not match — a flat margin would have pulled it against the
   * page title instead. */
  .omx-topbar__search + .omx-topbar__switch {
    margin-inline-start: calc(var(--omx-space-3) - var(--omx-space-5));
  }

  .omx-topbar__switch-icon {
    inline-size: var(--omx-size-icon-action);
    block-size: var(--omx-size-icon-action);
  }

  /* ⭐ THE DIVIDER — `.omx-sidebar__datum::before` on the other chrome surface,
   * same 22px, same 1px, same reason: it says the bar is ONE panel with two
   * registers rather than a row of unrelated glyphs. ⚠️ Short and centred, not
   * full height — a full-height rule would cut the bar into two boxes, which
   * is the opposite of the intent. The rail's note says the same thing.
   * ⚠️ `flex: 0 0 auto` for the reason the door needs it: this is a child of
   * the BAR, not of `__actions`, so the rule that protects the icon controls
   * from the shrink chain does not reach it. */
  .omx-topbar__divider {
    flex: 0 0 auto;
    align-self: center;
    inline-size: var(--omx-border-width);
    block-size: 22px;
    background: var(--omx-color-border-strong);
  }

  /* ⛔ The divider goes when the bar is too narrow to have clusters to divide.
   * Below 860px the search is already a bare icon; a separator between two
   * glyph buttons is noise, and the bar needs the 25px back. */
  @container omx-content (max-width: 860px) {
    .omx-topbar__divider { display: none; }
  }

  .omx-topbar__switch:hover,
  .omx-topbar__switch:focus-visible {
    /* ⚠️ 1.1px, not the door's 2.4. MEASURED: the glyph renders at 18px on a
     * 24 viewBox, so 1px of CSS is 1.33 user units — 2.4px would move each
     * tile 3.2 units when the gap between them is only 4, which reads as the
     * icon falling apart rather than as four things separating. */
    --omx-module-spread: 1.1px;
    color: var(--omx-color-accent);
    background: var(--omx-color-accent-subtle);
  }

  /* ⭐ The press CLOSES the doors again — the door is being used, and a
   * control that keeps animating under the finger reads as unresponsive. */
  .omx-topbar__switch:active {
    --omx-module-spread: 0.35px;
    background: var(--omx-color-control-active);
  }

  /* -- WHO ------------------------------------------------------------------ */
  /* 🔴 FIXED 2026-09-19 — THE TOPBAR SCROLLED SIDEWAYS 223px AT 768px.
   * Found the day Sidebar and Topbar finally got a gallery sample: with a long
   * tenant name at tablet width the whole page gained a horizontal scrollbar.
   *
   * ⚠️ THE TRUNCATION WAS ALREADY CORRECT AND STILL DID NOTHING.
   * `__user-name` and `__user-role` (then `__user-tenant`) already carry
   * `overflow:hidden; text-overflow:ellipsis; white-space:nowrap`, and
   * `__user-text` already carries `min-inline-size: 0`. The ellipsis never fired
   * because NO ANCESTOR WOULD GIVE THE LEAF A CONSTRAINED BOX:
   *
   *   __actions        flex: 0 0 auto   -> refuses to shrink at ALL
   *   __user           (no rule)        -> min-width: auto
   *   __menu           (no rule)        -> min-width: auto
   *   __user-trigger   (no rule)        -> min-width: auto
   *   __user-text      min-inline-size: 0  ✅ the only correct link in the chain
   *
   * ⭐ A flex item's default `min-width: auto` means "never smaller than my
   * content", and nowrap content has no smaller size — so the item grows and
   * the row overflows. One `min-inline-size: 0` anywhere below the break does
   * nothing; the WHOLE chain has to yield.
   *
   * ⛔ The icon buttons are safe: search, notifications, switcher and caret each
   * carry their own `flex: 0 0 auto`, so letting the CONTAINER shrink squashes
   * the long text and nothing else.
   *
   * ⚠️ `@container omx-content (max-width: 600px)` hides the user text entirely —
   * which is why this only shows between 600 and ~900px, a band no one tested. */
  .omx-topbar__actions {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    flex: 0 1 auto;
    min-inline-size: 0;
  }

  /* 🔴 RESTORED. The dead-rule sweep that followed the menu migration removed
   * this whole rule because ONE of its three grouped selectors
   * (`.omx-topbar__menu`, the old <details>) had died with the migration — and
   * a grouped selector is not dead until EVERY selector in it is. The other two
   * are live and carry the truncation chain below.
   * ⚠️ That is the shape to watch when deleting CSS by class name. */
  .omx-topbar__user,
  .omx-topbar__user-trigger { min-inline-size: 0; }

  /* ⛔ AND ONLY THE USER TEXT MAY ABSORB IT. Letting `__actions` shrink without
   * this squashed a 16px icon button to 9.2px wide at 640px — still 16 TALL, so
   * the glyph was quietly ovalised rather than obviously broken.
   * ⭐ The row's give has to land somewhere specific: the buttons are fixed and
   * the long name truncates. A container that shrinks without saying WHICH child
   * yields just moves the damage to whichever child is least protected. */
  .omx-topbar__actions > .omx-button,
  .omx-topbar__actions > form,
  .omx-topbar__actions > .omx-topbar__search { flex: 0 0 auto; }

  .omx-topbar__bell { position: relative; }
  .omx-topbar__count {
    position: absolute;
    inset-block-start: -2px;
    inset-inline-end: -2px;
    min-inline-size: 15px;
    padding-inline: 3px;
    font-size: 9px;
    font-weight: var(--omx-font-weight-semibold);
    line-height: 15px;
    text-align: center;
    /* the ON-colour for danger-solid is a token; #fff here was the same
     * literal-in-a-component defect C6 found in Button. */
    color: var(--omx-color-danger-on);
    background: var(--omx-color-danger-solid);
    border-radius: var(--omx-radius-pill);
  }

  @container omx-content (max-width: 720px) {
  }

  /* -- the user menu: native <details>, so Escape and click-away are free ---- */   /* min-inline-size above */

  /* 🔧 A <button> SINCE THE MENU MIGRATION, NOT A <summary>. `list-style: none`
   * went with the disclosure; the four resets below replaced it, because a
   * <button> that inherits the UA's chrome renders as a grey pill around the
   * avatar. ⚠️ `font: inherit` is the one people forget: without it the user's
   * name silently drops to the browser's 13.3px button font. */
  .omx-topbar__user-trigger {
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    padding: var(--omx-space-2) var(--omx-space-3);
    border: 0;
    border-radius: var(--omx-radius-md);
    background: none;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
  }
  .omx-topbar__user-trigger::-webkit-details-marker { display: none; }
  .omx-topbar__user-trigger:hover { background: var(--omx-color-control-hover); }
  .omx-topbar__user-trigger:active { background: var(--omx-color-control-active); }

  .omx-topbar__user-text { display: grid; text-align: start; min-inline-size: 0; line-height: 1.25; }
  .omx-topbar__user-name {
    font-size: var(--omx-font-size-body);
    font-weight: var(--omx-font-weight-medium);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* 🔧 `__user-tenant` -> `__user-role` (owner, 2026-09-22). The element no
   * longer holds a tenant, and a class named for what it used to carry is the
   * exact drift this package keeps getting bitten by — `omx-topbar__menu` on
   * two different elements is what removed sign-out from every desktop. */
  .omx-topbar__user-role { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .omx-topbar__user-caret { inline-size: 12px; block-size: 12px; flex: 0 0 auto; color: var(--omx-color-text-subtle); }

  @container omx-content (max-width: 600px) {
    .omx-topbar__user-text { display: none; }
  }

  /* -- APPEARANCE PANEL (Stage 4.1) -----------------------------------------
   * ⛔ `position: fixed`, NOT absolute. A [popover] is promoted to the TOP LAYER,
   * where its containing block is the VIEWPORT — an `absolute` offset would
   * resolve against the page, not against the trigger, and the panel would land
   * somewhere unrelated. Fixed coordinates under the bar are deterministic and
   * need no anchor positioning.
   * ⭐ `shadow-xl` because it is a popover: Stage 1.5's rule is that an overlay
   * never casts lighter than what it covers. */
  .omx-appearance-pop {
    position: fixed;
    inset-block-start: calc(var(--omx-topbar-height, 52px) + var(--omx-space-3));
    inset-inline-end: var(--omx-space-6);
    inset-inline-start: auto;
    inline-size: min(22rem, calc(100vw - var(--omx-space-9)));
    max-block-size: min(32rem, calc(100dvh - var(--omx-topbar-height, 52px) - var(--omx-space-9)));
    overflow: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: var(--omx-space-6);
    background: var(--omx-color-surface-raised);
    color: var(--omx-color-text);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    box-shadow: var(--omx-shadow-xl);
  }
  .omx-appearance-pop::backdrop { background: transparent; }

  /* ⭐ ICON THEN LABEL on every row (owner ruling 2026-09-20). `display: flex`
   * replaces `block` — the rows were plain text and a menu of bare words makes
   * a person read all of them to find one. */
  .omx-topbar__menu-item--danger { color: var(--omx-color-danger-text); }

  /* The group heading inside the account menu. Same job as the sidebar's
   * section label: an index, not a heading. */

  /* ⚠️ One step back from the label it sits beside — a glyph that repeats the
   * word should not shout it. `aria-hidden` in the markup for the same reason.
   * Non-text, so the floor is 3.0 and `text-muted` clears it with room. */

  /* A settled fact rather than a control — today's only language. It keeps the
   * item's shape so the list does not jump when a second one arrives. */
  /* omx-no-press: aria-disabled. There is nothing to press, and a press state would say there was */

  /* ⭐ THE BAR EARNS ITS EDGE ONLY ONCE SOMETHING IS UNDER IT.
   * At rest the topbar is a hairline on white — quiet, which is right. The
   * moment the content scrolls, a shadow says the page continues beneath the
   * bar rather than starting at it. `Shell.js` flips the attribute once per
   * crossing, not per frame.
   * ⚠️ `--omx-shadow-sm` deliberately, not `md`: the topbar is the LOWEST
   * elevated thing on the page and must stay under every popover, drawer and
   * modal that can open in front of it. */
  .omx-shell[data-omx-scrolled="true"] .omx-topbar {
    box-shadow: var(--omx-shadow-sm);
    transition: box-shadow var(--omx-duration-fast) var(--omx-easing-standard);
  }

  @media print { .omx-topbar { display: none; } }
}

/* --- Navigation/WorkspacePicker/WorkspacePicker.css ---------------- */
/* =============================================================================
 * L7 · Navigation · WorkspacePicker.
 * ========================================================================== */

@layer omx.component {

  .omx-workspaces { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }
  .omx-workspaces__caption { margin: 0; }

  .omx-workspaces__warning {
    margin: 0;
    padding: var(--omx-space-3) var(--omx-space-4);
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-warning-text);
    background: var(--omx-color-warning-bg);
    color: var(--omx-color-warning-text);
    border-radius: var(--omx-radius-xs);
  }

  .omx-workspaces__form { display: grid; gap: var(--omx-space-4); }
  .omx-workspaces__list { display: grid; gap: var(--omx-space-2); list-style: none; padding: 0; margin: 0; }
  .omx-workspaces__item { display: flex; min-inline-size: 0; }

  .omx-workspaces__row {
    appearance: none;
    display: flex;
    align-items: center;
    gap: var(--omx-space-4);
    inline-size: 100%;
    padding: var(--omx-space-4);
    text-align: start;
    font: inherit;
    color: var(--omx-color-text);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-sm);
    min-block-size: var(--omx-control-height);
  }

  button.omx-workspaces__row { cursor: pointer; }
  button.omx-workspaces__row:hover { background: var(--omx-color-control-hover); border-color: var(--omx-color-border-strong); }
  button.omx-workspaces__row:active { background: var(--omx-color-control-active); border-color: var(--omx-color-border-control); }

  /* Where you already are: marked in WORDS ("You are here") as well as by the
   * accent rail, and not a control at all. */
  .omx-workspaces__row--current {
    background: var(--omx-color-accent-subtle);
    border-color: var(--omx-color-accent);
    cursor: default;
  }
  .omx-workspaces__current { margin-inline-start: auto; color: var(--omx-color-accent-hover); font-weight: var(--omx-font-weight-medium); }

  .omx-workspaces__text { display: grid; min-inline-size: 0; }
  .omx-workspaces__name { font-weight: var(--omx-font-weight-medium); }
  .omx-workspaces__hint { color: var(--omx-color-text-muted); }

  .omx-workspaces__go { inline-size: 14px; block-size: 14px; flex: none; margin-inline-start: auto; color: var(--omx-color-text-muted); }

  @media print { .omx-workspaces__form { display: none; } }
}

/* --- Collaboration/Annotation/Annotation.css ----------------------- */
@layer omx.component {

  /* L8 · Collaboration · Annotation — another system's verdict, beside ours.
   *
   * ⛔ VISUALLY SECONDARY, AND NEVER A BADGE. Muted, caption-sized, set off by a
   * rule on its leading edge: enrichment, not authority (PROD F05 P1). No tone
   * and no icon — a green "APPROVED" pill beside a shade this dye house has not
   * approved would say exactly what KNITDYE forbids. The conflict banner is the
   * package's own Banner, and is the only loud thing here. */

  .omx-annot {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-annot__head,
  .omx-annot__undecided,
  .omx-annot__uncompared {
    margin: 0;
    font-size: var(--omx-font-size-caption);
  }

  .omx-annot__head { color: var(--omx-color-text-muted); }

  /* The one sentence that must not be missed when nothing is recorded here. */
  .omx-annot__undecided { font-weight: var(--omx-font-weight-medium); }

  .omx-annot__list {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .omx-annot__item {
    padding-inline-start: var(--omx-space-3);
    border-inline-start: var(--omx-border-width) solid var(--omx-color-border-strong);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-annot__fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-1) var(--omx-space-5);
    margin: 0;
  }

  .omx-annot__field {
    display: flex;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-annot__field dd {
    margin: 0;
    overflow-wrap: anywhere;
  }

  .omx-annot__uncompared { margin-block-start: var(--omx-space-1); }
}

/* --- Collaboration/Approvals/Approvals.css ------------------------- */
/* =============================================================================
 * L8 · Collaboration · Approvals.
 * ========================================================================== */

@layer omx.component {

  .omx-approvals { display: grid; gap: var(--omx-space-6); min-inline-size: 0; }
  .omx-approvals__caption { color: var(--omx-color-text-muted); margin: 0; }

  .omx-approvals__steps { display: grid; gap: 0; list-style: none; padding: 0; margin: 0; }

  .omx-approvals__step {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--omx-space-4);
    padding-block-end: var(--omx-space-6);
    position: relative;
    min-inline-size: 0;
  }

  /* The rail joining the markers. It stops at the last step rather than
   * trailing into nothing. */
  .omx-approvals__step:not(:last-child)::before {
    content: "";
    position: absolute;
    inset-block: 24px var(--omx-space-1);
    inset-inline-start: 11px;
    inline-size: var(--omx-border-width-strong);
    background: var(--omx-color-border);
  }

  .omx-approvals__marker {
    inline-size: 24px;
    block-size: 24px;
    display: grid;
    place-items: center;
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-surface-sunken);
    color: var(--omx-color-text-muted);
    border: var(--omx-border-width) solid var(--omx-color-border);
    z-index: var(--omx-z-raised);
  }
  .omx-approvals__marker svg { inline-size: 14px; block-size: 14px; }

  .omx-approvals__step[data-omx-approval-state="success"] .omx-approvals__marker {
    background: var(--omx-color-success-bg); color: var(--omx-color-success-text);
    border-color: var(--omx-color-success-text);
  }
  .omx-approvals__step[data-omx-approval-state="danger"] .omx-approvals__marker {
    background: var(--omx-color-danger-bg); color: var(--omx-color-danger-text);
    border-color: var(--omx-color-danger-text);
  }
  .omx-approvals__step[data-omx-approval-state="warning"] .omx-approvals__marker {
    background: var(--omx-color-warning-bg); color: var(--omx-color-warning-text);
    border-color: var(--omx-color-warning-text);
  }
  .omx-approvals__step[data-omx-approval-state="info"] .omx-approvals__marker {
    background: var(--omx-color-info-bg); color: var(--omx-color-info-text);
    border-color: var(--omx-color-info-text);
  }

  .omx-approvals__body { display: grid; gap: var(--omx-space-2); min-inline-size: 0; }

  .omx-approvals__title {
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--omx-space-2) var(--omx-space-4);
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-approvals__state {
    font-size: var(--omx-font-size-caption);
    font-weight: var(--omx-font-weight-regular);
    color: var(--omx-color-text-muted);
  }
  .omx-approvals__step[data-omx-approval-state="success"] .omx-approvals__state { color: var(--omx-color-success-text); }
  .omx-approvals__step[data-omx-approval-state="danger"]  .omx-approvals__state { color: var(--omx-color-danger-text); }
  .omx-approvals__step[data-omx-approval-state="warning"] .omx-approvals__state { color: var(--omx-color-warning-text); }
  .omx-approvals__step[data-omx-approval-state="info"]    .omx-approvals__state { color: var(--omx-color-info-text); }

  /* A step that was not required is quiet but PRESENT — hiding it would erase
   * the fact that the route had one. */
  .omx-approvals__step[data-omx-approval-state="muted"] .omx-approvals__title,
  .omx-approvals__step[data-omx-approval-state="waiting"] .omx-approvals__title {
    color: var(--omx-color-text-muted);
    font-weight: var(--omx-font-weight-regular);
  }

  .omx-approvals__who {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-muted);
    margin: 0;
    flex-wrap: wrap;
  }
  .omx-approvals__sep { color: var(--omx-color-text-subtle); }

  .omx-approvals__comment {
    margin: 0;
    padding: var(--omx-space-3) var(--omx-space-4);
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-border);
    background: var(--omx-color-surface-sunken);
    border-radius: var(--omx-radius-xs);
    color: var(--omx-color-text);
  }

  .omx-approvals__decide {
    display: grid;
    gap: var(--omx-space-5);
    padding-block-start: var(--omx-space-5);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  @media print {
    /* The route is the evidence; the buttons are not. */
    .omx-approvals__decide { display: none; }
    .omx-approvals__step { break-inside: avoid; }
  }
}

/* --- Collaboration/Attachments/Attachments.css --------------------- */
/* =============================================================================
 * L8 · Collaboration · Attachments.
 * ========================================================================== */

@layer omx.component {

  .omx-attachments { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-attachments__caption {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-3);
    color: var(--omx-color-text-muted);
    margin: 0;
  }
  .omx-attachments__count {
    font-variant-numeric: tabular-nums;
    padding: 0 var(--omx-space-2);
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-neutral-bg);
    color: var(--omx-color-neutral-text);
    font-size: var(--omx-font-size-caption);
  }

  .omx-attachments__list { display: grid; gap: var(--omx-space-1); list-style: none; padding: 0; margin: 0; }

  .omx-attachments__item {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--omx-space-4);
    padding: var(--omx-space-3);
    border-radius: var(--omx-radius-sm);
    min-inline-size: 0;
  }
  .omx-attachments__item:hover { background: var(--omx-color-control-hover); }
  /* ⚠️ `a.` IS LOAD-BEARING — the same class is a <span> when there is no Href,
   * and a press state on an unlinked filename is a lie. */
  .omx-attachments__item:has(a.omx-attachments__name:active) { background: var(--omx-color-control-active); }

  .omx-attachments__icon { inline-size: 18px; block-size: 18px; color: var(--omx-color-text-muted); flex: none; }

  .omx-attachments__text { display: grid; min-inline-size: 0; }

  .omx-attachments__name {
    font-weight: var(--omx-font-weight-medium);
    /* 🔴 Ellipsis at the END, never in the middle, and never replaced by an
     * icon: the file NAME is the only thing telling a person what the file is,
     * and "Invoice-4471-…-final.pdf" hides the part that distinguishes it from
     * the other seven invoices. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .omx-attachments__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-muted);
  }
  .omx-attachments__sep { color: var(--omx-color-text-subtle); }

  .omx-attachments__remove { display: flex; }

  .omx-attachments__upload { display: grid; gap: var(--omx-space-4); justify-items: start; }

  .omx-attachments__drop {
    display: grid;
    justify-items: center;
    gap: var(--omx-space-3);
    inline-size: 100%;
    padding: var(--omx-space-7);
    border: var(--omx-border-width-strong) dashed var(--omx-color-border);
    border-radius: var(--omx-radius-md);
    background: var(--omx-color-surface-sunken);
    text-align: center;
  }

  .omx-attachments__drop[data-omx-dropzone="1"] {
    border-color: var(--omx-color-accent);
    background: var(--omx-color-accent-subtle);
  }

  .omx-attachments__pick { display: inline-flex; cursor: pointer; }

  /* 🔴 The input is not `display: none`. A hidden file input is a control a
   * screen reader cannot reach and a keyboard cannot focus. It is transparent
   * and stretched over the button instead, so it keeps its focus ring — which
   * the :focus-within below puts back on the visible button. */
  .omx-attachments__file {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    opacity: 0;
  }
  .omx-attachments__pick:focus-within .omx-button {
    outline: var(--omx-border-width-strong) solid var(--omx-color-accent);
    outline-offset: 2px;
  }

  .omx-attachments__drop-hint { color: var(--omx-color-text-muted); margin: 0; }

  @media print {
    .omx-attachments__upload, .omx-attachments__remove { display: none; }
    .omx-attachments__item { break-inside: avoid; }
    /* On paper an ellipsis hides the only identifying thing about the file. */
    .omx-attachments__name { overflow: visible; white-space: normal; }
  }
}

/* --- Collaboration/CapabilityGate/CapabilityGate.css --------------- */
/* =============================================================================
 * L8 · Collaboration · CapabilityGate — the Explain fallback, and only that.
 *
 * When the capability IS present the gate emits no element at all, so there is
 * nothing to style: guarding a block must not change how the block looks.
 * ========================================================================== */

@layer omx.component {

  .omx-gate {
    margin: 0;
    padding: var(--omx-space-4) var(--omx-space-5);
    border: var(--omx-border-width) dashed var(--omx-color-border);
    border-radius: var(--omx-radius-sm);
    background: var(--omx-color-surface-sunken);
    color: var(--omx-color-text-muted);
  }

  @media print {
    /* A licensing notice on a printed document is noise; the absence of the
     * section is already the fact. */
    .omx-gate { display: none; }
  }
}

/* --- Collaboration/Comments/Comments.css --------------------------- */
/* =============================================================================
 * L8 · Collaboration · Comments.
 * ========================================================================== */

@layer omx.component {

  .omx-comments { display: grid; gap: var(--omx-space-6); min-inline-size: 0; }

  .omx-comments__caption {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-3);
    color: var(--omx-color-text-muted);
    margin: 0;
  }
  .omx-comments__count {
    font-variant-numeric: tabular-nums;
    padding: 0 var(--omx-space-2);
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-neutral-bg);
    color: var(--omx-color-neutral-text);
    font-size: var(--omx-font-size-caption);
  }

  .omx-comments__list { display: grid; gap: var(--omx-space-6); list-style: none; padding: 0; margin: 0; }

  .omx-comments__item {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--omx-space-4);
    min-inline-size: 0;
  }

  .omx-comments__body { display: grid; gap: var(--omx-space-2); min-inline-size: 0; }

  .omx-comments__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-muted);
    margin: 0;
  }
  .omx-comments__author { font-weight: var(--omx-font-weight-semibold); color: var(--omx-color-text); }
  .omx-comments__you, .omx-comments__role { color: var(--omx-color-text-subtle); }
  .omx-comments__sep { color: var(--omx-color-text-subtle); }

  .omx-comments__internal {
    display: flex;
    align-items: center;
    gap: var(--omx-space-2);
    color: var(--omx-color-warning-text);
    margin: 0;
    font-weight: var(--omx-font-weight-medium);
  }
  .omx-comments__internal svg { inline-size: 12px; block-size: 12px; flex: none; }

  .omx-comments__text {
    margin: 0;
    /* Newlines become structure. The body is plain text by design, so this is
     * the only paragraph mechanism it gets — and it needs no parser. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;   /* a pasted URL must not widen the record panel */
  }

  /* The tint is a SECOND signal behind the sentence above it, never the only
   * one — and it is a left rail, which survives forced-colors. */
  .omx-comments__item--internal .omx-comments__body {
    border-inline-start: var(--omx-border-width-strong) solid var(--omx-color-warning-text);
    padding-inline-start: var(--omx-space-4);
  }

  .omx-comments__composer {
    display: grid;
    gap: var(--omx-space-4);
    padding-block-start: var(--omx-space-5);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
  }

  .omx-comments__actions {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--omx-space-5);
    flex-wrap: wrap;
  }
  .omx-comments__post { margin-inline-start: auto; }

  @media print {
    .omx-comments__composer { display: none; }
    .omx-comments__item { break-inside: avoid; }
  }
}

/* --- Collaboration/CommitGroup/CommitGroup.css --------------------- */
/* ==========================================================================
 * L8 · Collaboration · CommitGroup — these go together, or none of them go.
 *
 * ⭐ THE GROUP IS A BOUNDED THING, and it is drawn as one. A list of lines with
 * a button under it reads as a list with a button; a panel with an edge reads
 * as a unit, which is precisely the claim the component is making.
 *
 * ⛔ A BLOCKED MEMBER IS NOT PAINTED RED. It is not an error — it is a line
 * that cannot go yet, and the sentence beside it says why. Red would train
 * people to dismiss the one thing standing between them and a wrong posting.
 * ========================================================================== */

@layer omx.component {

  .omx-commit-group {
    display: grid;
    gap: var(--omx-space-4);
    padding: var(--omx-space-5);
    border: var(--omx-border-width) solid var(--omx-color-border-strong);
    border-radius: var(--omx-radius-md);
    background: var(--omx-color-surface-raised);
  }

  .omx-commit-group__head {
    display: grid;
    gap: var(--omx-space-1);
  }

  .omx-commit-group__title {
    margin: 0;
  }

  .omx-commit-group__rationale {
    margin: 0;
    color: var(--omx-color-text-muted);
  }

  .omx-commit-group__list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: var(--omx-space-2);
  }

  /* ⛔ NO CHECKBOX COLUMN, and the layout is built so one has nowhere to go.
     A member that can be deselected is a group that can be committed in part,
     which is the defect this component exists to make inexpressible. */
  .omx-commit-group__member {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--omx-space-1) var(--omx-space-3);
    padding-block: var(--omx-space-2);
    border-block-end: var(--omx-border-width) solid var(--omx-color-border);
  }

  .omx-commit-group__member:last-child {
    border-block-end: 0;
  }

  .omx-commit-group__label {
    color: var(--omx-color-text);
  }

  .omx-commit-group__detail {
    color: var(--omx-color-text-muted);
    font-variant-numeric: tabular-nums;
  }

  /* Already committed. ⚠️ Stated, so a re-run does not look like it will do
     more than it will — and quiet, because it is not what the reader is here
     to decide. */
  .omx-commit-group__done {
    margin-inline-start: auto;
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-subtle);
  }

  .omx-commit-group__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--omx-space-3);
  }

  .omx-commit-group__count {
    color: var(--omx-color-text-muted);
  }

  .omx-commit-group__refusal {
    margin: 0;
    display: grid;
    gap: var(--omx-space-1);
  }

  /* ⭐ The named members. The half that turns a refusal into a route. */
  .omx-commit-group__why {
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
  }

  @media (forced-colors: active) {
    .omx-commit-group { border-color: CanvasText; }
    .omx-commit-group__member { border-block-end-color: CanvasText; }
  }

}

/* --- Collaboration/Notifications/Notifications.css ----------------- */
/* =============================================================================
 * L8 · Collaboration · Notifications.
 * ========================================================================== */

@layer omx.component {

  .omx-notifications { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-notifications__head {
    display: flex;
    align-items: baseline;
    gap: var(--omx-space-4);
    flex-wrap: wrap;
  }
  .omx-notifications__title { margin: 0; }
  .omx-notifications__unread { color: var(--omx-color-text-muted); margin: 0; font-variant-numeric: tabular-nums; }
  .omx-notifications__markall { margin-inline-start: auto; }

  .omx-notifications__group { display: grid; gap: var(--omx-space-2); }

  .omx-notifications__group-label {
    margin: 0;
    color: var(--omx-color-text-muted);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-notifications__list { display: grid; gap: var(--omx-space-1); list-style: none; padding: 0; margin: 0; }

  .omx-notifications__item {
    display: grid;
    grid-template-columns: 20px 1fr;
    gap: var(--omx-space-4);
    padding: var(--omx-space-3) var(--omx-space-3);
    border-radius: var(--omx-radius-sm);
    min-inline-size: 0;
  }
  .omx-notifications__item:hover { background: var(--omx-color-control-hover); }
  .omx-notifications__item:has(.omx-notifications__link:active) { background: var(--omx-color-control-active); }

  .omx-notifications__marker { display: grid; place-items: center; padding-block-start: 2px; }
  .omx-notifications__icon { inline-size: 16px; block-size: 16px; color: var(--omx-color-text-muted); }

  .omx-notifications__dot {
    inline-size: 8px;
    block-size: 8px;
    border-radius: var(--omx-radius-pill);
    background: var(--omx-color-accent);
  }

  .omx-notifications__body { display: grid; gap: var(--omx-space-1); min-inline-size: 0; }
  .omx-notifications__line { min-inline-size: 0; }
  .omx-notifications__link { color: inherit; text-decoration: none; }
  .omx-notifications__link:hover { text-decoration: underline; }

  /* Unread is heavier AND marked AND named. Three carriers, because each one
   * alone fails somebody. */
  .omx-notifications__item[aria-current="true"] .omx-notifications__link {
    font-weight: var(--omx-font-weight-semibold);
    color: var(--omx-color-text);
  }
  .omx-notifications__item:not([aria-current="true"]) .omx-notifications__link {
    color: var(--omx-color-text-muted);
  }

  /* ⚠️ SCOPED TO THE ITEM ON PURPOSE. `.omx-notifications__link:active` alone
   * lost to the two `[aria-current]` descendant rules above — same specificity
   * count, and they come later. A press state that is out-specified renders
   * nothing and looks exactly like a press state nobody wrote. */
  .omx-notifications__item .omx-notifications__link:active { color: var(--omx-color-accent-active); }

  .omx-notifications__detail {
    color: var(--omx-color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }

  .omx-notifications__meta {
    display: flex;
    gap: var(--omx-space-2);
    color: var(--omx-color-text-subtle);
    flex-wrap: wrap;
  }
  .omx-notifications__sep { color: var(--omx-color-text-subtle); }

  .omx-notifications__seeall { margin: 0; padding-block-start: var(--omx-space-3); }

  @media (forced-colors: active) {
    /* The dot is a background, and backgrounds are dropped in forced-colors —
     * a border is what survives. */
    .omx-notifications__dot { border: 4px solid Highlight; background: Highlight; }
  }

  @media print {
    .omx-notifications__markall { display: none; }
    .omx-notifications__item { break-inside: avoid; }
  }
}

/* --- Collaboration/Permissions/Permissions.css --------------------- */
/* =============================================================================
 * L8 · Collaboration · Permissions — a crosstab whose cells are controls.
 * ========================================================================== */

@layer omx.component {

  .omx-permissions { display: grid; gap: var(--omx-space-5); min-inline-size: 0; }

  .omx-permissions__scroll {
    max-inline-size: 100%;
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
  }

  /* The capability column is frozen. Scrolling sideways past the name of the
   * permission you are granting is how the wrong box gets ticked. */
  .omx-permissions__cap,
  .omx-permissions__corner {
    position: sticky;
    inset-inline-start: 0;
    z-index: var(--omx-z-raised);
    background: var(--omx-color-surface);
    text-align: start;
    font-weight: var(--omx-font-weight-regular);
    min-inline-size: 220px;
  }
  .omx-permissions__corner { z-index: calc(var(--omx-z-raised) + 2); }
  .omx-table--sticky .omx-table__head .omx-permissions__corner { inset-block-start: 0; }

  .omx-permissions__cap { display: table-cell; }
  .omx-permissions__cap-label { display: block; }
  .omx-permissions__cap-hint,
  .omx-permissions__role-hint { display: block; color: var(--omx-color-text-muted); font-weight: var(--omx-font-weight-regular); }

  .omx-permissions__role { text-align: center; vertical-align: bottom; min-inline-size: 96px; }
  .omx-permissions__role-label { display: block; }

  .omx-permissions__group-cell {
    background: var(--omx-color-surface-sunken);
    font-weight: var(--omx-font-weight-semibold);
    text-align: start;
    position: sticky;
    inset-inline-start: 0;
  }

  .omx-permissions__cell { text-align: center; }
  .omx-permissions__box { cursor: pointer; }
  .omx-permissions__box:disabled { cursor: not-allowed; }

  /* Inherited reads as granted-but-not-here: the box is ticked and dimmed, and
   * the word beside it says why. The word is the part that works for everyone. */
  .omx-permissions__cell[data-omx-permission="inherited"] { color: var(--omx-color-text-muted); }
  .omx-permissions__inherited { display: block; color: var(--omx-color-text-subtle); }

  .omx-permissions__cell[data-omx-permission="notapplicable"] { color: var(--omx-color-text-subtle); }

  @media print {
    .omx-permissions__scroll { overflow: visible; border-color: #000; }
    .omx-permissions__cap, .omx-permissions__corner { position: static; }
  }
}

/* --- Collaboration/Routing/Routing.css ----------------------------- */
@layer omx.component {

  /* L8 · Collaboration · Routing — who is told about which exceptions, and how.
   *
   * ⭐ It IS a table and looks like one: it reuses the table's own classes. What
   * is added is the sentence each rule leaves on a row — narrowed, held back,
   * delivers nothing — in words, never in colour alone. */

  .omx-routing {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-2);
    min-inline-size: 0;
  }

  .omx-routing__caption {
    caption-side: top;
    padding-block-end: var(--omx-space-2);
    text-align: start;
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-routing__role {
    text-align: start;
    font-weight: var(--omx-font-weight-medium);
  }

  .omx-routing__note,
  .omx-routing__held {
    display: block;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-routing__nothing { font-weight: var(--omx-font-weight-medium); }

  .omx-routing__empty,
  .omx-routing__by-role {
    margin: 0;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }
}

/* --- Output/Export/Export.css -------------------------------------- */
/* =============================================================================
 * L9 · Output · Export.
 * ========================================================================== */

@layer omx.component {

  .omx-export { display: grid; gap: var(--omx-space-3); min-inline-size: 0; }

  .omx-export__label { color: var(--omx-color-text-muted); }

  .omx-export--inline {
    display: flex;
    align-items: center;
    gap: var(--omx-space-3);
    flex-wrap: wrap;
  }

  .omx-export__format { inline-size: auto; min-inline-size: 0; }

  .omx-export__scope {
    color: var(--omx-color-text-muted);
    margin: 0;
  }
  /* Inline, the sentence takes the whole second line rather than being squeezed
   * between the controls — it is a sentence, not a label. */
  .omx-export--inline .omx-export__scope { flex-basis: 100%; }

  @media print { .omx-export { display: none; } }
}

/* --- Output/Print/Print.css ---------------------------------------- */
/* =============================================================================
 * L9 · Output · Print — a business document, on screen AND on paper.
 *
 * ⚠️ The document renders on SCREEN too. A print layout that only exists inside
 * @media print cannot be looked at without printing it, so every defect in it
 * is found by a customer. Print rules below only remove the chrome and fix the
 * ink; they do not invent a second layout.
 * ========================================================================== */

@layer omx.component {

  .omx-print {
    display: grid;
    gap: var(--omx-space-7);
    /* A4 content width at 96dpi less 15mm margins. The on-screen document is
     * the same shape as the paper one, so what is seen is what is printed. */
    /* ⚠️ max-inline-size, not inline-size. `inline-size` is a TARGET a grid will
     * exceed when its content demands it; `max-inline-size` is a ceiling it
     * will not. At 320px the document rendered 373px wide and scrolled. */
    inline-size: 100%;
    max-inline-size: 750px;
    min-inline-size: 0;
    margin-inline: auto;
    padding: var(--omx-space-9);
    background: var(--omx-color-surface);
    color: var(--omx-color-text);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-md);
  }

  .omx-print__bar { display: flex; justify-content: flex-end; }

  .omx-print__watermark-note {
    margin: 0;
    padding: var(--omx-space-3) var(--omx-space-4);
    border: var(--omx-border-width-strong) solid var(--omx-color-warning-text);
    color: var(--omx-color-warning-text);
    font-weight: var(--omx-font-weight-bold);
    letter-spacing: var(--omx-letter-spacing-wide);
    text-transform: uppercase;
    text-align: center;
  }

  .omx-print__head {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--omx-space-8);
    align-items: start;
    padding-block-end: var(--omx-space-6);
    border-block-end: var(--omx-border-width-strong) solid var(--omx-color-text);
  }

  .omx-print__logo { max-block-size: 48px; max-inline-size: 180px; margin-block-end: var(--omx-space-4); display: block; }
  .omx-print__org { margin: 0; font-size: var(--omx-font-size-title); font-weight: var(--omx-font-weight-bold); }

  /* Newlines do the line breaks. The address is caller data; turning it into
   * <br> in C# would mean building markup around a string we do not control. */
  .omx-print__address, .omx-print__to-body { white-space: pre-line; margin: var(--omx-space-2) 0 0; }
  .omx-print__ids { margin: var(--omx-space-2) 0 0; color: var(--omx-color-text-muted); font-size: var(--omx-font-size-caption); }

  .omx-print__identity { text-align: end; display: grid; gap: var(--omx-space-2); justify-items: end; }
  .omx-print__title {
    margin: 0;
    font-size: var(--omx-font-size-display);
    letter-spacing: var(--omx-letter-spacing-tight);
  }
  .omx-print__number { margin: 0; font-weight: var(--omx-font-weight-semibold); font-variant-numeric: tabular-nums; }

  .omx-print__facts {
    display: grid;
    grid-template-columns: auto auto;
    gap: var(--omx-space-1) var(--omx-space-4);
    margin: var(--omx-space-3) 0 0;
  }
  .omx-print__facts dt { color: var(--omx-color-text-muted); text-align: end; }
  .omx-print__facts dd { margin: 0; font-variant-numeric: tabular-nums; }

  .omx-print__to-label {
    margin: 0 0 var(--omx-space-1);
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
  }

  .omx-print__body { display: grid; gap: var(--omx-space-6); min-inline-size: 0; }

  .omx-print__footnote { white-space: pre-line; color: var(--omx-color-text-muted); }
  .omx-print__footnote-body { margin: 0; }

  .omx-print__signatures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--omx-space-8);
    margin-block-start: var(--omx-space-11);
  }
  .omx-print__signature-rule {
    display: block;
    border-block-start: var(--omx-border-width) solid var(--omx-color-text);
    margin-block-end: var(--omx-space-2);
  }
  .omx-print__signature-title { margin: 0; font-weight: var(--omx-font-weight-semibold); }
  .omx-print__signature-name { margin: 0; color: var(--omx-color-text-muted); font-size: var(--omx-font-size-caption); }

  .omx-print__provenance {
    margin: 0;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
    border-block-start: var(--omx-border-width) solid var(--omx-color-border-subtle);
    padding-block-start: var(--omx-space-3);
  }

  /* ⚠️ The on-screen preview has to survive a phone. The document itself is A4
   * by definition, but the PREVIEW of it is a web page, and a two-column
   * letterhead cannot fit 320px. Measured: /gallery/print scrolled sideways by
   * 60px at 320px and 5px at 375px.
   *
   * ⛔ This is a SCREEN rule only — @media print below is untouched, so the
   * paper output keeps its two-column head. */
  @media (max-width: 640px) {
    .omx-print { padding: var(--omx-space-6); }
    .omx-print__head {
      grid-template-columns: minmax(0, 1fr);
      gap: var(--omx-space-5);
    }
    .omx-print__identity { text-align: start; justify-items: start; }
    .omx-print__facts { grid-template-columns: auto minmax(0, 1fr); }
    .omx-print__facts dt { text-align: start; }
    .omx-print__title { font-size: var(--omx-font-size-title); }
  }

  @media print {
    @page {
      size: A4;
      margin: 15mm;
    }

    .omx-print {
      inline-size: auto;
      margin: 0;
      padding: 0;
      border: 0;
      border-radius: 0;
      /* 🔴 Ink is not colour. Black on white, no tints: a status conveyed by a
       * pale amber wash does not exist on a mono laser, and across a thousand
       * invoices it is a toner cartridge. Everything the screen says in colour,
       * the paper says in words. */
      background: #fff;
      color: #000;
    }

    .omx-print__bar { display: none; }

    .omx-print__head { border-block-end-color: #000; }
    .omx-print__ids,
    .omx-print__facts dt,
    .omx-print__to-label,
    .omx-print__footnote,
    .omx-print__signature-name,
    .omx-print__provenance { color: #000; }

    .omx-print__watermark-note { border-color: #000; color: #000; }

    /* The diagonal stamp. Fixed so it lands on every page, and behind the
     * content rather than over it — a watermark that obscures a figure has
     * turned a document into an argument. */
    .omx-print[data-omx-watermark]::before {
      content: attr(data-omx-watermark);
      position: fixed;
      inset: 40% 0 auto 0;
      text-align: center;
      font-size: 72pt;
      font-weight: 700;
      letter-spacing: 0.1em;
      color: #000;
      opacity: 0.07;
      rotate: -24deg;
      z-index: var(--omx-z-behind);
      pointer-events: none;
    }

    /* A signature block alone on a final page reads as a missing document. */
    .omx-print__signatures { break-inside: avoid; break-before: avoid; }
    .omx-print__signature { break-inside: avoid; }
    .omx-print__head, .omx-print__to { break-inside: avoid; break-after: avoid; }
    .omx-print__provenance { break-before: avoid; }
  }
}

/* --- Archetypes/Archetypes.css ------------------------------------- */
/* =============================================================================
 * L10 · Archetypes — the states wrapper, and the per-archetype frames.
 *
 * The REGION GRID itself lives in Layout/Page.css: `.omx-page__body` reads
 * `[data-omx-archetype]` from its parent and lays the slots out. What is here
 * is the shape-specific tuning that only makes sense once the fifteen are named,
 * plus the states wrapper.
 * ========================================================================== */

@layer omx.module {   /* L10 is the last layer: an archetype's rule must be able
                       * to answer a component's, and never the other way round. */

  .omx-states { min-inline-size: 0; }

  /* A region that is not ready has no content of its own to give it height, so
   * it would collapse and the page would jump when the content arrives. */
  .omx-states:not([data-omx-state="ready"]) { min-block-size: 120px; display: grid; }

  /* 🔴 A skeleton must never outlive its usefulness on a printed page — paper
   * has no loading state, and a page of grey bars is a page of nothing. */
  @media print {
    .omx-states[data-omx-state="loading"] { display: none; }
  }

  /* -- shape tuning --------------------------------------------------------
   * ⛔ ARCHETYPE REGION LAYOUT DOES NOT LIVE HERE. It lives in
   * `Layout/Page.css`, which says so in its own header: "Each archetype gets its
   * region layout HERE, once."
   *
   * 🔴 Slice 8 added a "narrow column" block for entity-form, wizard-import,
   * auth-screen and system-state — and this file writes into `@layer omx.module`,
   * the LAST layer, so it silently overrode rules Page.css had already got right.
   * Measured at 1900px before removal:
   *
   *   entity-form   .omx-page__body capped to 468px while STILL a two-column
   *                 grid -> cols "99.9px 352px". The aside ate the form and the
   *                 main region was 100px wide.
   *   system-state  .omx-page__body w=0, cols 0px. The page collapsed entirely.
   *
   * ⭐⭐ Nothing caught it: the gate reads source, the render assertions read
   * markup, the contrast sweep reads colour — and the SNAPSHOT BASELINE PASSED,
   * because it was captured with the defect already present. A baseline records
   * what IS, never what SHOULD BE.
   *
   * ⇒ What remains below is only what genuinely has no other owner: a fact
   * about one archetype that no component and no region grid can express. */

  /* A report is a document on screen before it is a document on paper, so it
   * gets the paper's measure rather than the shell's width. */
  .omx-page[data-omx-archetype="report"] .omx-region[data-omx-slot="main"] {
    max-inline-size: 1000px;
  }

  /* A board and a calendar own their own horizontal scrolling, so the main
   * region must not add a second scrollbar around them. */
  .omx-page[data-omx-archetype="card-board"] .omx-region[data-omx-slot="main"],
  .omx-page[data-omx-archetype="calendar"] .omx-region[data-omx-slot="main"] {
    overflow: visible;
    min-inline-size: 0;
  }

  @media print {
    /* Print an entity list or a report, and the filters are the question, not
     * the answer — but a reader holding the paper needs to know WHICH filters
     * produced it. So filters print and the toolbar does not. */
    .omx-page .omx-region[data-omx-slot="toolbar"] { display: none; }
  }
}

/* --- Layouts/Layouts.css ------------------------------------------- */
/* =============================================================================
 * The frames that are not the shell: auth, launcher, document.
 *
 * `Navigation/Shell` owns the application frame. These three are the pages that
 * deliberately do NOT have it, and each of them is missing the shell for a
 * different reason — recorded in each layout file.
 * ========================================================================== */

@layer omx.module {

  /* -- the fragment: _OmxFragment -------------------------------------------
   * ⛔ NO FRAME OF ITS OWN — no border, no background, no width. The host places
   * it and draws whatever container it sits in; a fragment that drew its own
   * panel would be a box inside the host's box. It spaces its title from its
   * body, and that is all.
   * ⭐ min-inline-size: 0, so a wide table inside it scrolls in the host's column
   * instead of pushing the host's layout wider. */
  .omx-fragment {
    display: flex;
    flex-direction: column;
    gap: var(--omx-space-3);
    min-inline-size: 0;
  }

  .omx-fragment__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--omx-space-3);
    row-gap: var(--omx-space-1);
  }

  .omx-fragment__title {
    margin: 0;
    font-size: var(--omx-font-size-title);
    font-weight: var(--omx-font-weight-semibold);
  }

  .omx-fragment__owner,
  .omx-fragment__home {
    margin: 0;
    color: var(--omx-color-text-muted);
    font-size: var(--omx-font-size-caption);
  }

  .omx-fragment__body { min-inline-size: 0; }

  /* A refusal is SEEN: dashed, where the fragment would have been. A blank gap
   * in the host page would read as "nothing to show", which is a different fact. */
  .omx-fragment__refused {
    margin: 0;
    padding: var(--omx-space-3);
    border: var(--omx-border-width) dashed var(--omx-color-border-strong);
    border-radius: var(--omx-radius-sm);
    color: var(--omx-color-text-muted);
  }

  /* -- the wall: _OmxBoard --------------------------------------------------
   * 🔴 A board FILLS the panel. No measure, no centring, no max width: the
   * constraint that makes a reading page readable makes a wall unreadable.
   *
   * ⭐ 100dvh, not 100vh. The same URL gets opened on a phone to check a line,
   * and vh there is the address-bar-less height -- so the bottom row hides under
   * the browser chrome exactly when someone is squinting at it. */
  .omx-board {
    min-block-size: 100dvh;
    margin: 0;
    background: var(--omx-color-surface-sunken);
    color: var(--omx-color-text);
    /* Nothing here is scrolled by a person. Content that does not fit is a
     * design error on the board, not something to hide behind a scrollbar. */
    overflow: hidden;
  }

  .omx-board__stage {
    block-size: 100dvh;
    padding: var(--omx-space-7);
    display: grid;
    gap: var(--omx-space-6);
    /* 🔴 ONE ROW, AND IT FILLS. The stage holds a single child (the page), so
     * `align-content` here distributes nothing -- that was this rule's second
     * wrong version. Worse, without an explicit `1fr` the row is CONTENT-SIZED,
     * and the page's own `block-size: 100%` then resolves against a track that
     * depends on it: the classic circular percentage, which collapses to the
     * content height and leaves the bottom of the panel empty.
     * ⭐ `1fr` makes the row fill the panel; the PAGE BODY then spreads its rows
     * inside it (Page.css). Two different jobs, two different elements. */
    grid-template-rows: 1fr;
  }

  /* ⚠️ STALENESS IS A PAGE-LEVEL FACT here, not a component's. When the feed
   * dies every figure on the board is suspect, so the whole stage dims and a bar
   * appears. ⛔ The numbers are NOT hidden -- someone needs to know what the line
   * was doing when contact was lost. */
  .omx-board[data-omx-board-stale] .omx-board__stage { opacity: 0.55; }

  .omx-board__stale-bar { display: none; }

  .omx-board[data-omx-board-stale] .omx-board__stale-bar {
    display: block;
    padding: var(--omx-space-4) var(--omx-space-6);
    background: var(--omx-color-warning-bg);
    color: var(--omx-color-warning-text);
    font-size: var(--omx-font-size-lead);
    font-weight: var(--omx-font-weight-bold);
    text-align: center;
    letter-spacing: var(--omx-letter-spacing-wide);
  }

  /* ⭐ The figures ARE the page. Tabular numerals so a changing digit does not
   * shift the ones beside it -- on a wall, a jittering number reads as a fault. */
  .omx-board__figure {
    font-size: clamp(3rem, 9vw, 9rem);
    font-weight: var(--omx-font-weight-bold);
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  .omx-board__label {
    font-size: var(--omx-font-size-caption);
    color: var(--omx-color-text-muted);
    text-transform: uppercase;
    letter-spacing: var(--omx-letter-spacing-wide);
  }

  /* ⛔ NEVER COLOUR ALONE. The PROD spec: "a floor screen is read at an angle,
   * in poor light, by people who may not distinguish red from green" -- so the
   * SIGN on the number is what is authoritative and the colour merely agrees. */
  .omx-board__figure--behind { color: var(--omx-color-danger-text); }
  .omx-board__figure--ahead  { color: var(--omx-color-success-text); }



  /* -- auth --------------------------------------------------------------- */

  .omx-auth {
    min-block-size: 100dvh;
    background: var(--omx-color-canvas);
  }

  .omx-auth__stage {
    min-block-size: 100dvh;
    display: grid;
    /* 🔴 A DEFINITE TRACK. With `justify-items: center` and an implicit `auto`
     * column, the track is sized by its CONTENT — so the card's
     * `inline-size: min(100%, 380px)` resolved its `100%` against a width that
     * depended on the card, and settled at the widest child's max-content
     * (the SSO button's label). Measured at 320px: the card rendered 384px and
     * the page scrolled sideways by 57px.
     *
     * ⭐ Same trap as `place-content: center` collapsing system-state to zero —
     * a percentage inside a content-sized track. There it went to 0, here it
     * grew. A definite `minmax(0, 1fr)` track fixes both directions. */
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    align-content: center;
    gap: var(--omx-space-6);
    padding: var(--omx-space-8);
  }

  .omx-auth__card {
    inline-size: min(100%, 380px);
    display: grid;
    gap: var(--omx-space-6);
    padding: var(--omx-space-9);
    background: var(--omx-color-surface);
    border: var(--omx-border-width) solid var(--omx-color-border);
    border-radius: var(--omx-radius-lg);
    box-shadow: var(--omx-shadow-md);
  }

  .omx-auth__head { display: grid; justify-items: center; gap: var(--omx-space-2); text-align: center; }

  .omx-auth__mark {
    inline-size: 36px;
    block-size: 36px;
    border-radius: var(--omx-radius-md);
    /* ⭐ THE SWEEP (owner ruling 2026-09-19). On a sign-in screen this square IS
     * the product — it is the only mark on the page and the first thing anyone
     * ever sees of Omnixa. A flat accent blue here is the generic login card
     * every SaaS ships. Both ends of the sweep clear 3.0 against the panel. */
    background: var(--omx-brand-sweep);
  }

  /* ⭐ MOVED TO THE NEW 22px STEP 2026-09-19. On an auth screen this IS the page
   * heading — there is nothing above it — so it was reaching for `title` to get
   * presence it could not have at 18. ⛔ The others that consume `title` were
   * judged and left: `.omx-text-title` IS the named step, the sidebar brand is a
   * lockup rather than a heading, and the print header is a physical medium with
   * its own size constraints and no evidence of a hierarchy problem. */
  .omx-auth__app { margin: 0; font-size: var(--omx-font-size-title-lg); }
  .omx-auth__subtitle { color: var(--omx-color-text-muted); margin: 0; }
  .omx-auth__footnote { color: var(--omx-color-text-subtle); margin: 0; text-align: center; }

  /* -- launcher ----------------------------------------------------------- */

  .omx-launcher {
    min-block-size: 100dvh;
    display: grid;
    grid-template-rows: auto 1fr;
    background: var(--omx-color-canvas);
    min-inline-size: 0;

    /* 🔴 The Topbar's responsive rules are `@container omx-content (max-width: …)`.
     * `_OmxShell` gets that container from `.omx-shell__main`; this frame had
     * NOTHING declaring it, so on the launcher the topbar still refused to
     * shrink — measured at 375px: `.omx-topbar__actions` 455px wide, document
     * 623px, the page scrolling sideways.
     * ⚠️ Any FUTURE frame that renders <vc:topbar> needs this too. */
    container-type: inline-size;
    container-name: omx-content;
  }

  .omx-launcher__content {
    /* Its own container, so anything dropped in sizes against the content area
     * and not the window — the same contract every region gets. */
    container-type: inline-size;
    container-name: omx-slot;
    padding: var(--omx-space-8);
    min-inline-size: 0;
  }

  /* -- document ----------------------------------------------------------- */

  .omx-printpage {
    /* The grey ground is what makes the white page look like a page on screen.
     * It is the only decoration this frame has, and it does not print. */
    background: var(--omx-color-canvas);
    padding: var(--omx-space-8) 0;
  }

  @media print {
    .omx-printpage { background: #fff; padding: 0; }
  }
}

/* --- GENERATED: the light-dark() fallback ------------------------ */
/* ⛔ Do not edit. Derived from Foundation/tokens.css by ci/build-assets.py.
 * Browsers WITH light-dark() skip this block entirely and keep both themes.
 * Browsers WITHOUT it would otherwise render unstyled - transparent
 * backgrounds and black text - because the failure is at substitution,
 * not at parse. They get the LIGHT palette here, which is a readable
 * product rather than a broken one.
 * ⚠️ Same layer, declared LAST: a custom property is decided by source
 * order, so this must come after the block it falls back for. */
@layer omx.base {
  @supports not (color: light-dark(#fff, #000)) {
    .omx-ui {
      --omx-color-canvas: #eef1f6;
      --omx-color-surface: #ffffff;
      --omx-color-surface-raised: #ffffff;
      --omx-color-surface-sunken: #ecf0f6;
      --omx-color-surface-overlay: rgba(16, 25, 40, 0.44);
      --omx-color-text: #1b2434;
      --omx-color-text-muted: #505f74;
      --omx-color-text-subtle: #616e80;
      --omx-color-text-inverse: #eef3fb;
      --omx-color-text-link: #2059cf;
      --omx-color-border-subtle: #e2e9f2;
      --omx-color-border: #d7e0ec;
      --omx-color-border-strong: #adbbcd;
      --omx-color-border-control: #7c8aa0;
      --omx-color-accent: #2660d8;
      --omx-color-accent-hover: #2059cf;
      --omx-color-accent-active: #1b4cb4;
      --omx-color-accent-subtle: #d8e5fc;
      --omx-color-accent-text: #f2f7ff;
      --omx-color-accent-ring: rgba(47, 111, 235, 0.3);
      --omx-color-brand-mark: var(--omx-brand-violet);
      --omx-color-control: #f1f5fc;
      --omx-color-control-hover: #e7eef9;
      --omx-color-control-active: #ced8e7;
      --omx-color-info-bg: #e9f1ff;
      --omx-color-info-subtle: #dbe8ff;
      --omx-color-info-border: #4a84e8;
      --omx-color-info-solid: #2660d8;
      --omx-color-info-on: #ffffff;
      --omx-color-info-text: #295fd4;
      --omx-color-success-bg: #e9f8f0;
      --omx-color-success-subtle: #cdeade;
      --omx-color-success-border: #2e9265;
      --omx-color-success-solid: #1f7a52;
      --omx-color-success-on: #ffffff;
      --omx-color-success-text: #1f6d4a;
      --omx-color-warning-bg: #fff7e9;
      --omx-color-warning-subtle: #fdeed6;
      --omx-color-warning-border: #b3760f;
      --omx-color-warning-solid: #9c5c17;
      --omx-color-warning-on: #ffffff;
      --omx-color-warning-text: #9c5c17;
      --omx-color-danger-bg: #ffedf1;
      --omx-color-danger-subtle: #fddfe5;
      --omx-color-danger-border: #d9505e;
      --omx-color-danger-text: #a84652;
      --omx-color-danger-solid: #d12f3d;
      --omx-color-neutral-bg: #eef2f7;
      --omx-color-neutral-subtle: #dee5ee;
      --omx-color-neutral-border: #7c8aa0;
      --omx-color-neutral-solid: #4a5568;
      --omx-color-neutral-on: #ffffff;
      --omx-color-neutral-text: #4a5568;
      --omx-color-selection-bg: rgba(47, 111, 235, 0.2);
      --omx-color-rating-on: #a86f12;
      --omx-color-rating-off: #c7d2e0;
      --omx-chart-1: #2660d8;
      --omx-chart-2: #b3730f;
      --omx-chart-3: #1f6d4a;
      --omx-chart-4: #a84652;
      --omx-chart-5: #6b46c1;
      --omx-chart-6: #0e7490;
      --omx-chart-grid: #e2e9f2;
      --omx-shadow-xs: 0 1px 2px rgba(16, 25, 40, 0.06);
      --omx-shadow-sm: 0 1px 3px rgba(16, 25, 40, 0.08), 0 1px 2px rgba(16, 25, 40, 0.04);
      --omx-shadow-md: 0 4px 12px rgba(16, 25, 40, 0.10);
      --omx-shadow-lg: 0 12px 32px rgba(16, 25, 40, 0.14);
      --omx-shadow-xl: 0 16px 40px rgba(16, 25, 40, 0.20);
      --omx-color-hairline: rgb(0 0 0 / 8%);
    }
  }
}
