/* =============================================================================
   SENUA AI DESIGN SYSTEM -- tokens
   The single source of truth for every Senua AI property: marketing sites,
   product consoles, portals, documentation.

   WHAT THIS IS BUILT FROM, and what it deliberately is not:

   - Carbon (IBM, Apache-2.0) contributes the RULES: the spacing scale, the type
     scale, the semantic token LAYER model, the grid and the accessibility
     targets. Carbon's best idea is not its blue -- it is that components never
     reference a raw colour, only a role (layer-01, text-secondary,
     border-subtle). Re-theming then changes one block instead of a stylesheet.
   - Geist (Vercel, OFL-1.1) contributes the TYPEFACE only. Two woff2 files,
     self-hosted. Using a font is not adopting a design system.
   - Senua contributes the identity: our own accent, our own dark-first ground,
     our own display tracking.

   NO REACT, NO NPM, NO BUILD STEP, NO EXTERNAL REQUEST. Plain CSS custom
   properties consumed by plain HTML. This is deliberate: all of our products run
   on your hardware, or your cloud, and require no third-party dependencies.

   PROVENANCE: values marked [src] were read from the published package;
   [doc] were taken from the published documentation. See /about/licences/.
   ============================================================================= */

:root {
  /* --- SPACING [src: @carbon/layout _spacing.scss] -------------------------
     The only legal spacing values. Anything not on this scale is a defect. */
  --sp-01: 0.125rem;  /*  2px */
  --sp-02: 0.25rem;   /*  4px */
  --sp-03: 0.5rem;    /*  8px */
  --sp-04: 0.75rem;   /* 12px */
  --sp-05: 1rem;      /* 16px */
  --sp-06: 1.5rem;    /* 24px */
  --sp-07: 2rem;      /* 32px */
  --sp-08: 2.5rem;    /* 40px */
  --sp-09: 3rem;      /* 48px */
  --sp-10: 4rem;      /* 64px */
  --sp-11: 5rem;      /* 80px */
  --sp-12: 6rem;      /* 96px */
  --sp-13: 10rem;     /* 160px */

  /* --- TYPE SCALE [doc: Carbon type scale, 17 steps] ----------------------- */
  --ts-01: 0.75rem;   /* 12 */   --ts-02: 0.875rem;  /* 14 */
  --ts-03: 1rem;      /* 16 */   --ts-04: 1.125rem;  /* 18 */
  --ts-05: 1.25rem;   /* 20 */   --ts-06: 1.5rem;    /* 24 */
  --ts-07: 1.75rem;   /* 28 */   --ts-08: 2rem;      /* 32 */
  --ts-09: 2.25rem;   /* 36 */   --ts-10: 2.625rem;  /* 42 */
  --ts-11: 3rem;      /* 48 */   --ts-12: 3.375rem;  /* 54 */
  --ts-13: 3.75rem;   /* 60 */   --ts-14: 4.25rem;   /* 68 */
  --ts-15: 4.75rem;   /* 76 */   --ts-16: 5.25rem;   /* 84 */
  --ts-17: 5.75rem;   /* 92 */

  /* --- TYPEFACE (Geist, OFL-1.1, self-hosted) ------------------------------ */
  --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  /* ONE TYPEFACE. Geist for everything, Geist Mono for code, labels and data.
     A serif was tried for section ledes and removed: it made a third family on
     every page, and hierarchy is the type scale's job, not a second face's.
     --font-serif IS GONE. It resolved to --font-sans, so it named nothing: a
     token whose only content is "I am another token" is a rename waiting to
     become a lie. Anything that referenced it must reference --font-sans. */

  /* --- TRACKING -------------------------------------------------------------
     Carbon does not track display type negatively. Measured against ten
     benchmark sites it must: Vercel runs -0.02em at 14-20px, -0.04em at 24-32px
     and -0.06em at 40px+. Untracked display type is the single most visible
     amateur tell, so this is where we deviate from Carbon deliberately. */
  --tr-body: -0.011em;
  --tr-tight: -0.02em;
  --tr-display: -0.035em;
  --tr-hero: -0.045em;
  --tr-label: 0.08em;   /* the only positive: uppercase mono eyebrows */

  /* --- LINE HEIGHT [src: @carbon/type _styles.scss] ------------------------ */
  --lh-body: 1.5;
  --lh-compact: 1.42857;
  --lh-heading: 1.25;
  --lh-display: 1.13;
  --lh-hero: 1.05;

  /* --- GRID [doc: Carbon 16-column] ---------------------------------------- */
  --grid-columns: 16;
  --grid-gutter: var(--sp-07);
  /* D3: these are REFERENCE VALUES, not switches. A media query cannot read a
     custom property, so every @media in this system carries the literal and
     these exist so a consumer can read the intended breakpoints from one place.
     If you change one, you must change the literals too -- nothing enforces it. */
  --bp-sm: 320px; --bp-md: 672px; --bp-lg: 1056px; --bp-xlg: 1312px; --bp-max: 1584px;
  /* The container IS the xlg breakpoint -- the documentation already said so,
     while the two were separate literals that nothing kept in step. One of them
     is now derived, so the claim cannot go stale. */
  --container: var(--bp-xlg);
  --measure: 640px;   /* prose. Linear ships 640px 85 times; Stripe 639px 44 times. */

  /* --- PANEL SIZE -----------------------------------------------------------
     THE WIDTH OF A THING THAT FLOATS: a dialog, a drawer, a popover, a tooltip,
     a toast. This scale exists because the overlay layer recorded its absence
     as the one token gap it was still owed, and the complaint was precise: a
     panel width is not a spacing value, yet every one of them was written as a
     multiple of --sp-13, the 160px spacing step. `calc(var(--sp-13) * 2.25)`
     is a width reached by feel and then dressed as a scale -- the arithmetic
     is the tell, and a fractional multiplier of a step is the loudest form of
     it.

     THESE VALUES ARE THE ONES THAT WERE ALREADY SHIPPING. Nothing here is a
     new width and no panel changed size: the seven multipliers in the overlay
     layer were 1.25, 1.5, 2, 2.25, 2.5, 3 and 4, and these are what they
     resolved to. Naming them changes where the next argument about a panel
     width happens -- in a scale, against the steps either side of it, rather
     than in a multiplier nobody else can see.

     BOTH ENDS ARE DERIVED rather than restated, for the reason --container is:
     a literal repeated in two places is a claim nothing keeps in step. The
     bottom of the scale is the smallest viewport the system supports, and the
     top is the prose measure -- a panel is somewhere to read, and no panel
     reads wider than prose does.

     WHAT THIS DOES NOT CLOSE, stated so the scale is not read as more than it
     is: senua-navigation.css still composes three widths out of spacing steps
     -- the menu drawer at --sp-13 * 2, the side nav at --sp-13 + --sp-12 and
     the overflow menu at --sp-13 + --sp-09 -- and senua-forms.css caps the
     scrolling table at --sp-13 * 2. The side nav's 256px has a source (it is
     the width Carbon's own side nav ships) and the others do not. They were
     never recorded as open and are not closed here. */
  --panel-01: 200px;           /* a tooltip, on a narrow screen */
  --panel-02: 240px;           /* a tooltip */
  --panel-03: var(--bp-sm);    /* 320 -- a popover, a small dialog */
  --panel-04: 360px;           /* a toast */
  --panel-05: 400px;           /* an alert dialog */
  --panel-06: 480px;           /* a dialog, a drawer */
  --panel-07: var(--measure);  /* 640 -- a large dialog. Nothing reads wider. */

  /* --- RADIUS / BORDER ------------------------------------------------------
     CARBON IS SQUARE. Zero radius on every surface and control. This is the
     single most recognisable thing about the system and I originally deviated
     from it on a preference -- which defeats the purpose of adopting a system,
     since the value is in the parts you would not have chosen yourself. Square
     reads institutional and engineered; rounded reads consumer.
     The one exception is a status pill, where the shape carries meaning. */
  --r-sm: 0; --r-md: 0; --r-lg: 0; --r-xl: 0; --r-pill: 9999px;
  --border-w: 1px;

  /* --- FOCUS GEOMETRY -------------------------------------------------------
     The ring was 2px/2px hard-coded in four places. WCAG 2.4.13 sizes a focus
     indicator, so the size is a system decision, not a per-component one, and
     it must be changeable in one edit. Inset is for controls whose ring must
     stay inside the box (a field, a segment) so it is not clipped by a
     neighbour. */
  --focus-w: 2px;
  --focus-offset: 2px;
  --focus-offset-inset: -2px;

  /* --- OPACITY --------------------------------------------------------------
     The only legal opacities. Disabled controls are exempt from 1.4.3, which is
     the ONLY reason .45 is allowed to exist -- never apply it to live content. */
  --opacity-disabled: .45;

  /* --- Z-INDEX --------------------------------------------------------------
     Four layers, named. Bare 60 and 999 were competing with whatever a
     consuming site had already invented at those numbers. */
  --z-behind: -1;      /* a decorative pseudo-element under its own content */
  --z-raised: 1;       /* content lifted above that pseudo-element */
  --z-dropdown: 60;    /* a menu over the page */
  --z-toast: 120;      /* a transient message over a menu */
  --z-skip: 999;       /* the skip link, over everything */

  /* --- MOTION ---------------------------------------------------------------
     One duration, one curve. All ten benchmark sites sit in 100-300ms. */
  --dur: 180ms;
  --dur-fast: 120ms;
  /* A LOOPING duration is a different decision from a transition. 180ms is a
     strobe when it repeats, so a looping indicator was running
     calc(var(--dur) * 8) -- arithmetic standing in for a name. */
  --dur-slow: 1440ms;
  /* A LOOP that indicates work in progress. 1440ms reads as stalled on a
     spinner; 180ms reads as a strobe. This is the third distinct duration
     because it answers a third question, not because two were not enough. */
  --dur-loop: 720ms;
  /* THE FOURTH DURATION IS "effectively none", AND IT IS A DURATION RATHER
     THAN AN ABSENCE. Under prefers-reduced-motion the base layer does not
     remove animation -- removing it cancels the animationend a component may
     be waiting on, and the component then never finishes. It shortens it past
     perception instead. That idiom was a bare .01ms written twice in the base
     layer, the last raw value in that file, with a comment asking for a name.
     This is the name. */
  --dur-none: 0.01ms;
  --ease: cubic-bezier(0.215, 0.61, 0.355, 1);

  /* Hero scrim and on-photograph text, as ROLES. The hero was the one component
     using raw #fff and rgba() literals, which is why it could not respond to the
     light theme -- B5. */
  /* Tuned by measuring the rendered pixels, not by eye. The scrim needs to be
     dark enough under the headline for white type to clear WCAG AA and NOTHING
     more -- it was 0.97 with a SECOND full-frame vertical gradient stacked on
     top, so even the bright side of the photograph was carrying 0.18 x 0.55 of
     darkening and the whole image read as mud. */
  /* The scrim colour, as channels, so a gradient can state its own falloff
     curve without inventing a colour. The named stops below stay for anything
     that wants three values; the hero ramp uses the channels directly because
     a smooth falloff needs six stops and six more colour tokens would be six
     names for one decision. */
  --scrim-rgb: 8 9 11;
  --scrim-strong: rgba(8, 9, 11, .90);
  --scrim-mid:    rgba(8, 9, 11, .40);
  --scrim-soft:   rgba(8, 9, 11, 0);
  /* The phone scrim is VERTICAL, because at that width the subject is cropped
     out and the ramp has nothing to protect on the right. These two were still
     raw rgba() literals inside the media query -- the same B5 defect the block
     above was written to fix, surviving one breakpoint down where nobody
     looked. Values unchanged, so nothing moves; they are only reachable now. */
  --hero-scrim-block-start: rgba(8, 9, 11, .88);
  --hero-scrim-block-end:   rgba(8, 9, 11, .80);
  /* The accent/cyan grade laid over the photograph. It is a treatment value,
     not a UI opacity, which is why it lives with the hero and not on the
     opacity scale -- but it was still a bare .14 inside a component. */
  --hero-grade: .14;
  --text-on-media:     #ffffff;
  --text-on-media-2:   rgba(255, 255, 255, .88);
  --text-on-media-3:   rgba(255, 255, 255, .62);

  /* --- BRAND ----------------------------------------------------------------
     Senua's own. Not IBM blue, not Vercel black.

     ONE VIOLET CANNOT DO BOTH JOBS -- this is the arithmetic, not an opinion.
     A violet carrying white 14px/500 button text needs relative luminance
     <= 0.183 (that is 4.5:1 against #fff). The same violet used as TEXT on
     --layer-02 needs luminance >= 0.211 (4.5:1 against #16181c). Those two
     windows do not overlap, so the ramp splits: a FILL step that white sits on,
     and a TEXT step that sits on a layer. Measured, not eyeballed.

     --brand-violet was #7c5cff and is now #7556f0. That is a 13% luminance
     drop and it is what takes white-on-accent from 4.35:1 (fail) to 4.83:1.
     Hue and saturation are untouched -- it is the same violet, one step down a
     ramp, and side by side it reads as the same brand. */
  --brand-violet:        #7556f0;  /* FILL. white on this = 4.83:1 */
  --brand-violet-hover:  #694ed9;  /* 5.69:1. Hover DARKENS -- see below */
  --brand-violet-active: #5f47c3;  /* 6.61:1 */
  --brand-violet-light:  #a48cff;  /* violet as TEXT on a dark layer: 6.12:1 on --layer-03 */
  --brand-violet-deep:   #644bcf;  /* violet as TEXT on a light layer: 4.80:1 on light --layer-03 */
  --brand-violet-ink:    #221146;  /* the light-theme focus ring: 3.51:1 even against the accent fill */
  /* HOVER DARKENS RATHER THAN LIGHTENS, which is Carbon's model (blue-60 ->
     blue-70) and the only direction available: lightening a filled button
     drops white below 4.5:1, which is exactly how the old #9d7bff hover
     reached 3.13:1. */
  --brand-cyan:       #22d3ee;
  --brand-cyan-light: #67e8f9;  /* focus ring: needs 3:1 against the ACCENT FILL too,
                                   because the segmented control's ring is inset */
}

/* =============================================================================
   SEMANTIC LAYER -- Carbon's model, Senua's values.
   Components reference ROLES below, never the raw values above. Re-theming or
   adding a light mode changes only this block.
   ============================================================================= */
:root {
  /* surfaces, four elevation steps, dark-first */
  --background:   #0a0b0d;
  --layer-01:     #101215;
  --layer-02:     #16181c;
  --layer-03:     #1c1f24;
  /* A FIELD IS A LAYER-02 SURFACE. It was #16181c in dark -- byte-identical to
     --layer-02 -- and #f4f4f5 in light, byte-identical to --layer-01: the same
     role pointing at a different rung in each theme, by accident, because both
     were copied literals. One definition now, and it cannot drift again. */
  --field:        var(--layer-02);
  --overlay:      rgba(8, 9, 11, 0.72);

  /* text, four steps -- all four in use, which is what stops a page reading flat */
  --text-primary:     #f7f8f8;
  --text-secondary:   #cdd2db;
  --text-helper:      #8a8f98;
  --text-disabled:    #5f646d;
  --text-on-accent:   #ffffff;

  /* borders are alpha hairlines so they sit correctly over any layer.
     THREE BORDER ROLES, AND THE DIFFERENCE IS LEGAL, NOT AESTHETIC:
     subtle and strong are DECORATION -- a card edge, a table rule, a divider.
     They measure 1.12:1 to 1.35:1, which is correct for decoration and
     disqualifying for anything else. --border-interactive is the boundary of a
     CONTROL, which WCAG 1.4.11 requires to reach 3:1, and .btn--ghost /
     .btn--secondary / fieldset.seg / .badge had been drawing their only visible
     edge with --border-strong at 1.25:1. Alpha is kept deliberately: the
     composite tracks whatever layer the control sits on, so one value clears
     3:1 over all four rungs (measured 3.79-3.84 dark, 3.52-3.69 light). */
  --border-subtle:      rgba(255, 255, 255, 0.06);
  --border-strong:      rgba(255, 255, 255, 0.10);
  --border-interactive: rgba(255, 255, 255, 0.40);
  --border-focus:       var(--brand-cyan-light);
  /* THE RING ON AN ACCENT FILL IS A DIFFERENT DECISION. The segmented control
     insets its ring, so on the checked segment the ring's neighbour is not the
     page -- it is the accent. One colour cannot clear 3:1 against both a dark
     ground and a saturated fill, and pretending it can is how a ring ends up
     at 1.25:1 on the one control that needs it most. Measured, not reasoned:
     cyan-light on a cyan accent came to 1.25:1. */
  --border-focus-on-accent: var(--brand-cyan-light);

  /* interaction */
  --accent:        var(--brand-violet);
  --accent-hover:  var(--brand-violet-hover);
  --accent-active: var(--brand-violet-active);
  /* Violet as TEXT or as an icon on a layer. NOT interchangeable with --accent:
     --accent is a fill that white sits on, this is ink that sits on a layer.
     Using --accent as text measured 4.09:1 on --layer-02 and 3.80:1 on
     --layer-03 -- inside a card, which is the only place it ever appears. */
  --accent-text:   var(--brand-violet-light);
  --accent-tint:   rgba(117, 86, 240, 0.12);
  --link:          #a99bff;

  /* status -- functional only. Never use support-error for a call to action.
     Every one of these is TEXT (.notice__kind, .field__error, .badge) and a
     1px border, so each must clear 4.5:1 on the worst layer it can sit on. */
  --support-error:   #fa4d56;
  --support-success: #30a46c;
  --support-warning: #f2b712;
  --support-info:    var(--brand-cyan);

  /* DARK MODE SHIPS NO SHADOWS. Depth is the layer ladder plus a hairline.
     This is Linear's rule and it is why their dark UI looks solid. The tokens
     are still declared here because a FLOATING element -- a menu, the focused
     skip link -- needs separation from whatever it covers, and in light theme
     a hairline alone does not give it. Dark resolves them to none, so the rule
     costs nothing and the component does not need to know which theme it is in. */
  --shadow-sm: none;
  --shadow-md: none;
}

/* Light theme: the same role names, different values. Nothing else changes.

   EVERY ROLE THAT CARRIES CONTRAST MUST BE OVERRIDDEN HERE, and the ones that
   were not are why this theme failed. A role left out does not fall back to
   something sensible -- it inherits the DARK value onto a white surface, which
   is how --support-info reached 1.81:1, --support-warning 1.82:1 and, worst,
   --border-focus 1.81:1: the focus ring was effectively invisible in light
   theme and the system failed WCAG 1.4.11 outright. The rule for anyone adding
   a colour role: if it is declared in :root it is declared here too, or it is
   a role that is genuinely theme-independent (white on the accent fill) and
   says so.

   The status hues are the dark values walked down the same ramp -- same hue,
   same saturation, luminance taken to where 4.5:1 lands on --layer-03, which
   is the darkest surface any of them sits on (.notice and .card). */
[data-theme="light"] {
  --background: #ffffff;  --layer-01: #f4f4f5;  --layer-02: #ededf0;  --layer-03: #e4e4e8;
  --overlay: rgba(22, 24, 28, 0.6);
  /* This used to read "--field is NOT overridden: it is var(--layer-02) and
     follows the theme." That is true only when the theme is on the SAME
     element that declares it. A custom property defined as var(--other)
     substitutes where it is DECLARED -- on :root -- and what inherits down is
     the already-resolved colour. So a light theme scoped to a SECTION of a
     dark page re-themed --layer-02 and left --field at the dark #16181c: a
     dark input box in a light panel, --text-primary on it measuring 1.00:1.
     Measured across all 116 tokens, --field was the only one with this shape.
     Re-declaring it here re-runs the substitution in this scope. */
  --field: var(--layer-02);
  --text-primary: #16181c; --text-secondary: #3c4149;
  /* was #6f6e77 -- 4.31:1 on --layer-02 and 3.97:1 on --layer-03, which is
     precisely where .notice and .card put helper text. */
  --text-helper: #63626b;
  --text-disabled: #9a9aa2; --text-on-accent: #ffffff;
  --border-subtle: rgba(0, 0, 0, 0.05); --border-strong: rgba(0, 0, 0, 0.10);
  --border-interactive: rgba(0, 0, 0, 0.48);
  /* The ring must clear 3:1 against every surface it can touch INCLUDING the
     accent fill, because the segmented control insets it. On a light ground
     that forces a near-black ring: anything light enough to read as cyan
     against #fff is under 3:1 against the violet. Violet-black keeps the brand. */
  --border-focus: var(--brand-violet-ink);
  --border-focus-on-accent: var(--brand-violet-ink);
  --accent-text: var(--brand-violet-deep);
  --accent-tint: rgba(117, 86, 240, 0.10);
  --link: #5b42d6;
  --support-error:   #ae363c;
  --support-success: #206e49;
  --support-warning: #7b5d09;
  --support-info:    #116b79;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
  --shadow-md: 0 4px 12px rgba(0,0,0,.06);
}

/* =============================================================================
   THEMES
   A theme is ONLY token values. No theme adds a rule, a component or a
   selector -- if one needed to, the component would be wrong rather than the
   theme. That is the whole return on the semantic-role model: a component asks
   for --layer-02 and --text-secondary and never learns what those resolve to.

   Every theme below was MEASURED, not asserted: each text role against every
   surface it can sit on, each border and focus ring against its neighbour, in
   the same harness that measures the two originals. A theme that fails a pair
   does not ship, because a palette that reads well and fails AA is worse than
   one that does neither -- it gets adopted first and fails procurement later.
   ============================================================================= */

/* --- MIDNIGHT ----------------------------------------------------------------
   Cooler and deeper than the default, with cyan carrying the accent instead of
   violet. For an operations console: a room that is dark on purpose, where the
   screen is looked at for a shift rather than for a minute. */
[data-theme="midnight"] {
  --background: #05080f;  --layer-01: #0a0f1a;  --layer-02: #101827;  --layer-03: #182234;
  --overlay: rgba(2, 4, 8, 0.72);
  --field: var(--layer-02);
  --text-primary: #f2f6fb; --text-secondary: #c3cfdf;
  --text-helper: #8695ab; --text-disabled: #5b6779;
  /* White, because the accent moved DOWN the cyan ramp so that white would
     clear on it. The dark label here was correct for the bright #22d3ee it
     used to be and became wrong the moment the fill changed -- measured at
     1.87:1 on the pressed state. A colour that is right only in combination
     has to move when its partner does. */
  --text-on-accent: #ffffff;
  --border-subtle: rgba(160, 200, 255, 0.08);
  --border-strong: rgba(160, 200, 255, 0.14);
  --border-interactive: rgba(190, 215, 240, 0.44);
  --border-focus: var(--brand-cyan-light);
  /* Cyan is a FILL here, so the label on it goes dark rather than white --
     white on #22d3ee is 1.9:1 and no amount of preference fixes that. */
  /* Down the cyan ramp until white clears 4.5:1 ON the fill and the light ring
     clears 3:1 AGAINST it. #22d3ee carries neither -- white on it is 1.9:1. */
  --accent: #0e7490; --accent-hover: #0c6278; --accent-active: #0a5060;
  --border-focus-on-accent: var(--brand-cyan-light);
  --accent-text: #6fdcef;
  --accent-tint: rgba(34, 211, 238, 0.12);
  --link: #6fdcef;
  --support-error: #ff8b90; --support-success: #5fd9a4;
  --support-warning: #ffc861; --support-info: #7fd9ea;
  --shadow-sm: none; --shadow-md: none;
}

/* --- VIOLET ------------------------------------------------------------------
   The brand palette carried into the surfaces themselves rather than kept to
   the accent. For a marketing property, where the page is meant to look like
   the company and not like a console. */
[data-theme="violet"] {
  --background: #0b0714;  --layer-01: #130d20;  --layer-02: #1a122c;  --layer-03: #241938;
  --overlay: rgba(8, 5, 16, 0.70);
  --field: var(--layer-02);
  --text-primary: #f8f6fd; --text-secondary: #d3cbe6;
  --text-helper: #9b92b4; --text-disabled: #6b6382;
  --text-on-accent: #ffffff;
  --border-subtle: rgba(196, 180, 255, 0.09);
  --border-strong: rgba(196, 180, 255, 0.15);
  --border-interactive: rgba(210, 198, 255, 0.46);
  --border-focus: var(--brand-cyan-light);
  --border-focus-on-accent: var(--brand-cyan-light);
  --accent: var(--brand-violet); --accent-hover: var(--brand-violet-hover);
  --accent-active: var(--brand-violet-active);
  --accent-text: #b9a5ff;
  --accent-tint: rgba(117, 86, 240, 0.16);
  --link: #b9a5ff;
  --support-error: #ff8b90; --support-success: #5fd9a4;
  --support-warning: #ffc861; --support-info: #7fd9ea;
  --shadow-sm: none; --shadow-md: none;
}

/* --- PAPER -------------------------------------------------------------------
   A warm light theme for long-form reading. The neutral light theme is correct
   for an interface and slightly clinical for a page somebody reads for twenty
   minutes; this warms the ground without tinting the type. */
[data-theme="paper"] {
  --background: #fbf9f5;  --layer-01: #f4f1ea;  --layer-02: #ebe7de;  --layer-03: #e1dcd1;
  --overlay: rgba(28, 24, 18, 0.58);
  --field: var(--layer-02);
  --text-primary: #1b1813; --text-secondary: #403a31;
  --text-helper: #67604f; --text-disabled: #9c948a;
  --text-on-accent: #ffffff;
  --border-subtle: rgba(40, 32, 20, 0.07);
  --border-strong: rgba(40, 32, 20, 0.13);
  --border-interactive: rgba(40, 32, 20, 0.56);
  --border-focus: var(--brand-violet-ink);
  --border-focus-on-accent: var(--brand-violet-ink);
  --accent: var(--brand-violet); --accent-hover: var(--brand-violet-hover);
  --accent-active: var(--brand-violet-active);
  --accent-text: #5b43c9;
  --accent-tint: rgba(117, 86, 240, 0.10);
  --link: #5b42d6;
  --support-error: #a1322f; --support-success: #1d6440;
  --support-warning: #6f5406; --support-info: #0f5f6c;
  --shadow-sm: 0 1px 2px rgba(60,50,30,.06);
  --shadow-md: 0 4px 12px rgba(60,50,30,.08);
}

/* --- CONTRAST ----------------------------------------------------------------
   Maximum separation, for a reader who needs it and for a procurement
   requirement that asks for more than AA. Pure grounds, no translucent
   borders -- a hairline at 6% opacity is invisible to the person this theme
   exists for, so every border here is solid and every one of them is visible.
   Not merely a darker light theme: the accent moves too, because a violet that
   reads well at AA does not reach AAA against white. */
/* THE LADDER HAD A DEAD RUNG. --layer-01 was #ffffff, byte-identical to
   --background, so in this theme alone a layered section painted the colour
   already behind it: `.section--layer` rendered as nothing, and on the theme
   page the "Layer 01" swatch was a white rectangle on a white ground with no
   edge -- an absence that every assertion passed, because nothing was wrong,
   it simply could not be seen. Every other theme steps four distinct
   surfaces; this one stepped three and claimed four.
   The ground stays pure white, which is what "pure grounds" meant, and the
   three rungs above it step evenly (9, 10, 10) instead of (0, 13, 12). All
   type in this theme is dark, so every rung moving DOWN can only raise a
   contrast ratio -- re-measured across all sixty pairs, not reasoned. */
[data-theme="contrast"] {
  --background: #ffffff;  --layer-01: #f6f6f6;  --layer-02: #ececec;  --layer-03: #e2e2e2;
  --overlay: rgba(0, 0, 0, 0.75);
  --field: #ffffff;
  --text-primary: #000000; --text-secondary: #1a1a1a;
  --text-helper: #3d3d3d; --text-disabled: #767676;
  --text-on-accent: #ffffff;
  --border-subtle: #767676; --border-strong: #000000;
  --border-interactive: #000000;
  --border-focus: #0b3d91;
  --border-focus-on-accent: #ffffff;
  --accent: #2b1b8f; --accent-hover: #1f1370; --accent-active: #160d52;
  --accent-text: #2b1b8f;
  --accent-tint: rgba(43, 27, 143, 0.12);
  --link: #1a3fb0;
  --support-error: #8c0f14; --support-success: #0f4d2c;
  --support-warning: #5a4200; --support-info: #08464f;
  --shadow-sm: none; --shadow-md: none;
}
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- base
   Font faces, reset, and the element defaults. Requires senua-tokens.css.
   ============================================================================= */

/* Geist, OFL-1.1, self-hosted. Variable weight so one file covers 100-900 and
   there is exactly one request per family. font-display:swap so text paints
   immediately on a slow link rather than leaving a blank hero. */
@font-face {
  font-family: "Geist";
  src: url("../fonts/Geist-Variable.woff2") format("woff2-variations"),
       url("../fonts/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("../fonts/GeistMono-Variable.woff2") format("woff2-variations"),
       url("../fonts/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900; font-style: normal; font-display: swap;
}

*, *::before, *::after { box-sizing: border-box; }
/* D10/D11: without color-scheme a dark-first system still gets light native
   controls -- a white select popup, a light scrollbar and browser-blue ticks.
   accent-color paints the native checkbox, radio and range in the brand. */
html { -webkit-text-size-adjust: 100%; color-scheme: dark; accent-color: var(--accent); }
[data-theme="light"] { color-scheme: light; }
body {
  margin: 0;
  background: var(--background);
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  /* TABULAR FIGURES ARE THE DEFAULT, AND THAT IS A CHOICE ABOUT DATA, NOT ABOUT
     PROSE. Monospaced figures are correct in a metric, a table column and a
     price, where digits must align down the column and must not reflow as a
     counter ticks. They are wrong in a sentence: tabular forms are wider than
     proportional ones and set a visible gap around every number in running
     text. This was declared globally with no way back, so a paragraph could not
     opt out. .figures--proportional is the way back; .figures--tabular restores
     the default inside a subtree that opted out further up. Both inherit. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
.figures--proportional { font-variant-numeric: proportional-nums; }
.figures--tabular      { font-variant-numeric: tabular-nums; }
/* audio, iframe, object and embed were missing. A bare <audio> carries an
   intrinsic width of about 300px in Chromium, so it is the element that pushes
   a 320px page sideways -- and it does it without any rule looking wrong.
   height:auto stays off the replaced elements that have no intrinsic ratio. */
img, svg, video { max-width: 100%; height: auto; }
audio, iframe, object, embed { max-width: 100%; }

/* --- the type scale, as STEPS ----------------------------------------------
   A STEP IS THREE VALUES, NEVER ONE. The scale used to carry size only, so a
   12px specimen and a 92px specimen rendered at identical leading and identical
   tracking -- which contradicts this system's own stated rule, and is the
   untracked display type the tokens file calls the single most visible amateur
   tell. Size, leading and tracking are now declared together at every step.

   The table, and it is the ONLY place these pairings are decided:

     step   px   leading         tracking        band
     01-05  12-20  --lh-body       --tr-body     text: prose, helper, labels
     06-07  24-28  --lh-heading    --tr-tight    heading
     08     32     --lh-heading    --tr-display  heading, display tracking
     09-10  36-42  --lh-display    --tr-display  display
     11     48     --lh-display    --tr-hero     display, hero tracking
     12-17  54-92  --lh-hero       --tr-hero     hero

   Leading collapses 1.5 -> 1.05 and tracking tightens -0.011em -> -0.045em as
   the step climbs, monotonically. Values come from tokens only; the top of the
   scale wants a tighter pair than --tr-hero/--lh-hero and there is no token for
   it (see the report note on --tr-mega / --lh-mega).

   These classes are also what makes the scale HONEST: before this, five of the
   seventeen steps (12, 14, 15, 16, 17) were referenced by no rule at all. */
.ts-01 { font-size: var(--ts-01); line-height: var(--lh-body);    letter-spacing: var(--tr-body); }
.ts-02 { font-size: var(--ts-02); line-height: var(--lh-body);    letter-spacing: var(--tr-body); }
.ts-03 { font-size: var(--ts-03); line-height: var(--lh-body);    letter-spacing: var(--tr-body); }
.ts-04 { font-size: var(--ts-04); line-height: var(--lh-body);    letter-spacing: var(--tr-body); }
.ts-05 { font-size: var(--ts-05); line-height: var(--lh-body);    letter-spacing: var(--tr-body); }
.ts-06 { font-size: var(--ts-06); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
.ts-07 { font-size: var(--ts-07); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
.ts-08 { font-size: var(--ts-08); line-height: var(--lh-heading); letter-spacing: var(--tr-display); }
.ts-09 { font-size: var(--ts-09); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
.ts-10 { font-size: var(--ts-10); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
.ts-11 { font-size: var(--ts-11); line-height: var(--lh-display); letter-spacing: var(--tr-hero); }
.ts-12 { font-size: var(--ts-12); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
.ts-13 { font-size: var(--ts-13); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
.ts-14 { font-size: var(--ts-14); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
.ts-15 { font-size: var(--ts-15); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
.ts-16 { font-size: var(--ts-16); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
.ts-17 { font-size: var(--ts-17); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }

/* --- headings --------------------------------------------------------------
   Tracking tightens as size grows and leading collapses toward 1.0. Weight
   caps at 600: Stripe sets every heading at 300 and nobody credible ships 800. */
/* Headings need space ABOVE them, not only below. Without it a heading crowds
   whatever precedes it -- visible immediately on the component page, where the
   metric source line collided with the next heading. margin-top collapses
   against a preceding margin-bottom, so this does not double up. */
h1, h2, h3, h4 {
  margin: 0 0 var(--sp-05);
  color: var(--text-primary);
  font-weight: 600;
  text-wrap: balance;
}
/* TYPE STEPS AT BREAKPOINTS, IT DOES NOT CLAMP. clamp() interpolates, so at
   1440px a clamp(28px, 3.2vw, 48px) heading renders 46.1px -- a value on no
   scale. A scale whose values are only hit at two viewport widths is not a
   scale. Carbon steps discretely and so do we: every rendered size is a token
   at every width, which is also what makes it testable.

   THE LADDER, and every cell is a step from the table above:

              < 672px        >= 672px       >= 1056px
     h1       09  36px       11  48px       13  60px
     h2       07  28px       09  36px       11  48px
     h3       06  24px       07  28px       08  32px
     h4       04  18px       05  20px       05  20px
     body     03  16px       03  16px       03  16px

   Two defects this fixes. (1) h1 used to sit at step 08 below 672px -- the same
   32px as .metric__value, so a page title and a statistic rendered identically
   on a phone and the hierarchy simply was not there. h1 now opens at step 09.
   (2) h3 and h4 never stepped at ANY breakpoint while h1 and h2 stepped twice,
   so the gap between h2 and h3 grew from 4px to 20px as the viewport widened
   and the lower half of the ladder went flat. Both now step.

   Leading and tracking follow each element's CURRENT step, which is the whole
   point of the table -- h1 is not tracked at --tr-hero when it is rendering
   36px on a phone. */
h1 { font-size: var(--ts-09); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
h2 { font-size: var(--ts-07); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
h3 { font-size: var(--ts-06); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
/* h4 is the one documented departure from the step table: step 04 sits in the
   text band, whose leading is --lh-body, and a two-line heading does not want
   prose leading. A heading takes heading leading at every step it occupies. */
h4 { font-size: var(--ts-04); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
@media (min-width: 672px) {
  h1 { font-size: var(--ts-11); line-height: var(--lh-display); letter-spacing: var(--tr-hero); }
  h2 { font-size: var(--ts-09); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
  h3 { font-size: var(--ts-07); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
  h4 { font-size: var(--ts-05); line-height: var(--lh-heading); letter-spacing: var(--tr-tight); }
}
@media (min-width: 1056px) {
  h1 { font-size: var(--ts-13); line-height: var(--lh-hero);    letter-spacing: var(--tr-hero); }
  h2 { font-size: var(--ts-11); line-height: var(--lh-display); letter-spacing: var(--tr-hero); }
  h3 { font-size: var(--ts-08); line-height: var(--lh-heading); letter-spacing: var(--tr-display); }
}

/* --- display headings ------------------------------------------------------
   THE ROLE THE TOP OF THE SCALE WAS MISSING. Steps 12 and 14-17 (54, 68, 76,
   84, 92px) were referenced by nothing, so the system advertised seventeen
   steps and used twelve. A scale with five dead rungs is a scale that is
   lying about its own range, and the honest repair is either to delete them or
   to give them the job they exist for. They exist for one job: the single
   largest piece of type on a page -- a hero headline, a campaign statement, a
   figure that carries a section on its own.

   .display is that headline. .display--sm is one rung below it, for a section
   opener that must outrank an h1 without matching the hero.

   These step across FIVE breakpoints rather than three, because the difference
   between a 48px phone headline and a 92px headline at 1584px is the whole
   reason the top of the scale exists, and three steps cannot cover it without
   a jump no scale contains. Still steps, never clamp().

                  < 672     >= 672    >= 1056   >= 1312   >= 1584
     .display     11 48px   13 60px   15 76px   16 84px   17 92px
     .display--sm 10 42px   12 54px   14 68px   15 76px   15 76px       */
.display     { font-size: var(--ts-11); line-height: var(--lh-display); letter-spacing: var(--tr-hero); }
.display--sm { font-size: var(--ts-10); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
@media (min-width: 672px) {
  .display     { font-size: var(--ts-13); line-height: var(--lh-hero); letter-spacing: var(--tr-hero); }
  .display--sm { font-size: var(--ts-12); line-height: var(--lh-hero); letter-spacing: var(--tr-hero); }
}
@media (min-width: 1056px) {
  .display     { font-size: var(--ts-15); }
  .display--sm { font-size: var(--ts-14); }
}
@media (min-width: 1312px) {
  .display     { font-size: var(--ts-16); }
  .display--sm { font-size: var(--ts-15); }
}
@media (min-width: 1584px) {
  .display     { font-size: var(--ts-17); }
}

h2 { margin-top: var(--sp-11); }
h3 { margin-top: var(--sp-08); }
h4 { margin-top: var(--sp-07); }
:is(h1,h2,h3,h4) + :is(h2,h3,h4) { margin-top: var(--sp-05); }
.hero :is(h1,h2,h3), .section > .container > :first-child { margin-top: 0; }

p, li { text-wrap: pretty; }
p { margin: 0 0 var(--sp-05); max-width: var(--measure); }
/* A list of prose is prose. Capped only when unclassed -- a component's list
   (the nav, a card grid) carries a class and owns its own width. */
ul:not([class]), ol:not([class]), dl:not([class]) { max-width: var(--measure); }
/* LINKS: a bare `a` rule that sets a colour and leaves the browser's default
   underline in place destroys any link that WRAPS content. On the home page
   every product card is <a class="prod">, so every word inside it -- heading,
   label, body copy -- rendered underlined and lavender. That is the single
   worst rendering defect this site has had.
   The rule: a link is unstyled by default and INHERITS its context. Only a
   link inside running prose is coloured and underlined, because that is the
   only place a reader needs it marked.

   BOTH HALVES OF THE MODEL ARE NOW ZERO-SPECIFICITY, AND THE PAIRING IS THE
   POINT -- see the long note on the prose rule below. The reset is wrapped in
   :where() so the prose rule (also zero) can still beat it on source order,
   and so that ANY class a component brings beats both. `a` at (0,0,1) would
   out-specify a zero-specificity prose rule and leave every prose link
   unmarked, which is the one way this change could have gone wrong.

   A zero-specificity author rule still beats the browser's own stylesheet:
   origin decides before specificity does, so the default blue and the default
   underline are removed exactly as before. */
:where(a) { color: inherit; text-decoration: none; }
:where(a:hover) { color: inherit; }
/* PROSE means running text. The first version of this rule listed `li`, which
   made every navigation item prose -- nav lists, footer columns and menus all
   rendered underlined. A list inside a nav is not prose, so the rule excludes
   anything under a nav or a list that has opted out with .list--bare. */
/* A list that has opted out of being a list. Declared here, not merely named
   inside a :not(), which is how it existed before -- referenced twice and
   defined nowhere, so it was undiscoverable by reading the file and any
   consumer using it got no list reset either. */
.list--bare { list-style: none; margin: 0; padding: 0; }

/* THE PROSE MARK, AND WHY IT IS NOW WORTH (0,0,0).

   This rule has caused five separate rendering defects, all of the same shape:
   the main menu held at a caption size, product cards rendering as underlined
   lavender text, every footer link coming out violet on a dark bar, a bare
   list contradicting its own caption, and a stepper rendering two of its five
   step titles as prose links in the middle of an interface. Each time a
   component class was written correctly and LOST, and each time the fix
   looked like it should have worked.

   IT WAS NEVER THE (0,2,1) IT LOOKED LIKE. :not() carries the specificity of
   its argument, so `:not(nav li)` added two elements and `:not(.list--bare li)`
   added a class and an element:

     :where(li) :not(nav li) :not(.list--bare li)   a  :not([class])
       0          2 elements   1 class + 1 element  1 el   1 class
                                                    = (0, 2, 4)

   A component class at (0,2,1) loses to that on ELEMENT count, which is not a
   number anyone reads off the selector. Three separate layers then wrote
   defensive selectors to out-shout a number they could not see:
   `.stepper .stepper__step .stepper__title a` is three classes; the media row
   is `.media-list > li > a.media-object:not(.btn):not([class*="nav"])`. Those
   are not components expressing themselves, they are components fighting.

   SO THE RULE STOPS BEING A CONTEST. The whole selector goes inside :where(),
   which makes it (0,0,0), and every component class in the system -- one
   class, at (0,1,0) -- now beats it outright. Nothing has to know the number,
   because the number is zero.

   The three alternatives, and why not:
   - LEAVE IT AND DOCUMENT (0,2,4). The number has been documented in three
     files already and the defect happened twice more. A number that has to be
     known to write a correct component is a rule that will be got wrong again.
   - SCOPE IT TO AN EXPLICIT .prose CONTAINER. Correct in the abstract, but it
     moves the failure from "a component link is marked" to "a prose link is
     NOT marked", which is silent: an unmarked link in a paragraph still looks
     like text and nobody files it. This system has already shipped that exact
     failure once, when the selector list was accidentally broken and no
     paragraph link was styled for weeks with nothing to notice.
   - RAISE IT TO THREE CLASSES. That is the arms race that produced the three
     defensive selectors above.

   TWO CONDITIONS MAKE (0,0,0) SAFE, and both are met above:
   1. The `a` reset must also be zero, or `a { color: inherit }` at (0,0,1)
      out-specifies this and no prose link is ever marked. It is `:where(a)`.
   2. Being equal, SOURCE ORDER decides -- this rule must stay BELOW the reset
      in this file. It does, and this file is concatenated second, before every
      component layer, so a component can only ever win.

   `a:not([class])` is kept inside the :where(), where it costs nothing. It is
   the same contract the link page states to every consumer -- "if a link is
   not in a paragraph, give it a class" -- and it means a classed anchor is
   outside this rule by MATCHING as well as by weight. Belt and braces, now
   that the braces are free.

   The exclusions are kept for the same reason: an anchor with no class inside
   a nav or inside a .list--bare is still not prose, and dropping them would
   newly mark links that three components deliberately leave bare. The nav
   exclusion now covers the paragraph branch as well, which it never did: at
   (0,1,1) that branch used to out-specify `nav a` (0,0,2), so a paragraph
   inside a navigation was prose and a list item inside one was not. At zero
   the contest would have flipped the other way and produced a link with the
   prose COLOUR and no underline -- a half-state. It is stated instead, and
   inside :where() it costs nothing to state. */
:where(
  :is(p, dd, figcaption, td, .lede, .evidence__note, .field__hint):not(nav *) a:not([class]),
  li:not(nav li):not(.list--bare li) a:not([class])
) {
  color: var(--link); text-decoration: underline;
  /* Was `1px` and `2px` literal. The hairline is --border-w and the offset is
     two of them, so both are now derived from the token rather than from a
     number that happens to match it today. */
  text-decoration-thickness: var(--border-w);
  text-underline-offset: calc(var(--border-w) * 2);
}
/* This omitted `li` while the rule above includes it, so a link in a prose
   list was the only prose link that did nothing on hover. Same exclusions, and
   figcaption / .evidence__note / .field__hint are included here too -- they
   were marked by the rule above and had no hover, which is the same asymmetry
   one step further on. */
:where(
  :is(p, dd, figcaption, td, .lede, .evidence__note, .field__hint):not(nav *) a:not([class]):hover,
  li:not(nav li):not(.list--bare li) a:not([class]):hover
) {
  color: var(--text-primary); }
/* `.doc-nav` was a DOCUMENTATION class sitting in the shipped product
   stylesheet: no consumer has a .doc-nav, and the docs own their own chrome.
   Removed. Nothing changes -- .doc-nav is a <nav>, so `nav a` already matched
   it, and its links are .doc-nav__link, which [class*="nav__"] already
   matched. */
nav a, [class*="nav__"] a { text-decoration: none; }

/* Visible focus on every interactive element. WCAG 2.4.7, and a procurement
   question in every government tender we will answer. */
/* D16: a skip link only works if its target can receive focus. The CSS cannot
   supply that -- the target element needs tabindex="-1" in the markup, or Enter
   moves the viewport without moving focus. Stated here because it was verified
   failing and is invisible otherwise. */
/* The ring was `2px` and its offset `2px`, both literal. It is now --focus-w
   and --focus-offset, declared in the tokens file as FOCUS GEOMETRY, because
   WCAG 2.4.13 sizes a focus indicator and that makes the size a system
   decision rather than a per-component one. (The note that used to sit here
   asked for exactly those two tokens as though they did not exist. They do,
   and this rule has been reading them; the request outlived its own fix.) */
:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-sm);
}

/* --- code, and the size it takes from its context --------------------------
   TWO FAILURES, AND THE FIX MUST AVOID BOTH.

   (1) NEVER size type in em. em compounds against whatever the element sits
       inside, so one rule produced 12.95px in one place and 14.8px in another
       -- values on no scale, and a <code> inside a <pre> compounded twice.
   (2) But a flat `font-size: var(--ts-02)` was not the answer either: pinned at
       14px, code rendered LARGER than the 12px helper text containing it. A
       field hint, a badge and a metric source all ran 12px with 14px code
       inside them, so the annotation outweighed the sentence.

   The mechanism is an inherited custom property. A context declares the STEP
   its code should take; code reads it. The value is TAKEN, never multiplied, so
   nesting cannot compound -- a <code> inside a <pre> inside a hint resolves to
   the same step all three times, which is exactly what em could not do. Every
   resolved value is a token on the scale.

   Consumers: declare --ts-code next to your own font-size whenever you set one
   below --ts-03. The component classes listed below are covered here because
   they sit in senua-components.css, which this file does not own. */
code, pre, kbd, samp { font-family: var(--font-mono); font-size: var(--ts-code, var(--ts-02)); }
body   { --ts-code: var(--ts-02); }   /* 16px prose -> 14px code */
.lede  { --ts-code: var(--ts-04); }   /* 20px lede  -> 18px code */
/* The 12px contexts. Code inside them matches them and never exceeds them;
   there is no step below 01 to drop to. */
.eyebrow, .metric__source, .field__hint, .field__error, .badge, .notice__kind,
.evidence__bar, .nav__menu em { --ts-code: var(--ts-01); }
/* Code in a heading takes the heading's own size. `inherit` is not `em`: it
   copies the parent's computed size once and cannot compound. */
:is(h1, h2, h3, h4) :is(code, kbd, samp) { font-size: inherit; }
/* A <pre> keeps its line breaks by definition, so a long line cannot wrap and
   pushes the page sideways. Code blocks, tables and diagrams are the ONLY
   things allowed to be wider than the viewport, and only inside their own
   scroll container -- never by scrolling the document. */
pre { max-width: 100%; overflow-x: auto; }

/* --- the editorial lede ----------------------------------------------------
   One per section, under the h2. The serif is what makes a section opening read
   as considered rather than as UI copy. */
.lede {
  /* GEIST, not a serif. The lede was set in Charter/Georgia, which made a third
     typeface on every page. It is distinguished now by SIZE and COLOUR, which is
     what a type scale is for, rather than by a different face. */
  /* Step 05, and the step's own leading and tracking. The leading was written
     as a literal 1.5 -- the same number --lh-body holds, so it looked harmless,
     but it meant a change to the body leading token silently left the lede
     behind. A raw value that happens to agree with a token is still a raw
     value. */
  font-size: var(--ts-05); line-height: var(--lh-body); letter-spacing: var(--tr-body);
  color: var(--text-primary);
  max-width: var(--measure);
  margin-bottom: var(--sp-07);
}
/* REMOVED: `@media (min-width: 1056px) { .lede { font-size: var(--ts-05); } }`
   -- a breakpoint that set the step the element already had. It read as a
   responsive rule and changed nothing at any width. */

/* --- the eyebrow -----------------------------------------------------------
   12px mono, uppercase, wide-tracked. Present on five of the ten benchmark
   sites and the cheapest single upgrade to a section header. */
/* A SEPARATOR, WHICH THIS SYSTEM DID NOT HAVE. <hr> had no rule anywhere, so
   it rendered the browser's default -- `border: 1px inset`, a raw 3D groove on
   no token, in a system whose claim is that there are none. Worse than that it
   was invisible: as a flex item in a column, the UA's `margin-inline: auto`
   shrank it to its own content width and it drew as a DOT in the middle of the
   row. Found on a consuming site's pricing page, where a rule between two
   settings rows rendered as a full stop floating in space.

   It is a hairline at the border role, full width, with a spacing step either
   side. `border: 0` first, because the UA border is what has to go; `block-size:
   0` because an <hr> with a height and a border is two lines. */
hr {
  margin-block: var(--sp-06);
  margin-inline: 0;
  inline-size: 100%;
  block-size: 0;
  border: 0;
  border-block-start: var(--border-w) solid var(--border-subtle);
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--ts-01); font-weight: 500;
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--text-helper);
  margin: 0 0 var(--sp-04);
}

/* A RUN OF TEXT IN THE MONO FACE that is not code: an identifier, a hash, a
   path, a version, a measured figure inside a sentence. <code> is the wrong
   element for those -- it says "this is code", a screen reader may announce it
   as such, and the system gives it a tinted box that a version number in a
   heading should not be wearing.

   IT WAS REFERENCED AND NEVER DEFINED, which is worse than either. The only
   rule naming .mono in this system was the overflow-wrap line below, so a page
   that used it got word-breaking and the body face -- the one property it was
   not asked for and none of the one it was. Two consuming sites had already
   written their own `.mono { font-family: var(--font-mono) }` locally, which is
   the system's gap being paid for downstream, twice. It keeps the tabular
   figures because the things set in this face are overwhelmingly numbers, and
   a column of them that jitters as it updates is the reason the token exists. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-label);
}

/* --- layout ---------------------------------------------------------------- */
.container { width: 100%; max-width: var(--container); margin-inline: auto;
             padding-inline: var(--sp-06); }
@media (min-width: 1056px) { .container { padding-inline: var(--sp-07); } }

.section { padding-block: var(--sp-12); }
@media (max-width: 1055px) { .section { padding-block: var(--sp-10); } }
@media (max-width: 671px)  { .section { padding-block: var(--sp-09); } }
.section--layer { background: var(--layer-01); color: var(--text-secondary); }

/* Carbon's 16-column grid, as CSS grid. Subgrid-free so it works everywhere. */
/* D2: with a fixed 32px gutter the 16-column grid is unusable from 672px to
   about 1000px -- fifteen gutters take 480px of a 624px container, leaving 9px
   columns. Carbon narrows its gutter at the smaller breakpoints and this did
   not. Three steps: 4 columns below md, 8 columns with a 16px gutter through
   the md band, 16 columns with the full gutter from lg. */
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-05); }
@media (min-width: 672px) {
  .grid { grid-template-columns: repeat(8, minmax(0, 1fr)); gap: var(--sp-05); }
}
@media (min-width: 1056px) {
  .grid { grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr)); gap: var(--grid-gutter); }
}

/* Every auto-fit track carries a real floor. minmax(0,1fr) with auto-fit gives
   the algorithm no minimum, packs everything into one row, and produces an
   overflow no single element reports -- a defect this system already shipped once. */
.cols-2, .cols-3, .cols-4 { display: grid; gap: var(--sp-07); }
/* THE COMMENT THAT USED TO SIT HERE DESCRIBED VALUES THAT ARE NOT IN THIS
   FILE. It announced "RAW VALUES, DELIBERATELY LEFT, AND THE ONLY ONES IN THIS
   FILE -- 280/240/200px track floors", and asked for --col-min-2/3/4 tokens to
   replace them. There are no 280/240/200 anywhere in these rules: the columns
   step discretely at the breakpoints below, exactly like the type ladder, and
   have done since auto-fit was removed. A comment naming values that do not
   exist is worse than no comment -- it sends the next reader looking for a
   defect that was already fixed, and it asks the tokens file for three tokens
   nothing would consume. Both halves are withdrawn.

   The columns step rather than auto-fit because auto-fit with minmax(0, 1fr)
   gives the packing algorithm no floor, collapses every track into one row and
   produces an overflow that no single element reports. That defect shipped
   once. */
.cols-2, .cols-3, .cols-4 { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 672px) {
  .cols-2, .cols-3, .cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1056px) {
  .cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* GRID CHILDREN MUST BE ALLOWED TO SHRINK. A grid item defaults to
   min-width:auto, which refuses to go below its content -- so one long
   unbreakable string (a URL, a mono token, a wide table) pushes the track past
   the viewport and the page scrolls sideways. The item reports the overflow,
   never the track, which is why it is hard to find. This one rule prevents the
   whole class of defect and it is the single most common responsive bug there
   is. */
/* D1: min-width:0 bounds the TRACK; it does nothing about CONTENT that cannot
   break. Measured by building this documentation: a single unbreakable token in
   one card body took a 320px viewport to scrollWidth 674, and a 1584px one to
   1728. The system declared overflow-wrap exactly once, on .metric__source.
   Both halves are needed: the track may shrink, and the text may break. */
.grid > *, .cols-2 > *, .cols-3 > *, .cols-4 > *, .metrics > * { min-width: 0; }
p, li, dd, dt, figcaption, .card__body, .metric__label, .metric__source,
.lede, .evidence__note, .field__hint, .badge, blockquote { overflow-wrap: break-word; }
/* td and th are deliberately NOT in that list. Auto table layout squeezes a
   column to whatever is left, and `break-word` then breaks ordinary words to
   fit it -- measured "throughout" needing 79px in a 61px cell at 412, and the
   same at 820 where there was room to spare. Without it the column cannot go
   below its widest word, the table exceeds its wrapper, and .table-wrap's
   overflow-x:auto scrolls, which is what that wrapper is for. A cell that will
   not fit is a scrolling problem, not a licence to break English. */
/* A URL, hash or identifier has no break opportunities at all, so it needs the
   stronger rule -- but only where such values actually appear. */
/* `anywhere` collapses an element's MIN-CONTENT to one character. On a table
   cell that hands auto table layout permission to shatter ordinary words --
   measured "throughout" broken across lines in a 61px cell at 412px wide, and
   the same at 820px where there was room to spare. A cell too narrow for its
   content is what .table-wrap's overflow-x:auto exists for. `anywhere` stays
   on the things that genuinely cannot be hyphenated: code, tokens, paths. */
code, kbd, samp, .mono, .metric__source { overflow-wrap: anywhere; }
.grid img, .cols-2 img, .cols-3 img, .cols-4 img { max-width: 100%; }

/* --dur-none is the documented reduced-motion idiom -- not "no animation",
   which would cancel the animationend handlers a component may be waiting on,
   but an animation too short to perceive. It was a bare .01ms in two places
   here, the last raw value in this file, with a note asking for a token. The
   token now exists in the tokens file beside the other three durations and
   this reads it, so a duration is a token everywhere without exception. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: var(--dur-none) !important; animation-iteration-count: 1 !important;
    transition-duration: var(--dur-none) !important; scroll-behavior: auto !important;
  }
}
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- components
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. That is Carbon's discipline and it is what makes a theme swap one
   block rather than a rewrite.
   ============================================================================= */

/* --- button ----------------------------------------------------------------
   Three variants, three sizes. Not four. Carbon ships more; a marketing site
   that needs a fourth button variant has a content problem, not a CSS one. */
.btn {
  /* CARBON BUTTON. 48px tall, square, label left-aligned.
     THE RIGHT GUTTER IS FOR AN ICON AND ONLY FOR AN ICON. Carbon's deep
     asymmetric padding exists so a 16px glyph can sit in the right-hand
     gutter; applied to a button with no icon it leaves 64px of dead space
     beside the label and reads as broken, which is exactly what it did here.
     Default is balanced padding; .btn--icon opts into the gutter. */
  display: inline-flex; align-items: center; justify-content: flex-start; gap: var(--sp-03);
  min-height: var(--sp-09); padding: var(--sp-04) var(--sp-06);
  font-family: var(--font-sans); font-size: var(--ts-02); font-weight: 500;
  line-height: var(--lh-compact); letter-spacing: var(--tr-body);
  border: var(--border-w) solid transparent; border-radius: var(--r-md);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
/* 12px was too small for a control label; the scale's body-small is the floor. */
.btn--sm { min-height: var(--sp-08); font-size: var(--ts-02); padding: var(--sp-03) var(--sp-05); }
.btn--lg { min-height: var(--sp-10); font-size: var(--ts-03); padding: var(--sp-05) var(--sp-07); }
/* B2: this reserved a 64px gutter and positioned NOTHING into it -- the exact
   dead space the variant exists to prevent. The icon is now placed in the
   gutter it reserves, and the gutter only opens when an icon is actually
   present. */
.btn--icon { padding-right: var(--sp-10); position: relative; }
.btn--icon > svg, .btn--icon > .btn__icon {
  position: absolute; right: var(--sp-05); top: 50%; transform: translateY(-50%);
  /* The icon is sized to the TYPE step its label is set in, not to a rem that
     happens to match one -- 16, 14 and 20 across the three sizes. Every size in
     this layer is a token; these were the last four rem literals in it, along
     with the three button heights and the box of a checkbox. */
  width: var(--ts-03); height: var(--ts-03); flex: 0 0 auto;
}
.btn--icon.btn--sm { padding-right: var(--sp-08); }
.btn--icon.btn--sm > svg, .btn--icon.btn--sm > .btn__icon { right: var(--sp-04); width: var(--ts-02); height: var(--ts-02); }
.btn--icon.btn--lg { padding-right: var(--sp-11); }
.btn--icon.btn--lg > svg, .btn--icon.btn--lg > .btn__icon { right: var(--sp-06); width: var(--ts-05); height: var(--ts-05); }
/* A full-width button, for a card footer or a narrow column. */
.btn--block { width: 100%; justify-content: center; padding-right: var(--sp-06); }
/* Centred label, for a button that is alone in a card footer. */
.btn--center { justify-content: center; }

/* BUTTON GROUP -- the system demonstrated three buttons in a row on three pages
   and owned no rule for the row, so the only thing between them was the
   collapsed whitespace of the source: a measured 4px, which is not a spacing
   decision, it is an accident of where the newline fell. Reflowing the HTML
   would have changed the rendering. A row of buttons is a component. */
.btn-group { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-05); }
/* Attached: the segmented-toolbar form, where the buttons share edges on
   purpose and the shared border must not double. */
.btn-group--attached { gap: 0; }
.btn-group--attached > .btn + .btn { margin-left: calc(-1 * var(--border-w)); }
.btn-group--end { justify-content: flex-end; }

/* DEFINITION LIST -- index.html set out the eleven rules as a <dl> and the
   system owned no treatment, so it rendered on the browser default: a 40px
   indent that is on no scale, and lines running the full container while the
   page two screens above declares "Prose obeys the measure. 640px. Not a
   suggestion." The page contradicted its own rule in its own rendering. */
.deflist { margin: 0; }
.deflist > dt { margin-top: var(--sp-06); color: var(--text-primary); font-weight: 600; }
.deflist > dt:first-of-type { margin-top: 0; }
.deflist > dd { margin: var(--sp-02) 0 0 0; max-width: var(--measure);
  color: var(--text-secondary); line-height: var(--lh-body); }
.btn--primary { background: var(--accent); color: var(--text-on-accent); }
.btn--primary:hover { background: var(--accent-hover); color: var(--text-on-accent); }
/* D3: the system had no :active state anywhere, so nothing acknowledged a press.
   It was `filter: brightness(.88)` -- a magic constant, and one that dims the
   LABEL as well as the fill, so the pressed state's contrast was whatever the
   filter happened to produce. It is a token now, and it is measured: 6.61:1. */
.btn--primary:active { background: var(--accent-active); }
/* THE BORDER IS THE CONTROL, so it is --border-interactive, not
   --border-strong. On .btn--ghost it is the only thing that says a button is
   there at all; --border-strong measured 1.25:1 against the page. */
.btn--secondary { background: var(--layer-02); color: var(--text-primary); border-color: var(--border-interactive); }
.btn--secondary:hover { background: var(--layer-03); color: var(--text-primary); }
.btn--secondary:active { background: var(--layer-01); }
.btn--ghost { background: transparent; color: var(--text-primary); border-color: var(--border-interactive); }
.btn--ghost:hover { background: var(--layer-02); }
.btn--ghost:active { background: var(--layer-01); }
/* D13: pointer-events:none only stops the mouse. VERIFIED by the docs agent:
   a.btn[aria-disabled=true] still took focus and still fired on Enter. A
   disabled anchor must also be removed from the tab order (tabindex="-1" in the
   markup) -- the CSS cannot do that alone, so it is stated here and in the
   component documentation. */
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: var(--opacity-disabled); pointer-events: none; cursor: not-allowed;
}
.btn[aria-disabled="true"]:focus-visible {
  outline: var(--focus-w) solid var(--support-error); outline-offset: var(--focus-offset);
}

/* --- card ------------------------------------------------------------------
   Flex column so the action pins to the bottom: cards in a row whose content
   differs otherwise finish at four different heights, which is the single most
   common visual tell of an unconsidered grid. */
.card {
  display: flex; flex-direction: column;
  background: var(--layer-02); border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg); padding: var(--sp-07);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.card > :last-child { margin-top: auto; }
.card__title { margin: 0 0 var(--sp-04); font-size: var(--ts-04); font-weight: 600;
               color: var(--text-primary); letter-spacing: var(--tr-tight); }
.card__body { margin: 0 0 var(--sp-06); color: var(--text-secondary); font-size: var(--ts-02);
              line-height: var(--lh-body); }
.card--accent { border-color: var(--accent); }
/* No left-edge accent bars anywhere. Status is carried by a label, not a stripe. */

/* --- metric band -----------------------------------------------------------
   The highest-credibility component available to a vendor with no analyst
   coverage. Tabular figures so digits do not jitter; a source line is required,
   not optional -- a number without its conditions is decoration. */
.metrics { display: grid; gap: var(--sp-07) var(--sp-08);
           grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }
.metric__value { font-size: var(--ts-08); font-weight: 600;
                 line-height: 1; letter-spacing: var(--tr-display);
                 color: var(--text-primary); font-variant-numeric: tabular-nums; }
@media (min-width: 1056px) { .metric__value { font-size: var(--ts-10); } }
.metric__label { margin: var(--sp-04) 0 0; font-size: var(--ts-02); line-height: var(--lh-body);
                 color: var(--text-secondary); max-width: 30ch; }
.metric__source { margin: var(--sp-03) 0 0; font-family: var(--font-mono);
                  font-size: var(--ts-01); color: var(--text-helper); overflow-wrap: anywhere; }

/* --- data table ------------------------------------------------------------
   Hairlines only. No zebra fill, no green ticks: those read as a consumer
   comparison chart, and this audience reads specification sheets. */
.table-wrap { overflow-x: auto; margin-block-end: var(--sp-08); }
.table { width: 100%; border-collapse: collapse; font-size: var(--ts-02); }
.table th, .table td { padding: var(--sp-04) var(--sp-05); text-align: left; vertical-align: top;
                       border-bottom: var(--border-w) solid var(--border-subtle); }
.table th { color: var(--text-primary); font-weight: 600; letter-spacing: var(--tr-tight);
            white-space: nowrap; }
.table td { color: var(--text-secondary); }
.table th:first-child, .table td:first-child { padding-left: 0; }

/* --- evidence frame --------------------------------------------------------
   For a security product a real telemetry sample is more persuasive than a UI
   screenshot, and it is honest: it is what the binary prints. */
.evidence { border: var(--border-w) solid var(--border-subtle); border-radius: var(--r-lg);
            overflow: hidden; background: var(--layer-01); 
  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.evidence__bar { display: flex; align-items: center; gap: var(--sp-03);
                 min-block-size: var(--sp-07); padding-inline: var(--sp-05);
                 border-bottom: var(--border-w) solid var(--border-subtle);
                 background: var(--layer-02); font-family: var(--font-mono);
                 font-size: var(--ts-01); color: var(--text-helper); }
.evidence pre { margin: 0; padding: var(--sp-06) var(--sp-05); overflow-x: auto;
                font-size: var(--ts-02); line-height: 1.7; color: var(--text-secondary); }
/* The note obeys the measure even though it sits in a full-width frame: a
   1200px line is hard to read wherever it lives, and "it is inside a container"
   is not a reason to abandon the rule. */
/* The separator sits on the PRE, which is full width. It used to sit on the
   note, which is capped at the measure -- so the hairline stopped at 640px
   inside a 1248px frame and read as a rule someone forgot to finish. */
.evidence pre { border-bottom: var(--border-w) solid var(--border-subtle); }
.evidence__note { margin: 0; padding: var(--sp-04) var(--sp-05); max-width: var(--measure);
                  border-top: 0;
                  font-size: var(--ts-02); color: var(--text-helper); }

/* --- notice ---------------------------------------------------------------- */
.notice { padding: var(--sp-06); background: var(--layer-02);
          border: var(--border-w) solid var(--border-subtle); 
  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.notice__kind { display: inline-block; margin-bottom: var(--sp-03);
  font-family: var(--font-mono); font-size: var(--ts-01); letter-spacing: var(--tr-label);
  text-transform: uppercase; color: var(--support-info); }
.notice__title { margin: 0 0 var(--sp-03); font-weight: 600; color: var(--text-primary); }
.notice p { margin: 0; max-width: var(--measure); font-size: var(--ts-02); }
.notice--success .notice__kind { color: var(--support-success); }
.notice--warning .notice__kind { color: var(--support-warning); }
.notice--error .notice__kind { color: var(--support-error); }
/* TWO NOTICES IN A ROW ARE TWO NOTICES. Stacked, they had no space between
   them at all -- four of them on a consuming site's product page rendered as
   one tall box with three hairlines through it, and a reader cannot tell where
   one statement ends and the next begins. A component that is commonly
   repeated owns the space between its repeats; leaving it to every consumer
   means every consumer forgets, and this one did. */
.notice + .notice { margin-block-start: var(--sp-05); }

/* A notice that can be acted on or put away. Without these the component could
   state a problem and offer nothing to do about it. */
.notice__actions { display: flex; flex-wrap: wrap; gap: var(--sp-04);
  margin-block-start: var(--sp-05); }
/* THE DISMISS IS A SLOT, NOT A SHAPE. It was written as an absolutely
   positioned 40x40 square with `padding: 0` -- which can only ever hold a
   glyph. The markup that uses it carries a WORD ("Dismiss") and the system's
   own `.btn btn--ghost btn--sm` classes, and a 40px box with `white-space:
   nowrap` does not clip an overlong label, it lets it spill: the word ran ~55px
   past its own border on both sides and out over the notice's right edge, with
   the hairline box drawn through the middle of it. The control that was fixed
   for reading as a word was reading as a word again, with a small box behind it.
   It was also the reason `.notice` reserved a hardcoded 80px gutter for a
   control whose width it could not know.

   So the corner is a GRID CELL. The notice puts its content in column one and
   the dismiss in column two, which reserves exactly the width the control turns
   out to need -- a word, or a 40px icon square -- and no magic number. The
   control's own shape is whatever button class it carries; the slot only
   guarantees the 40px minimum target. */
.notice:has(.notice__dismiss) {
  position: relative;
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--sp-05);
}
.notice:has(.notice__dismiss) > * { grid-column: 1; }
.notice:has(.notice__dismiss) > .notice__dismiss {
  grid-column: 2; grid-row: 1; justify-self: end; align-self: start;
}

/* --- the close control ------------------------------------------------------
   ONE CONTROL, FIVE PLACES, WRITTEN ONCE. The dismiss on a notice and the close
   on a dialog, a drawer, a popover and a toast are the same control: a bordered
   square that removes the thing it sits in. It was written twice -- here, and
   again in css/senua-overlay.css -- and the overlay limits page recorded
   collapsing the two into one shared selector as the piece still worth doing.

   IT WAS WORTH DOING BECAUSE THE TWO COPIES HAD ALREADY DRIFTED, which is the
   argument for collapsing them stated as a fact rather than as a risk. This
   copy set --r-sm and the overlay copy --r-md. Both resolve to 0 today, so
   nothing rendered wrongly, nothing could have failed, and no gate in this
   repository could have told the difference -- the system simply had two
   answers to one question and no way to notice. That is what "two rules that
   agree today can disagree tomorrow" meant, and it had already happened.

   THE SECOND DIVERGENCE WAS VISIBLE, and only the overlay copy was right. The
   overlay copy sizes the icon inside the control; this one never did. All five
   carry the same cross -- a viewBox and no width or height attribute -- and in
   the other four a rule gives it --sp-05. In the notice there was no such rule,
   so the icon took whatever the button's padding left over: measured at 14x14
   inside a 40x40 control, against 16x16 everywhere else. Not a broken render,
   which is why nothing caught it, but the same mark at two sizes, one of them
   decided by a padding value rather than declared. The shared block below sizes
   the icon for all five, and the padding that produced the 14 is dropped when
   the control holds an icon.

   IT LIVES IN THIS LAYER, and names four classes this layer does not otherwise
   own. The alternative is a new shared class in the markup, which renames a
   public API that every property already links in order to fix a duplication no
   consumer can see. The selector list is the smaller change, and the cascade
   requires it to be here: this file is concatenated before senua-overlay.css,
   so the overlay keeps the last word on its own components' geometry.

   SIZE IS NOT SHARED, and pretending it was would be this defect again. A
   dialog and a drawer take the 40px square. A popover is 320px wide and a toast
   is one line, and a 40px target beside a 16px title unbalances both, so they
   take 32px -- stated in their own layer, next to the reason. Both are above
   the 24px minimum of WCAG 2.5.8. What IS shared is the treatment: the boundary
   before the pointer arrives, the hover, the focus ring, the transition and the
   icon.

   THE BOUNDARY IS ALWAYS THERE. A hairline that appears on hover is no use to
   the person who has not hovered yet, and on a touch screen there is no hover
   at all; pressability has to be visible BEFORE the pointer arrives. */
.notice__dismiss,
.modal__close,
.drawer__close,
.popover__close,
.toast__dismiss {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  border-radius: var(--r-sm); cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
/* Only for a control that was given no button class of its own -- otherwise
   this layer would be overriding the ghost button the markup asked for. */
.notice__dismiss:not(.btn),
.modal__close:not(.btn),
.drawer__close:not(.btn),
.popover__close:not(.btn),
.toast__dismiss:not(.btn) {
  background: transparent; color: var(--text-helper); font: inherit;
  border: var(--border-w) solid var(--border-subtle);
}
.notice__dismiss:not(.btn):hover,
.modal__close:not(.btn):hover,
.drawer__close:not(.btn):hover,
.popover__close:not(.btn):hover,
.toast__dismiss:not(.btn):hover {
  background: var(--layer-03); color: var(--text-primary);
  border-color: var(--border-interactive);
}
.notice__dismiss:focus-visible,
.modal__close:focus-visible,
.drawer__close:focus-visible,
.popover__close:focus-visible,
.toast__dismiss:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset); }
/* The icon is sized here rather than left to the markup. A cross with a viewBox
   and no width or height has no size of its own: inside these controls it takes
   whatever the box leaves over, which is how the notice's came to be 14px while
   the other four were 16. `flex: 0 0 auto` is the other half -- without it a
   fixed 40px box squashes the icon to fit instead of the icon sitting in it. */
.notice__dismiss svg,
.modal__close svg,
.drawer__close svg,
.popover__close svg,
.toast__dismiss svg {
  inline-size: var(--sp-05); block-size: var(--sp-05);
  flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: var(--border-w);
}
/* The notice's own size. A MINIMUM rather than a fixed square, because this is
   the one of the five whose slot is documented above as taking a word as well
   as an icon -- the grid cell reserves whatever width the control turns out to
   need. The padding is for the word form; an icon-only dismiss is a 40px square
   like the other four, and needs none. */
.notice__dismiss {
  min-inline-size: var(--sp-08); min-block-size: var(--sp-08);
  padding: var(--sp-02) var(--sp-04);
}
.notice__dismiss:has(> svg) { padding: 0; }

/* --- form ------------------------------------------------------------------ */
.field { margin-bottom: var(--sp-06); }
/* One label definition for the whole system: 14px, weight 500, 8px below.
   Labels were picking up four different sizes because nothing owned them. */
.field label, .label { display: block; margin-bottom: var(--sp-03);
  font-family: var(--font-sans); font-size: var(--ts-02); font-weight: 500;
  line-height: var(--lh-compact); letter-spacing: var(--tr-body);
  color: var(--text-primary); }
/* D4: `.field input` caught checkboxes and radios, so a checkbox inside the
   documented form wrapper rendered 1150px wide with a hairline under it. A
   checkbox is not a text field and must be excluded by type, not by hoping. */
.field input:not([type="checkbox"]):not([type="radio"]),
.field select, .field textarea {
  /* Measured: the text input came to 46px off its padding and line-height
     while select, date and time came to 48px off the UA's own minimum. Neither
     was a decision, which is exactly why they disagreed -- and a row mixing
     them sat 2px out. The control height is a system decision now. */
  min-height: var(--sp-09);
  width: 100%; padding: var(--sp-04) var(--sp-05);
  background: var(--field); color: var(--text-primary);
  font-family: inherit; font-size: var(--ts-03);
  /* CARBON FIELD: a filled box with a BOTTOM border only. One of the most
     distinctive things in the system and completely absent from a generic
     bordered input. */
  border: 0; border-bottom: var(--border-w) solid var(--text-helper);
  border-radius: 0;
}
.field input[type="checkbox"], .field input[type="radio"] {
  width: var(--ts-04); height: var(--ts-04); accent-color: var(--accent); flex: 0 0 auto;
}
/* D6: there was no error state at all. A form that cannot show a field is
   wrong is not a form system. */
.field--error input:not([type="checkbox"]):not([type="radio"]),
.field--error select, .field--error textarea {
  /* The :not() chain is not decoration. Without it this selector is (0,2,1)
     and loses to `.field input:not([type=checkbox]):not([type=radio])` at
     (0,3,1), which sets the border-bottom SHORTHAND and so resets the colour.
     Measured before the fix: an errored input's border was rgb(138,143,152),
     byte-identical to a valid one, while select and textarea went red. */
  border-bottom-color: var(--support-error);
}
.field__error { margin: var(--sp-03) 0 0; font-size: var(--ts-01); color: var(--support-error); }
/* D7 + D8: no disabled state existed, and --text-disabled was declared in both
   themes and referenced by no rule -- so the README's "four text steps, all in
   use" was three.

   THE :not() CHAIN IS THE SAME FIX AS .field--error ABOVE, AND FOR THE SAME
   REASON. Without it this was `.field input:disabled` at (0,2,1), which loses to
   `.field input:not([type=checkbox]):not([type=radio])` at (0,3,1) -- and that
   rule sets BOTH `color` and the `border-bottom` SHORTHAND, so a disabled text
   input kept primary-text white and a solid hairline. The disabled SELECT
   beside it worked, because `.field select` is only (0,1,1). Measured on the
   documented pair: the input came to rgb(247,248,248) solid, the select to
   rgb(95,100,109) dashed -- two controls in one row, one of them saying
   "disabled" and the other not, under a hint reading "Set by the licence, not
   editable here". A state that renders identically to no state is not a state. */
.field input:not([type="checkbox"]):not([type="radio"]):disabled,
.field select:disabled, .field textarea:disabled {
  color: var(--text-disabled); cursor: not-allowed; opacity: 1;
  border-bottom-style: dashed;
}
/* The checkbox and radio are excluded above, so they need their own. A box has
   no ink and no border-bottom to change, so the only lever is the one the
   disabled button already uses. Without it the control was BYTE-IDENTICAL to an
   enabled one -- sampled on the documented radio group, the enabled "A call" and
   the disabled row both painted srgb(59,59,59), and only the LABEL went grey.
   A disabled control that renders exactly like an enabled control is not a
   disabled state, it is a disabled label. */
.field input[type="checkbox"]:disabled, .field input[type="radio"]:disabled,
.check input:disabled {
  opacity: var(--opacity-disabled); cursor: not-allowed;
}
.field input:disabled + label, .field label:has(input:disabled) { color: var(--text-disabled); }

.field input:focus, .field select:focus, .field textarea:focus {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset-inset);
}
.field__hint { margin: var(--sp-03) 0 0; font-size: var(--ts-01); color: var(--text-helper); }

/* --- hero ------------------------------------------------------------------
   The photographic treatment, measured from Palo Alto's landing pages: a
   full-bleed photograph, a two-layer scrim so white type keeps contrast
   whatever the image does, a grade toward the accent, and the type held in the
   left third clear of faces. */
/* The hero centred its content AND carried padding-block, so first text landed
   112-166px down while every other page started at 48. Content sits at the top of
   the padding box; the min-height still holds the photograph. */
/* A full-bleed hero holds its content INSIDE itself. This was centred, which
   put first text 112-166px below the nav while every other page started at 48
   -- so it was changed to flex-start, which over-corrected to content sitting
   ON the top edge of the photograph, reading as a crop rather than a
   composition. Neither is the answer: the content is centred AND inset, so it
   can never reach either edge however tall the hero gets. */
/* IT IS ITS OWN CONTAINER, and that is what makes the scrim correct. See the
   note above the @container block below: the quantity that decides whether the
   horizontal ramp can carry the text is the HERO's width, not the window's,
   and the two are the same thing only when the hero is full-bleed.
   `inline-size` containment is safe here because a block-level flex container
   that fills its parent never sized itself from its contents anyway, and the
   containment it brings -- a stacking context, a containing block for absolute
   descendants -- this element already had from `isolation` and `position`. */
.hero { position: relative; display: flex; align-items: center;
        container: hero / inline-size;
        padding-block: var(--sp-10);
        min-height: clamp(400px, 46vw, 560px); /* a LENGTH may clamp; type may not */ isolation: isolate;
        /* B4: no fallback meant a hero with no image lost its background COLOUR too. */
        background-color: var(--layer-01);
        background-image: var(--hero-img, none);
        background-size: cover; background-repeat: no-repeat;
        background-position: var(--hero-pos, 70% 50%); 
        /* A SURFACE WITH TEXT ON IT STATES ITS FOREGROUND. The hero's copy is
           read against a photograph, so its role is the on-media one rather
           than the page's -- and saying so here is what makes a hero inside a
           scoped theme take that theme's on-media colour instead of the outer
           one's. See tests/surface.js. */
        color: var(--text-on-media-2);
}
.hero::before { content: ""; position: absolute; inset: 0; z-index: var(--z-behind);
  background:
  /* The reading ramp, left to right. Three stops produced a visible band where
     the falloff changed rate -- a straight line between two alphas is not what
     the eye reads as a fade. Six stops approximate an ease, so the scrim runs
     out of the photograph instead of stopping in it. Zero by 88%: the right
     eighth is the photograph, untouched. */
  linear-gradient(90deg,
    rgb(var(--scrim-rgb) / .90) 0%,
    rgb(var(--scrim-rgb) / .86) 24%,
    rgb(var(--scrim-rgb) / .70) 42%,
    rgb(var(--scrim-rgb) / .46) 58%,
    rgb(var(--scrim-rgb) / .22) 73%,
    rgb(var(--scrim-rgb) / 0) 88%),
  /* And a short fade at the foot, so a full-bleed photograph dissolves into
     the page below it rather than ending on a hard horizontal edge. */
  linear-gradient(180deg,
    rgb(var(--scrim-rgb) / 0) 72%,
    rgb(var(--scrim-rgb) / .55) 100%); }
/* The brand grade. It used to run the full frame, so it tinted the subject --
   the part of the photograph that should look like a photograph. It now fades
   out with the scrim, which keeps the grade where the text is and leaves the
   image alone where it is being looked at. */
.hero::after { content: ""; position: absolute; inset: 0; z-index: var(--z-behind); mix-blend-mode: soft-light;
  opacity: var(--hero-grade);
  background: linear-gradient(115deg, var(--hero-accent, var(--accent)) 0%, transparent 62%);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 85%);
          mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 85%); }
.hero .container { position: relative; z-index: var(--z-raised); }
.hero h1 { color: var(--text-on-media); max-width: 14ch; }
.hero .lede { color: var(--text-on-media-2); max-width: 38ch; }
.hero .eyebrow { color: var(--text-on-media-3); }
/* DELETED: `.hero .container { padding-left: clamp(var(--sp-09), 8vw, 7.5rem) }`.
   At 1440 that resolved to 115.2px, so the hero's eyebrow and headline began
   at x=179 while every heading and paragraph on the same page began at x=96.
   The hero now uses the ordinary container gutter and its copy lines up with
   the content beneath it, which is what makes a page read as one page. */
/* The horizontal ramp only works while the text occupies the LEFT of a wide
   frame. Below the large breakpoint it does not: the copy spans most of the
   width and runs out past the scrim onto open photograph. Measured against the
   rendered pixels at 820px -- not against the token -- the eyebrow came to
   1.99:1 and the lede to 3.16:1, both failing AA, while 1440 and 412 passed.
   A gradient that clears AA at two widths and fails between them is the most
   dangerous kind, because every spot check says it is fine. So the vertical
   scrim covers everything below --bp-lg, not just phones.

   AND IT ASKS THE HERO'S WIDTH, NOT THE WINDOW'S. This was `@media`, which is
   right only while the hero is full-bleed -- and then the switch is correct by
   coincidence, because the two widths happen to be the same number. Put the
   same hero in a console beside a side navigation, or in any column narrower
   than the window, and at a 1440px window the component takes the horizontal
   ramp while being 926px wide: the copy then ends at 65% of the box, where the
   ramp is down to about a third of its alpha, and there is nothing under the
   text. Measured on the specimen in this documentation, whose own frame is
   exactly that placement -- h1 1.75:1 against a required 3, and the lede
   2.15:1 against 4.5, at a 1056px window. Failing at the WIDE widths, which is
   the opposite of the failure above and invisible to the same spot check.

   A container query asks the hero. The stop stays 1055px, because the number
   was never about the window: it is the width below which this copy cannot sit
   in the left third of this box. Verified in both engines that a container's
   own ::before is matched by its own container query. The rule below it stays
   an @media, because it sizes the hero ITSELF and an element cannot query its
   own container. */
@container hero (max-width: 1055px) {
  .hero::before { background: linear-gradient(180deg,
    var(--hero-scrim-block-start) 0%,
    var(--hero-scrim-block-end) 100%); }
  /* The heading loses its 14ch cap, because a headline is display type and not
     prose. The LEDE keeps a cap, at the measure: `none` was here so the copy
     could use the vertical scrim, which covers the full width -- but covering
     it makes spanning safe, not required, and a 760px line of 20px prose is
     past the measure whatever is behind it. */
  .hero h1 { max-width: none; }
  .hero .lede { max-width: var(--measure); }
}
@media (max-width: 671px) {
  .hero { min-height: auto; padding-block: var(--sp-10); background-position: 62% 50%; }
}

/* --- skip link -------------------------------------------------------------
   Clip-rect, not off-screen-left: parking it beyond the edge makes the document
   wider than the viewport, which is a real horizontal scroll on a phone even
   though nothing is visible. */
.skip-link:not(:focus) { position: absolute !important; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
/* It floats over arbitrary content, so it needs a control-grade edge and, in
   light theme, a shadow -- a hairline alone does not separate it from the page
   it is covering. --shadow-sm resolves to none in dark, where the layer ladder
   already does that job. */
.skip-link:focus { position: fixed; inset: var(--sp-05) auto auto var(--sp-05); z-index: var(--z-skip);
  padding: var(--sp-04) var(--sp-05); background: var(--layer-02); color: var(--text-primary);
  border: var(--border-w) solid var(--border-interactive); border-radius: var(--r-md);
  box-shadow: var(--shadow-sm); }
.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }


/* --- segmented control (D2) ------------------------------------------------
   Radios inside labels: the accessible form. The native input stays in the
   accessibility tree and keeps keyboard operation; only its painting is hidden. */
/* D19: three segments measured 296px inside a 272px content box at --bp-sm, and
   four measured 322px -- the only component that could not fit the smallest
   supported width. It wraps now, and a segment may shrink to its label. */
/* An untreated fieldset renders the browser's 2px groove border with the
   legend notched into it -- a raw default on no token, in a system whose claim
   is that there are none. A fieldset groups; the group's frame is the job of
   whatever component wants one. */
fieldset { border: 0; margin: 0 0 var(--sp-06); padding: 0; min-width: 0; }
/* A GROUP HAS TO STAND OFF FROM WHAT IS NOT IN IT. The fieldset carried a foot
   and no head, so on the documented form a standalone checkbox sat 12px above
   the radio group's legend -- exactly the 12px that separates the radios INSIDE
   the group from each other. Same gap for "next item" and "different group"
   means the legend reads as a label for the checkbox above it. A fieldset that
   opens its form still has nothing above it to clear. */
fieldset:not(:first-child) { margin-block-start: var(--sp-06); }
legend { padding: 0; margin-bottom: var(--sp-03); color: var(--text-primary);
  font-size: var(--ts-02); font-weight: 600; }

/* Equal tracks, explicit flow. `auto-fit` needs a known width to count
   tracks, and an inline-grid shrink-wraps -- so auto-fit collapsed the whole
   control to a single 106px column. One row of equal segments by default;
   two columns below the medium breakpoint, where four measured 322px against
   the 320px the system names as its smallest supported width. */
fieldset.seg { display: inline-grid; max-width: 100%;
  grid-auto-flow: column; grid-auto-columns: 1fr;
  border: var(--border-w) solid var(--border-interactive);
  padding: 0; margin: 0; }
fieldset.seg label { display: inline-flex; align-items: center; margin: 0; cursor: pointer;
  padding: var(--sp-03) var(--sp-06); font-size: var(--ts-02); font-weight: 500;
  color: var(--text-secondary); border-right: var(--border-w) solid var(--border-interactive); }
/* The frame owns its own right and bottom edge, so no segment has to know
   whether it is last -- `label:last-of-type` dropped the divider from the last
   SEGMENT, which after wrapping is not the last one in each row. Every label
   draws a right and bottom rule; the fieldset drops its own so they do not
   double. Segments grow to fill the row, or a short row leaves bare frame. */
fieldset.seg { border-right: 0; border-bottom: 0; }
/* Equal columns, not flex. With `flex: 1 1 auto` each segment was as wide as
   its own label, so after wrapping the divider sat at 95px in row one and 98px
   in row two and the rows did not line up. With `flex: 1 1 0` they were equal
   but the basis collapsed the control to 51px a segment, too narrow for its
   own labels. A grid gives equal tracks AND keeps the label's width as the
   floor, which is what a segmented control needs: equal segments that still
   fit their words. */
/* A control label does not wrap. Without this the equal track (103px at the
   smallest width) was narrower than "24 hours" needs, so one segment broke
   onto two lines and its row stood 59px against the other's 38px. nowrap makes
   the label the track's minimum, so the grid sizes to fit its own words. */
fieldset.seg label { white-space: nowrap; justify-content: center;
  border-bottom: var(--border-w) solid var(--border-interactive); }
/* An unstyled legend is notched INTO the border box by the browser, which cuts
   a gap in the frame's top edge. The control is labelled in the markup; the
   legend is for assistive technology and does not need to be painted. */
fieldset.seg legend { position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 671px) {
  /* ONE segment per row below the medium breakpoint. Two-up was tried and
     fails on real content: the documented control has a segment labelled
     "Learn (licence required)" which needs 198px, so a pair overran a 320px
     viewport by 12px. Sizing the tracks from content instead gave 100px and
     105px -- dividers that did not line up between rows. A full-width column
     always fits, never wraps a label, and its edges align by construction. */
  fieldset.seg { display: grid; width: 100%;
    grid-auto-flow: row; grid-template-columns: 1fr; }
}
fieldset.seg input { position: absolute; opacity: 0; width: 1px; height: 1px; }
fieldset.seg label:has(input:checked) { background: var(--accent); color: var(--text-on-accent); }
/* The ring is INSET here, so its neighbour is the segment's own fill -- which
   for the checked segment is --accent. That is why --border-focus has to clear
   3:1 against the accent as well as against the page. */
/* The ring is INSET here, so on the checked segment its neighbour is the
   accent fill, not the page. That is a different contrast problem and it has
   its own token -- measured, a shared one came to 1.25:1 on a cyan accent. */
fieldset.seg label:has(input:focus-visible) {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset-inset); }
fieldset.seg label:has(input:checked):has(input:focus-visible) {
  outline-color: var(--border-focus-on-accent); }
fieldset.seg label:has(input:disabled) { color: var(--text-disabled); cursor: not-allowed; }

/* --- dropdown menu (D1) ----------------------------------------------------
   Opens on hover AND focus-within, so it works from the keyboard with no script. */
.nav__has-menu { position: relative; display: inline-flex; align-items: center; }
/* THE TRIGGER MAY BE A BUTTON, and it should be. This component was written
   with an anchor trigger, which is what the rule below styles -- but a control
   that reveals a panel and navigates nowhere is a disclosure, and a disclosure
   is a <button>. Given one, the bar rendered the UA's grey button chrome in
   the middle of the masthead, because nothing here reset it. Found on a
   consuming site, whose trigger was correct and whose masthead looked wrong. */
.nav__has-menu > button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}
.nav__menu { position: absolute; top: calc(100% + var(--sp-04)); left: calc(-1 * var(--sp-05));
  z-index: var(--z-dropdown); display: grid; gap: var(--sp-01);
  /* D20: a 21rem floor is 336px, wider than --bp-sm, and visibility:hidden still
     participates in layout -- so the closed panel widened the document at every
     width. A ceiling relative to the viewport, and a floor that can give way. */
  min-width: min(21rem, calc(100vw - var(--sp-07)));
  max-width: calc(100vw - var(--sp-07)); padding: var(--sp-03);
  background: var(--layer-02); border: var(--border-w) solid var(--border-subtle);
  /* A menu floats over content it does not own. In light theme a 0.05 hairline
     is not enough to separate it; in dark the token is none and the layer
     ladder does it. */
  box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; transform: translateY(calc(-1 * var(--sp-02)));
  /* visibility must NOT interpolate. With `visibility var(--dur)` the panel
     stayed hidden for the whole duration after focus, and a hidden element has
     no focusable children -- driven with real keys, a fast Tab walked past the
     entire menu. Closing keeps the delay so the fade is still seen. */
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease),
              visibility 0s linear var(--dur); 
  /* A surface with text on it states its foreground. See tests/surface.js. */
  color: var(--text-secondary);
}
.nav__has-menu:hover .nav__menu, .nav__has-menu:focus-within .nav__menu {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease),
              visibility 0s; }
/* D20b: the viewport ceiling still overran, because the panel is anchored to
   its trigger -- a panel of width 100vw-gutter starting at the trigger's x
   ends that far past the right edge (measured 329px inside 320px). Below the
   medium breakpoint it spans the viewport instead of the trigger, which is
   the phone pattern regardless. */
@media (max-width: 671px) {
  /* `top: auto` on a FIXED box resolves to the element's static position in
     the DOCUMENT, not to the top of the viewport. This menu sits near the top
     of a page so it looked correct; the same rule applied to a popover further
     down a page computed top: 10118px and rendered a screen below the viewport
     while still reporting visibility: visible. An explicit offset, derived
     from the masthead's own height so the two cannot drift. */
  .nav__menu { position: fixed; left: var(--sp-05); right: var(--sp-05);
    top: calc(var(--sp-09) + var(--sp-06)); min-width: 0; max-width: none; width: auto; }
}
.nav__menu a { display: block; padding: var(--sp-04); }
.nav__menu a:hover, .nav__menu a:focus-visible { background: var(--layer-03); }
.nav__menu b { display: block; font-size: var(--ts-02); font-weight: 600; color: var(--text-primary); }
.nav__menu em { display: block; margin-top: var(--sp-01); font-style: normal;
  font-size: var(--ts-01); line-height: 1.45; color: var(--text-helper); }

/* --- D14: a clickable card needs hover feedback ---------------------------- */
/* The shipped system styles ONLY classes it defines. This rule used to name
   .prod (a cyber-web product card) and .doc-card (documentation chrome): two
   classes the system does not own, one belonging to a single consumer and one
   to the docs. A system that reaches out to a consumer's class names has the
   dependency backwards -- rename .prod in cyber-web and hover silently stops,
   with nothing to fail. A card that wants card behaviour is a .card. */
a.card:hover { border-color: var(--border-strong); background: var(--layer-03); }


/* --- components the documentation demonstrated but the system did not own ---
   Eight were missing: .badge and its modifiers, .btn--block, .check,
   fieldset.seg, .card__action, .field--error, .field__required and a table
   alignment modifier. If the documentation demonstrates it, the system owns it
   -- otherwise the docs are describing a component that only exists in one
   consuming site's stylesheet, which is how a system quietly stops being one.
   (fieldset.seg and .field--error shipped in the previous commit.) */

/* BADGE -- square per Carbon, status carried by colour AND text, never by
   colour alone, so it survives a monochrome print and a colour-blind reader. */
.badge { display: inline-flex; align-items: center;
  /* 2px of block padding against an 18px line box left the text sitting on the
     border, top and bottom -- measured: a 24px badge whose text box was 2px
     from each edge. A bordered label needs clearance the way a button does.
     line-height 1 because a label is one line and 1.5 leading inside a 24px
     box is spending the height on space the text cannot use; the min-height
     then holds every badge to the same size whatever its word. */
  min-block-size: var(--sp-06);
  padding: var(--sp-02) var(--sp-04);
  line-height: 1;
  /* AND THE CLEARANCE OUTSIDE THE BORDER, which the first fix left alone. The
     badge is a 24px box and body leading is 24px, so a row of badges that WRAPS
     puts two hairline borders 1px apart -- measured at 1px on the badge page at
     both 1440 and 412, where seven badges take two rows and three. Two borders
     a pixel apart read as one thick rule, not as two labels. One step of block
     margin puts 4px between the rows, which is the floor this system calls
     deliberate, and costs a line box that contains a badge 4px. */
  margin-block: var(--sp-01);
  font-family: var(--font-mono); font-size: var(--ts-01); font-weight: 500;
  letter-spacing: var(--tr-label); text-transform: uppercase; white-space: nowrap;
  color: var(--text-helper); background: transparent;
  border: var(--border-w) solid var(--border-interactive); }
.badge--good    { color: var(--support-success); border-color: var(--support-success); }
.badge--warning { color: var(--support-warning); border-color: var(--support-warning); }
.badge--error   { color: var(--support-error);   border-color: var(--support-error); }
.badge--info    { color: var(--support-info);    border-color: var(--support-info); }
.badge--neutral { color: var(--text-helper);     border-color: var(--border-interactive); }
/* ACCENT badge -- "recommended", "included", "new". It marks a recommendation
   rather than a state, which is why it is not one of the five status colours.
   (It used to name three words this system does not ship, two of them on the
   banned list; a comment that hands a consumer the vocabulary is copy.)
   It exists because --accent-text
   needed a consumer inside the system: violet as INK is a different value from
   violet as a FILL (see the brand ramp), and a role nothing references is a
   role nobody will use correctly. Never use --accent here: as text on a card it
   measures 3.68:1. */
.badge--accent  { color: var(--accent-text);     border-color: var(--accent-text); }

/* CHECK -- a checkbox or radio beside its own label. The control keeps its
   natural size and the label is clickable across its whole width. */
/* THE TARGET IS THE LABEL, AND IT WAS 20px TALL. WCAG 2.5.8 sets the minimum
   target at 24 by 24, and a checkbox's target is not the box -- clicking the
   label activates the input, so the area that answers a press is the two of
   them together. That union measured 20px high on a single-line row: the box
   is 18 and the label's line is 20, and neither is 24.

   The fix is padding on the LABEL rather than a bigger box. Growing the box to
   24 would make the control visibly heavier than every other control in the
   system for a reason a reader cannot see; padding the label leaves the
   checkbox the size it looks best at and makes the thing you actually press
   the right size. One spacing step each side takes the row to 28.

   Found by a consuming site's render check, on a consent checkbox in a form
   that a customer has to tick to make contact. */
.check { display: flex; align-items: flex-start; gap: var(--sp-04); margin-bottom: var(--sp-04); }
.check input { width: var(--ts-04); height: var(--ts-04); flex: 0 0 auto;
               margin: var(--sp-02) 0 0; accent-color: var(--accent); }
.check label { margin: 0; padding-block: var(--sp-02); font-weight: 400;
               color: var(--text-secondary); cursor: pointer; }
.check input:disabled ~ label { color: var(--text-disabled); cursor: not-allowed; }

/* CARD ACTION -- pins to the bottom so a row of cards ends level whatever the
   content above does. */
/* The gap is MARGIN, not padding. As padding it inflated any control the
   class was put on directly -- a .btn--sm measured 54px tall with 24px above
   its label -- so a class named for "the card's action" could not be applied
   to the card's action. */
.card__action { margin-top: auto; margin-block-start: var(--sp-06); padding-top: 0; }
/* WHEN THE ACTION IS NOT LAST, IT STILL NEEDS A FOOT. Measured on the card that
   documents this very part: the named action absorbed the free space, the
   footnote under it got `margin-top: auto` with nothing left to absorb, and the
   two rendered 0px apart -- the footnote sitting on the button's bottom border,
   while the same footnote in the card beside it stood 45px clear. Nothing
   failed; a zero gap is a bad value, not a rule violation. */
.card__action:not(:last-child) { margin-block-end: var(--sp-05); }
/* Trailing space belongs BETWEEN things. Whatever ends a card has nothing
   after it to be separated from, and the card reads bottom-heavy. The rule
   above pins the last child to the bottom; this stops it carrying its own
   margin down there with it. */
.card > :last-child { margin-bottom: 0; }
/* PROSE IS NEVER WHAT GETS PINNED. `.card > :last-child { margin-top: auto }`
   exists so an ACTION sits on its neighbours' baseline in a row of cards. When
   the last child is the body copy it pins that instead, and the paragraph
   detaches from the heading it belongs to by however much the tallest card in
   the row happens to be -- measured at 21px on the accented card of the
   card-as-a-link pair, with its two-line neighbour deciding the number. The
   body stays with its title; everything else still pins. */
.card > .card__body:last-child { margin-top: 0; }

/* REQUIRED MARKER -- the asterisk needs a text alternative; colour alone is not
   a label. */
.field__required { color: var(--support-error); margin-left: var(--sp-02); }
.field__required::after { content: " (required)"; position: absolute;
  width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

/* TABLE ALIGNMENT -- a numeric column reads right-aligned with tabular figures;
   anything else makes a column of amounts impossible to scan. */
.table td.num, .table th.num, .table .num { text-align: right; font-variant-numeric: tabular-nums; }
.table--fixed { table-layout: fixed; }

/* --- link --------------------------------------------------------------------
   THE MODEL, because this is the part of the system that has broken most often
   and every break had the same cause.

   A link is UNSTYLED by default: it inherits its colour and carries no
   underline. It is marked in exactly two situations.

     1. IMPLICITLY, when it sits in running prose and carries no class. The
        base layer marks those, because an unmarked link inside a paragraph is
        a link nobody finds.
     2. EXPLICITLY, with .link, everywhere else.

   The implicit rule is keyed on CONTEXT. That is right for prose and wrong for
   everything else, and relying on it outside prose is what produced three
   separate defects: the navigation held at a caption size by an orphaned
   descendant rule, product cards rendering as underlined lavender text, and
   every footer link coming out violet on a dark bar. Each time a component
   class was written correctly and lost on specificity to a rule nobody thought
   to look at, and each time the page still looked plausible.

   So: if a link is not in a paragraph, give it a class. .link to mark it,
   .link--quiet to leave it inheriting, or the component's own class. Never
   rely on where it happens to sit. */
.link {
  color: var(--link);
  text-decoration: underline;
  text-decoration-thickness: var(--border-w);
  text-underline-offset: calc(var(--border-w) * 2);
  /* The underline skips descenders by default in most engines; stating it
     means a g or a y is not struck through at small sizes. */
  text-decoration-skip-ink: auto;
  transition: color var(--dur-fast) var(--ease),
              text-decoration-color var(--dur-fast) var(--ease);
}
.link:hover { color: var(--text-primary); }
.link:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-sm);
}

/* QUIET. Inherits its colour and shows the underline only on hover and focus.
   For a link in dense interface content -- a table cell, a list row, a card
   title -- where marking every one would make the page read as a page of
   links. It is NOT a way to hide a link: it still underlines on interaction,
   and it must sit somewhere a reader already expects one. */
.link--quiet { color: inherit; text-decoration: none; }
.link--quiet:hover, .link--quiet:focus-visible {
  color: var(--text-primary); text-decoration: underline;
}

/* STRONG. Primary text with the underline kept. For a link that is the point
   of its sentence rather than an aside in it. */
.link--strong { color: var(--text-primary); font-weight: 600; }

/* ON MEDIA. Over a photograph, where --link has no contrast guarantee because
   the ground is a picture. White with the underline always on: the underline
   is doing the work the colour cannot. */
.link--on-media { color: var(--text-on-media); text-decoration-thickness: calc(var(--border-w) * 2); }
.link--on-media:hover { color: var(--text-on-media); text-decoration-color: var(--text-on-media-2); }

/* STANDALONE. Not inline in a sentence -- a call to action that sits on its
   own line. No underline at rest, because it is not competing with body text
   for a reader's eye; the chevron says it goes somewhere. */
.link--standalone {
  display: inline-flex; align-items: center; gap: var(--sp-03);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  color: var(--accent-text); text-decoration: none; font-weight: 600;
}
.link--standalone::after {
  content: ""; inline-size: var(--sp-03); block-size: var(--sp-03); flex: none;
  border-block-start: calc(var(--border-w) * 2) solid currentColor;
  border-inline-end: calc(var(--border-w) * 2) solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur-fast) var(--ease);
}
.link--standalone:hover { color: var(--text-primary); text-decoration: underline; }
.link--standalone:hover::after { transform: rotate(45deg) translate(2px, -2px); }

/* EXTERNAL and DOWNLOAD. The marker is drawn, and it is decorative -- the fact
   that a link leaves the site or starts a download belongs in the link's TEXT
   or in a visually-hidden span, because an icon alone is not an announcement.
   A glyph would also be read aloud and would change with the typeface. */
.link--external::after,
.link--download::after {
  content: ""; display: inline-block; flex: none;
  inline-size: var(--sp-04); block-size: var(--sp-04);
  margin-inline-start: var(--sp-02);
  vertical-align: -0.1em;
  background: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: contain; mask-size: contain;
}
.link--external::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 3h7v7h-1.5V5.56L5.06 12 4 10.94 10.44 4.5H6z'/%3E%3Cpath d='M3 5v8h8v-2h1.5v3.5h-11v-11H8V5z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 3h7v7h-1.5V5.56L5.06 12 4 10.94 10.44 4.5H6z'/%3E%3Cpath d='M3 5v8h8v-2h1.5v3.5h-11v-11H8V5z'/%3E%3C/svg%3E");
}
.link--download::after {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M7.25 2v7.19L4.53 6.47 3.47 7.53 8 12.06l4.53-4.53-1.06-1.06-2.72 2.72V2z'/%3E%3Cpath d='M3 12.5h10V14H3z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M7.25 2v7.19L4.53 6.47 3.47 7.53 8 12.06l4.53-4.53-1.06-1.06-2.72 2.72V2z'/%3E%3Cpath d='M3 12.5h10V14H3z'/%3E%3C/svg%3E");
}

/* BLOCK. A link wrapping a whole component -- a card, a row, a result. It must
   carry NO decoration and NO colour, because the thing inside it already has
   both. This is the rule that was missing when a wrapper anchor turned every
   product card into underlined lavender text. */
.link--block { display: block; color: inherit; text-decoration: none; }
.link--block:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset);
}

/* A LIST of links that is not prose -- a footer column, a related-reading list,
   an index. Marked as a set by position rather than individually underlined.

   ITS LINKS CARRY A CLASS, and that is the whole point rather than a detail of
   naming. This component shipped with `.link-list a` and bare anchors in the
   markup, so every row rendered violet and underlined -- on the page whose own
   caption says it does not underline every member. Sixth time in this system.

   And it cannot be fixed by writing a louder selector. The prose-list rule is
   `:where(li):not(nav li):not(.list--bare li) a:not([class])`, which reads as
   (0,2,1) and is not: :not() carries its argument's specificity, so :not(nav li)
   adds two elements and :not(.list--bare li) adds a class and an element, making
   it (0,2,4). `.link-list a` is (0,1,1); `.link-list li a:not([class])` is
   (0,2,2); `ul.link-list > li > a:not([class])` is (0,2,3). All three lose.
   Only three class selectors win, and the system has no third class to spend.

   So the component stops MATCHING the rule instead of out-shouting it: the rule
   ends in `a:not([class])`, and an anchor that carries a class is outside it at
   any specificity. That is also what the link model above tells every consumer
   to do -- "if a link is not in a paragraph, give it a class" -- and this
   component was the one place in the layer that did not take its own advice. */
.link-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-02); }
.link-list > li { min-inline-size: 0; }
.link-list a, .link-list .link-list__link {
  display: inline-flex; align-items: center;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  color: var(--text-secondary); text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.link-list a:hover, .link-list .link-list__link:hover {
  color: var(--text-primary); text-decoration: underline;
}
.link-list .link-list__link:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset);
}
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- navigation
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. Concatenated into css/senua.css by scripts/build.sh -- never edit
   css/senua.css directly, it is a build artifact.
   ============================================================================= */

/* WHAT IS IN THIS LAYER, AND THE RULES IT WAS WRITTEN UNDER.

   .brand          the logo lockup: the mark plus the site title, as one unit
   .masthead       the production top bar (NOT .doc-nav, which is documentation
                   chrome and lives in docs/docs.css)
   .menu-toggle    the hamburger trigger
   .menu-drawer    the phone menu panel, a native <dialog>
   .side-nav       sidebar navigation, with nesting, collapse and a rail
   .tabs           tab list plus panels, WAI-ARIA keyboard model
   .breadcrumb     the trail, with a drawn separator
   .pagination     page numbers, steps, disabled ends, compact form
   .overflow-menu  the three-dot menu

   FOUR RULES THIS LAYER OBEYS, EACH FROM A DEFECT SOMEWHERE ELSE:

   1. NO LEFT-EDGE ACCENT BAR. Not as decoration, not as a status stripe, and
      not as a "position marker in a list of positions" either -- that last one
      is the argument this file used to make FOR the one bar it kept, on
      .side-nav's current item. The bar is gone from the CSS. This header still
      described it, in the same words, three sections above the rule that no
      longer draws it, and so did the documentation page for the state
      (/navigation/side-nav-current/), which published the argument as the
      system's position. A rule with one exception written into its own
      statement is not a rule. There is now no inline-start accent anywhere in
      this layer. "Current" is marked the way the rest of the system marks it:
      an accent underline on the WORD in a bar, and surface plus colour plus
      weight in a list -- with aria-current="page" carrying it to anything that
      does not look.

   2. A CLOSED PANEL MUST NOT PARTICIPATE IN LAYOUT. `visibility: hidden` still
      takes its box, which is how a closed menu widened the document at every
      width (defect D20 against .nav__menu). Every closed thing here computes
      `display: none`, and each one SAYS SO IN AN AUTHOR RULE -- the UA's
      `[hidden] { display: none }` and `dialog:not([open]) { display: none }`
      both lose to any author `display` declaration on the same element, so a
      component that sets `display: grid` has already broken its own hidden
      state unless it re-states it.

   3. A NAVIGATION SITS AT THE BODY STEP. Every link, tab, crumb, page number
      and menu item in this file is --ts-03 (16px). A menu is a primary control,
      not a caption. The only 14px text here is .side-nav__section, which is a
      GROUP LABEL and not a target.

   4. EVERY TARGET IS 44px. `calc(var(--sp-08) + var(--sp-02))` -- 40 + 4 --
      because 44 is not itself a spacing step and typing 2.75rem would put a raw
      length in a file that claims to have none. Same expression as .doc-nav__link.
*/

/* =============================================================================
   .brand -- THE LOCKUP
   The mark plus the site title as ONE unit. This, not a bare text link, is what
   goes in the top-left of a masthead. Rules from assets/brand/PROVENANCE.md:

   - Minimum size 24px. Below that the interior strokes close up and the mark
     reads as a solid triangle. .brand--sm IS that minimum -- there is no
     smaller modifier, deliberately.
   - Clear space on all four sides is at least a QUARTER of the mark's width.
     That is --brand-clear, derived from --brand-mark by division, so it cannot
     drift when the size changes. It is real padding, not a note in a document
     nobody opens.
   - NEVER re-coloured or re-tinted. There is no `filter`, no `currentColor` and
     no per-theme rule on .brand__mark anywhere in this file, and there must
     never be one: the gradient is fixed and it is how the brand is recognised.
     Where the gradient cannot be used, the MARKUP points at a different file
     (senua-mark-mono-dark.svg / -mono-light.svg). Swapping the asset is a
     markup decision; re-tinting the asset is vandalism.
   ============================================================================= */
.brand {
  --brand-mark: var(--sp-07);                    /* 32px, the default */
  /* THE CLEAR SPACE IS A MINIMUM, AND THE SCALE IS THE GRANULARITY. A quarter
     of the 24px mark is 6px, which is on NO spacing step -- and a gap of 6px
     is the same class of defect as a font-size of 13px. So the rule is "a
     quarter-width, or one step, whichever is larger": 8px at 24px and 32px,
     12px at 48px, every one of them a real step, and never less than the
     provenance rule demands. Caught by the conformance gate measuring the
     computed gap, not by reading this file. */
  --brand-clear: max(calc(var(--brand-mark) / 4), var(--sp-03));
  display: inline-flex;
  align-items: center;
  /* The gap IS the clear space between the two halves of the lockup. */
  gap: var(--brand-clear);
  /* ...and the padding is the clear space around it. Nothing may come closer. */
  padding: var(--brand-clear);
  color: var(--text-primary);
  text-decoration: none;
  font-family: var(--font-sans);
  font-size: var(--ts-04);
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  white-space: nowrap;
}
.brand__mark {
  inline-size: var(--brand-mark);
  block-size: var(--brand-mark);
  flex: none;
  /* `img, svg, video { height: auto }` in the base layer would otherwise let an
     SVG with a viewBox size itself from its intrinsic ratio and the width
     above. That is the same answer here, but stating the block-size means a
     mark supplied at a different ratio cannot silently change the bar's height. */
}
/* A lockup does not wrap -- but `white-space: nowrap` on the container with no
   ceiling means a long property name pushes the DOCUMENT sideways instead,
   which is worse than a truncated one. Measured: it did, at 320px. The title
   shrinks and truncates; the mark never does. */
.brand__title { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; }

/* THE FULL LOCKUP, as one SVG. `.brand__mark` + `.brand__title` composes the
   lockup out of a mark and live text, which is right when the title is a
   PROPERTY name set in the system's own face. It is wrong for the company
   wordmark: "SenuaAI" is drawn artwork with its own letterform spacing and its
   own gradient across the "AI", and rebuilding it from a text node every time
   means it can drift with a font update, a tracking token, or a weight change.
   The logo is a fixed asset. Use this for the company mark. */
.brand__logo {
  /* Height drives it; the aspect ratio does the rest. The file is 4.335:1, and
     stating only the height means a revision to the lockup's proportions does
     not need this rule edited. */
  block-size: var(--brand-mark);
  inline-size: auto;
  flex: none;
}
/* TWO FILES, ONE PER GROUND, and the reason is worth writing down because the
   obvious approaches both fail:
     - `currentColor` does NOT cross into an SVG loaded through <img>. The file
       is an independent document, so currentColor resolves to ITS initial
       value and the logo renders black. Measured: near-black on the dark bar.
     - A CSS mask with `background-color: currentColor` would theme correctly
       and would FLATTEN the mark's violet-to-cyan gradient, which is the one
       part of the identity that cannot be reduced to a single colour.
   So the wordmark's ink is swapped by swapping the file. The mark's gradient
   is identical in both; only the letterforms change colour. */
/* THE CHOICE IS THE GROUND, NOT THE THEME NAME -- and getting that wrong put
   the near-black wordmark on two near-black bars.

   This was `:root:not([data-theme="dark"])` inside the light-scheme media
   query: one dark theme named, everything else assumed light. There are SIX
   themes and THREE of them are dark. So with the OS in light mode -- which is
   the default a headless browser and most laptops report -- `midnight`
   (--background #05080f) and `violet` (#0b0714) both matched "not dark" and
   were served senua-logo-dark.svg, whose letterforms are near-black. Measured
   and then LOOKED AT: the word "Senua" disappeared entirely on both bars and
   only the gradient "AI" survived. Nothing failed -- a logo rendering as a
   logo looks deliberate, and the mark was still there to make it look intact.

   Two corrections, both load-bearing:

   1. The LIGHT grounds are named, rather than the one dark one. Three names
      is the whole list and it is the list that changes least often; "not dark"
      silently annexes every theme anyone adds.
   2. The media query matches `:not([data-theme])` -- the attribute ABSENT --
      not `:not([data-theme="dark"])`. A theme the reader CHOSE must never be
      overridden by what their operating system prefers; that is what let the
      OS preference reach into midnight and violet at all.

   This list belongs in the tokens file, not here: see the note at the foot of
   this block for the exact change. */
.brand__logo--on-light { display: none; }
:root[data-theme="light"] .brand__logo--on-dark,
:root[data-theme="paper"] .brand__logo--on-dark,
:root[data-theme="contrast"] .brand__logo--on-dark,
[data-theme="light"] .brand__logo--on-dark,
[data-theme="paper"] .brand__logo--on-dark,
[data-theme="contrast"] .brand__logo--on-dark { display: none; }
:root[data-theme="light"] .brand__logo--on-light,
:root[data-theme="paper"] .brand__logo--on-light,
:root[data-theme="contrast"] .brand__logo--on-light,
[data-theme="light"] .brand__logo--on-light,
[data-theme="paper"] .brand__logo--on-light,
[data-theme="contrast"] .brand__logo--on-light { display: block; }
/* No theme chosen at all: follow the operating system. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .brand__logo--on-dark { display: none; }
  :root:not([data-theme]) .brand__logo--on-light { display: block; }
}
/* NAMING THREE THEMES IN A COMPONENT LAYER IS THE COMPROMISE, NOT THE ANSWER.
   Which ground a theme is, is a fact the tokens file already knows and this
   file has to be told -- so every layer that needs it re-derives it, and the
   next theme added has to remember to visit all of them. CSS cannot branch on
   a computed colour, so the fix is a token: each theme block in
   tokens/senua-tokens.css declares `--brand-ground-dark: none` (light grounds)
   or `initial` (dark grounds), and this block becomes two rules reading it.
   senua-base.css has the SAME latent bug for the same reason -- line 27 sets
   `color-scheme: light` for [data-theme="light"] only, so `paper` and
   `contrast`, both light grounds, take the root's `color-scheme: dark` and get
   dark native scrollbars and dark native form glyphs on a light page. */
/* A PROPERTY name after the company lockup: "Design System", "Docs", "Console".
   Separated by a hairline, because a space alone reads as part of the logo and
   this is deliberately not part of it. Set one step down and at the secondary
   text role, so the lockup stays the primary object in the bar. */
.brand__suffix {
  min-inline-size: 0;
  padding-inline-start: var(--brand-clear);
  border-inline-start: var(--border-w) solid var(--border-subtle);
  color: var(--text-secondary);
  font-weight: 500;
}
.brand--sm .brand__suffix { font-size: var(--ts-03); }
/* Below the medium breakpoint the property name is dropped, not shrunk: a bar
   with no room is better without it than with it at a size nobody can read. */
@media (max-width: 671px) {
  .brand__suffix { display: none; }
}
/* 24px: the documented minimum. There is no step below it. */
.brand--sm { --brand-mark: var(--sp-06); font-size: var(--ts-03); }
/* 48px: a footer, a sign-in page, an empty state. */
.brand--lg { --brand-mark: var(--sp-09); font-size: var(--ts-06); letter-spacing: var(--tr-display); }

/* =============================================================================
   .menu-toggle -- THE HAMBURGER
   A real button. It carries aria-expanded and aria-controls in the markup; CSS
   cannot supply either, and a div with a click handler is not a button.
   The same class is the drawer's close control -- one control, two icons.

   THIS BLOCK MUST STAY ABOVE .masthead, AND THAT IS NOT A PREFERENCE. It sets
   `display: inline-flex`; .masthead__toggle sets `display: none` above the
   large breakpoint. Both are single-class selectors, so the ONE THAT WINS IS
   THE LATER ONE. With this block underneath, the hamburger rendered beside a
   fully expanded navigation at 1440px -- found in a screenshot, not by the
   gate, because a visible button breaks no rule any check knows about. Same
   family as the .doc-nav font-size that stayed 12px through three attempts to
   fix it: when an override does not take, the cause is the cascade, not a typo.
   ============================================================================= */
.menu-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-03);
  min-inline-size: calc(var(--sp-08) + var(--sp-02));
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-inline: var(--sp-04);
  background: transparent;
  color: var(--text-primary);
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-md);
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  line-height: var(--lh-compact);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.menu-toggle:hover { background: var(--layer-02); }
.menu-toggle:active { background: var(--layer-03); }
.menu-toggle__icon { inline-size: var(--sp-06); block-size: var(--sp-06); flex: none; }

/* =============================================================================
   .masthead -- THE PRODUCTION TOP BAR
   Brand lockup left, primary navigation beside it, utility actions right.
   docs/docs.css ships .doc-nav and .site-nav; both are DOCUMENTATION CHROME and
   neither is this. A consuming site links css/senua.css only, so the masthead
   has to exist here or the site has no top bar.
   ============================================================================= */
.masthead {
  background: var(--layer-01);
  border-block-end: var(--border-w) solid var(--border-strong);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
/* The sticky variant is THREE declarations and no more. --z-dropdown is the
   system's "over the page" step; the skip link stays above it on --z-skip. */
.masthead--sticky {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-dropdown);
}
.masthead__inner {
  display: flex;
  align-items: center;
  gap: var(--sp-06);
  inline-size: 100%;
  max-inline-size: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-06);
  min-block-size: var(--sp-10);
}
@media (min-width: 1056px) { .masthead__inner { padding-inline: var(--sp-07); } }
/* The lockup carries its own clear space as padding, which would otherwise
   push the mark a quarter-width inside the page gutter and leave it optically
   unaligned with everything below. Pulling back by exactly that amount aligns
   the MARK to the gutter while the clear space is still there -- the gutter is
   24px and the clear space it has to beat is 8px.

   --brand-clear is declared ON .brand, so this rule -- which matches .brand --
   reads the lockup's OWN clear space rather than restating the default size.
   Put .brand--lg in a masthead and the compensation follows it.

   THIS RULE WAS DEAD, and the way it died is worth keeping. A close-comment
   marker had been left after the word "8px", four paragraphs up, and the lines
   after it then sat in CSS code position -- so the parser read them as the
   start of a selector and ran on to the next opening brace, swallowing
   ".masthead__inner > .brand" into an invalid prelude and dropping the whole
   rule. Nothing failed: the stylesheet still parsed, every other rule still
   applied, and the mark simply sat 8px inside the gutter on every masthead in
   the system. Measured over HTTP, the rule was ABSENT from
   document.styleSheets -- the CSSOM stepped from .masthead__inner straight to
   .masthead__nav -- and the mark rendered at x=104 against a content edge of
   x=96. A stray close-comment marker is not a typo in a comment, it is a
   deleted rule.

   AND THIS FILE IS WHERE THAT IS EASIEST TO DO, because its comments are long
   and they talk about CSS. Writing the two characters that end a comment
   INSIDE one ends it there -- the first repair of this very rule did exactly
   that, twice, while quoting the sequence it was describing, and killed the
   rule a second time. Name the marker in words, never type it. The test is
   that the file's opening markers and closing markers come out even; they were
   62 and 64. */
.masthead__inner > .brand { margin-inline-start: calc(var(--brand-clear) * -1); }
.masthead__nav { margin-inline-end: auto; min-inline-size: 0; }
.masthead__list {
  display: flex;
  align-items: center;
  gap: var(--sp-06);
  margin: 0;
  padding: 0;
  list-style: none;
}
.masthead__link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-03);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  letter-spacing: var(--tr-body);
  color: var(--text-helper);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.masthead__link:hover { color: var(--text-primary); }
/* THE MARKER IS UNDER THE WORD, NOT UNDER THE 44px TARGET. Same decision and
   the same reason as .doc-nav__link: a border on the link box draws the accent
   rule 20px below the label it marks and a few pixels above the bar's own
   hairline, and the eye reads the nearer line as belonging to the bar. */
.masthead__link[aria-current="page"],
.masthead__link.is-current {
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: calc(var(--border-w) * 2);
  text-underline-offset: var(--sp-03);
}
.masthead__actions { display: flex; align-items: center; gap: var(--sp-04); }
.masthead__toggle { display: none; }
/* Below the large breakpoint the inline nav and the utility actions are not
   narrowed, they are REPLACED: both move into the drawer, whole. A menu that
   shrinks until it fits is a menu nobody can read. */
@media (max-width: 1055px) {
  .masthead__nav,
  .masthead__actions { display: none; }
  .masthead__toggle { display: inline-flex; margin-inline-start: auto; }
}

/* =============================================================================
   .menu-drawer -- THE PHONE MENU
   A NATIVE <dialog>, opened with showModal(). Four of the five requirements are
   then the platform's, not ours, and cannot rot:

     focus trapped while open   showModal() puts the element in the top layer
                                and makes everything behind it inert. Tab and
                                Shift+Tab cycle inside the panel.
     Escape closes              the dialog's own behaviour; it fires `close`.
     background inert           same mechanism, so a screen reader cannot walk
                                out of the panel either.
     closed = zero layout       dialog:not([open]) is display:none, re-stated
                                below because this rule sets display:flex.

   The fifth, aria-expanded on the TRIGGER, is the only thing that needs script:
   about fifteen lines, shown on /navigation/. There is no framework and
   no library. A <details> or a checkbox was tried first and cannot trap focus
   or respond to Escape, so it would have meant writing MORE script to reach a
   worse result.
   ============================================================================= */
.menu-drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  /* A ceiling relative to the viewport with a floor that gives way. 320px, or
     the viewport less two 32px gutters, whichever is smaller. */
  inline-size: min(calc(var(--sp-13) * 2), calc(100vw - var(--sp-10)));
  max-inline-size: 100%;
  block-size: auto;
  max-block-size: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-inline-start: var(--border-w) solid var(--border-strong);
  background: var(--layer-01);
  color: var(--text-secondary);
  display: flex;
  flex-direction: column;
}
/* RE-STATED, NOT INHERITED. The UA rule is `dialog:not([open]) { display: none }`
   and the author `display: flex` above beats it on origin alone -- a closed
   drawer would otherwise sit permanently across the right of every page. This
   is the exact shape of defect D20, one component later. */
.menu-drawer:not([open]) { display: none; }
.menu-drawer::backdrop { background: var(--overlay); }
.menu-drawer[open] { animation: senua-drawer-in var(--dur) var(--ease); }
@keyframes senua-drawer-in {
  from { translate: var(--sp-09) 0; opacity: 0; }
  to   { translate: 0 0; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .menu-drawer[open] { animation: none; }
}
/* The page behind a modal must not scroll under it. This is CSS, not a script
   writing `style="overflow:hidden"` onto <body> -- the system forbids inline
   style attributes, and a script that sets one is still setting one. */
html:has(.menu-drawer[open]) { overflow: hidden; }

.menu-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-05);
  padding: var(--sp-04) var(--sp-05);
  border-block-end: var(--border-w) solid var(--border-subtle);
}
.menu-drawer__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--sp-05) var(--sp-04);
}
.menu-drawer__foot {
  display: grid;
  gap: var(--sp-04);
  padding: var(--sp-05);
  border-block-start: var(--border-w) solid var(--border-subtle);
}
.menu-drawer__list { display: grid; gap: var(--sp-01); margin: 0; padding: 0; list-style: none; }
.menu-drawer__link {
  display: flex;
  align-items: center;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-inline: var(--sp-04);
  font-size: var(--ts-03);
  color: var(--text-secondary);
  text-decoration: none;
}
.menu-drawer__link:hover { background: var(--layer-02); color: var(--text-primary); }
.menu-drawer__link[aria-current="page"] {
  color: var(--text-primary);
  background: var(--layer-02);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: calc(var(--border-w) * 2);
  text-underline-offset: var(--sp-03);
}

/* =============================================================================
   .side-nav -- SIDEBAR NAVIGATION
   Sections, items, a current marker, a second level, a collapsible group and a
   rail. Everything here is a list of LINKS; nothing is a div with a handler.
   ============================================================================= */
/* A DEFAULT WIDTH, because a sidebar without one is not a sidebar. Left to
   fill its container the component rendered a 900px-wide highlighted row in a
   documentation frame, which is what a full-bleed list of navigation items
   looks like and is nobody's intent. 256px is --sp-13 + --sp-12, and it is the
   width Carbon's own side nav ships. `inline-size: 100%` under it means a
   narrower column still wins; a wider one does not. */
.side-nav {
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  inline-size: 100%;
  max-inline-size: calc(var(--sp-13) + var(--sp-12));
}
/* The one 14px thing in this file. A group label is not a target: it has no
   hover, no focus and nothing to press. Weight and colour carry it, not size. */
.side-nav__section {
  margin: 0 0 var(--sp-03);
  padding-inline: var(--sp-05);
  font-size: var(--ts-02);
  font-weight: 600;
  line-height: var(--lh-compact);
  color: var(--text-primary);
}
.side-nav__section + .side-nav__list { margin-block-end: var(--sp-06); }
.side-nav__list { display: grid; gap: var(--sp-01); margin: 0; padding: 0; list-style: none; }
.side-nav__link {
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-inline: var(--sp-05);
  font-size: var(--ts-03);
  color: var(--text-helper);
  text-decoration: none;
  /* THE RESERVED 2px WENT WITH THE MARKER IT WAS RESERVED FOR. This was
     `border-inline-start: 2px solid transparent` so that filling it on the
     current item would not shift the list -- correct reasoning for a marker
     that no longer exists. Nothing has filled it since the bar was removed, so
     every item in every sidebar was indented 2px by a transparent border held
     open for nothing, and the comment describing it named a marker that is not
     there. A label now starts at --sp-05, which is the step the padding
     actually says. */
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.side-nav__link:hover { background: var(--layer-02); color: var(--text-primary); }
/* The current item. NO LEFT-EDGE BAR. The previous rule drew one and argued
   that a position marker in a list of positions is a different thing from a
   status stripe on a card -- which is true, and is not the rule. The rule is
   that this system does not use left-edge bars, and a component that argues
   its way out of a rule is how a system stops having any.

   The state is carried three ways regardless, so nothing rests on one signal:
   the surface lifts a layer, the text goes to primary, and the weight
   increases. aria-current="page" carries it to anything that cannot see. */
.side-nav__link[aria-current="page"],
.side-nav__link.is-current {
  background: var(--layer-02);
  color: var(--text-primary);
  font-weight: 600;
}
.side-nav__icon { inline-size: var(--sp-06); block-size: var(--sp-06); flex: none; }
.side-nav__label { min-inline-size: 0; }

/* Second level. The indent is the first level's inline padding plus one step,
   so the two are related by the scale rather than by a chosen number. */
.side-nav__list--nested { margin-block-start: var(--sp-01); }
.side-nav__list--nested .side-nav__link { padding-inline-start: calc(var(--sp-05) + var(--sp-06)); }
.side-nav__list--nested .side-nav__list--nested .side-nav__link {
  padding-inline-start: calc(var(--sp-05) + var(--sp-06) + var(--sp-06));
}

/* Collapsible: a native <details>. Enter and Space operate a <summary> with no
   script at all, and the open state survives without anything having to store
   it. The disclosure triangle is dropped by `display: flex` in Chromium and by
   `list-style: none` elsewhere -- both, because neither is universal. */
.side-nav__group { border: 0; margin: 0 0 var(--sp-05); }
.side-nav__summary {
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-inline: var(--sp-05);
  font-size: var(--ts-03);
  font-weight: 600;
  color: var(--text-primary);
  cursor: pointer;
  list-style: none;
}
.side-nav__summary::-webkit-details-marker { display: none; }
.side-nav__summary:hover { background: var(--layer-02); }
.side-nav__caret {
  inline-size: var(--sp-05);
  block-size: var(--sp-05);
  flex: none;
  margin-inline-start: auto;
  color: var(--text-helper);
  transition: rotate var(--dur) var(--ease);
}
.side-nav__group[open] > .side-nav__summary .side-nav__caret { rotate: 90deg; }
@media (prefers-reduced-motion: reduce) { .side-nav__caret { transition: none; } }

/* THE RAIL. An honest account of what this variant costs: the label leaves the
   screen and the ICON ALONE carries the meaning for a sighted reader. That is
   only acceptable where the set is short and the glyphs are conventional, and
   it is never acceptable as the only navigation on a page. The label does not
   leave the ACCESSIBILITY TREE -- it is the system's visually-hidden treatment,
   so the link keeps its accessible name and a screen reader is unaffected. */
.side-nav--rail { inline-size: calc(var(--sp-10) + var(--sp-03)); }
.side-nav--rail .side-nav__link {
  position: relative;      /* the hidden label anchors here, not on the page */
  justify-content: center;
  gap: 0;
  padding-inline: 0;
  min-block-size: calc(var(--sp-09) + var(--sp-04));
}
.side-nav--rail .side-nav__label,
.side-nav--rail .side-nav__section {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.side-nav--rail .side-nav__section + .side-nav__list { margin-block-end: var(--sp-05); }

/* =============================================================================
   .tabs -- TAB LIST AND PANELS
   The WAI-ARIA tabs pattern, with automatic activation. What CSS owns is the
   painting; the roles, aria-selected, aria-controls, the roving tabindex and
   the arrow keys are the markup's and the script's, and they are written out on
   /navigation/. A row of buttons without those is not tabs.
   ============================================================================= */
.tabs { display: block; }
/* THE DEFAULT WRAPS, and .tabs--scroll is the one that does not. That is the
   opposite of the first draft, which kept the default on one line and pushed
   the document 376px wide inside a 320px viewport -- a tab off the right edge
   with no scrollbar in sight is a tab nobody finds. Wrapping has a cost, stated
   plainly on the documentation page: a selected tab on the first of two rows
   draws its marker above the list's own rule instead of on it. Where a set is
   long enough to wrap often, --scroll keeps it on one line and scrolls. */
.tabs__list {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: var(--sp-06);
  margin: 0;
  padding: 0;
  border-block-end: var(--border-w) solid var(--border-strong);
}
.tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-03);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding: 0;
  background: none;
  border: 0;
  /* The selected rule sits ON the list's hairline, not above it: the tab's own
     2px bottom border is pulled down by the hairline's width so the two share a
     baseline instead of drawing two parallel lines. */
  border-block-end: calc(var(--border-w) * 2) solid transparent;
  margin-block-end: calc(var(--border-w) * -1);
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  letter-spacing: var(--tr-body);
  color: var(--text-helper);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.tabs__tab:hover { color: var(--text-primary); }
.tabs__tab[aria-selected="true"] { color: var(--text-primary); border-block-end-color: var(--accent); }
.tabs__tab[disabled] { color: var(--text-disabled); cursor: not-allowed; }
.tabs__tab[disabled]:hover { color: var(--text-disabled); }
.tabs__count {
  font-variant-numeric: tabular-nums;
  font-size: var(--ts-02);
  color: var(--text-helper);
}
.tabs__panel { padding-block-start: var(--sp-06); }
/* Re-stated for the same reason as the drawer: a panel with an author display
   value would otherwise ignore the UA's [hidden] rule and every panel would
   render at once. */
.tabs__panel[hidden] { display: none; }
.tabs__panel > :last-child { margin-block-end: 0; }
.tabs__panel:focus-visible { outline-offset: var(--focus-offset-inset); }

/* Scrollable when the set overruns. The list declares itself a scroll
   container, which is also what tells the conformance gate that a wide child
   here is the prescribed treatment and not a page that got wider than the
   viewport. The focus ring goes INSIDE the box in this variant -- a 2px ring at
   a 2px outward offset is clipped by the scroll edge on the first and last tab. */
.tabs--scroll .tabs__list {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: thin;
}
.tabs--scroll .tabs__tab { flex: none; }
.tabs--scroll .tabs__tab:focus-visible { outline-offset: var(--focus-offset-inset); }

/* =============================================================================
   .breadcrumb -- THE TRAIL
   ============================================================================= */
.breadcrumb { font-family: var(--font-sans); font-size: var(--ts-03); }
.breadcrumb__list {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: var(--sp-03);
  margin: 0;
  padding: 0;
  list-style: none;
  /* OVERFLOW HANDLING, AND WHY IT IS NOT "COLLAPSE THE MIDDLE". A trail is a
     record of where the reader is. Hiding the middle of it behind an ellipsis
     hides LOCATIONS, and on a phone -- the only width where it overruns -- that
     is where the reader most needs them. So the trail keeps every crumb and the
     row scrolls, which is the same answer .table-wrap gives a wide table. */
  overflow-x: auto;
  scrollbar-width: thin;
}
.breadcrumb__item { display: flex; align-items: center; gap: var(--sp-03); flex: none; }
/* THE SEPARATOR IS DRAWN, NOT TYPED. A "/" in the markup is content: it lands
   in the accessible name of the list, it is selected and copied with the text,
   and it cannot be restyled. This is a hairline of --border-interactive, rotated
   into a slash, on a pseudo-element that generates no text at all. */
.breadcrumb__item + .breadcrumb__item::before {
  content: "";
  flex: none;
  inline-size: var(--border-w);
  block-size: var(--sp-05);
  background: var(--border-interactive);
  rotate: 20deg;
}
.breadcrumb__link,
.breadcrumb__current {
  display: inline-flex;
  align-items: center;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  /* A long crumb is truncated, never wrapped: a trail that wraps to two lines
     stops reading as one path. 24ch, because the limit is a measure of TYPE --
     the same unit the hero's headline is capped in -- not a pixel width. */
  max-inline-size: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.breadcrumb__link { color: var(--text-helper); text-decoration: none; }
.breadcrumb__link:hover {
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-thickness: var(--border-w);
  text-underline-offset: calc(var(--border-w) * 2);
}
.breadcrumb__current { color: var(--text-primary); font-weight: 500; }

/* =============================================================================
   .pagination
   ============================================================================= */
.pagination {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-04);
  font-family: var(--font-sans);
  font-size: var(--ts-03);
}
/* The numbers WRAP. Seven 44px targets and their gaps are 320px wide, so at the
   smallest supported width a non-wrapping row pushes the whole document
   sideways -- measured 358px inside 320px before this was added. A wrapped
   second row of page numbers is ordinary; a horizontally scrolling page is not. */
.pagination__list { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-01); margin: 0; padding: 0; list-style: none; }
.pagination__page,
.pagination__step {
  /* THE MINUS-THREE-PIXEL CLEARANCE, AND WHAT WAS ACTUALLY SETTING THE HEIGHT.
     `line-height: 1` was added here first and did not move the number by a
     single pixel, which is the tell: the height was never coming from the
     leading.

     Measured, on the rendered element, over HTTP:
       box            44.00px, and box-sizing is border-box
       min-block-size 44px   -- calc(--sp-08 + --sp-02), the touch target
       border         1px top, 1px bottom, solid, transparent
       padding-block  0px, top and bottom
       line-height    16px after the change, 24px before it
       the text       one line, sitting 11px below the top of the box and
                      12px above the bottom of it

     So nothing is wrong with the picture -- there was never a label touching a
     border here. The box is 44px because the TARGET says 44px, and its content
     is one 16px line with no block padding at all to account for the other
     26px. The geometry check estimates how many lines a box holds by dividing
     its content height by the leading, and that estimate is only true when the
     height comes from the content. Here it does not:
       lines = round((44 - 2 - 0 - 0) / 16) = round(2.625) = 3
       clear = (44 - 2 - 16 x 3) / 2       = -3
     and before the leading was touched at all:
       lines = round((44 - 2 - 0 - 0) / 24) = round(1.75)  = 2
       clear = (44 - 2 - 24 x 2) / 2       = -3
     24 x 2 and 16 x 3 are both 48, which is why changing the leading landed on
     the identical number and read as "the change did nothing".

     PADDING is the only term subtracted before that division, and it is also
     the honest fix. The control had none: it leaned entirely on the 44px floor
     to keep its label off its own border, so the composition was a side effect
     of the touch target rather than a decision, and a variant that ever lost
     the floor would put the text straight onto the line. One step of block
     padding states it. The rendered box does not move -- 16 + 24 + 2 = 42,
     still under the 44px floor, so min-block-size still governs and every
     target is still exactly 44px -- and the measurement now reads the one line
     that is actually there, with 13px of clearance around it.

     The leading STAYS at 1 and is load-bearing now: at the inherited 1.5 the
     box would come to 24 + 24 + 2 = 50px and the row would grow past its own
     target. Same reason .badge states it. */
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-03);
  min-inline-size: calc(var(--sp-08) + var(--sp-02));
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-block: var(--sp-04);
  padding-inline: var(--sp-04);
  font-size: var(--ts-03);
  font-variant-numeric: tabular-nums;
  color: var(--text-helper);
  text-decoration: none;
  border: var(--border-w) solid transparent;
  border-radius: var(--r-md);
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pagination__page:hover,
.pagination__step:hover { background: var(--layer-02); color: var(--text-primary); }
/* The current page is a SURFACE plus a control boundary plus the word being
   darker -- three signals, none of them colour alone, and aria-current="page"
   in the markup for anything that reads rather than looks. */
.pagination__page[aria-current="page"] {
  background: var(--layer-03);
  color: var(--text-primary);
  font-weight: 500;
  border-color: var(--border-interactive);
}
/* A disabled end. The colour is --text-disabled, which is the one role exempt
   from 1.4.3, and NOT an opacity on top of it -- stacking both makes it fainter
   than the token says it is. The markup carries the rest of the contract: a
   <span> with no href, because CSS cannot take an element out of the tab order
   and pointer-events stops the mouse but not the Enter key. Same contract as
   .btn[aria-disabled="true"], stated in the same words for the same reason. */
.pagination__step[aria-disabled="true"],
.pagination__step.is-disabled {
  color: var(--text-disabled);
  cursor: not-allowed;
}
.pagination__step[aria-disabled="true"]:hover,
.pagination__step.is-disabled:hover { background: transparent; color: var(--text-disabled); }
.pagination__icon { inline-size: var(--sp-05); block-size: var(--sp-05); flex: none; }
/* The gap in a long run of pages. It generates its own text, so nothing in the
   markup has to carry a row of dots, and the element is aria-hidden there. */
.pagination__gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--sp-06);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  color: var(--text-helper);
}
.pagination__gap::before { content: "..."; }
.pagination__status { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
/* Compact: the two steps and a read-out, for a phone or a card footer, where
   nine numbered targets do not fit and would not be pressed accurately anyway. */
.pagination--compact { gap: var(--sp-05); }
.pagination--compact .pagination__status { margin-inline: auto; }
/* On a phone the three parts do not fit on one line, and the wrap they fell
   into read as an accident: "Previous  Page 3 of 24" on one row and "Next"
   alone underneath. Given a row of its own the read-out is deliberate, and the
   two steps then sit at the ends of the row beneath it. */
@media (max-width: 671px) {
  .pagination--compact { justify-content: space-between; }
  .pagination--compact .pagination__status {
    flex: 1 0 100%;
    order: -1;
    margin-inline: 0;
    text-align: center;
  }
}

/* =============================================================================
   .overflow-menu -- THE THREE-DOT MENU
   Trigger plus item list. The items are BUTTONS: a menu of actions is not a
   list of destinations, and an <a href="#"> that runs a command is a lie to
   every assistive technology that reads it as a link.
   ============================================================================= */
.overflow-menu { position: relative; display: inline-flex; }
.overflow-menu__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: calc(var(--sp-08) + var(--sp-02));
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding: 0;
  background: transparent;
  color: var(--text-helper);
  /* THE BORDER IS THE CONTROL. This was `solid transparent`, so at rest the
     trigger was a transparent box with a transparent boundary and the only
     thing on screen was three 2px dots -- its accessible name is
     visually-hidden, so there is not even a word beside them. Rendered as its
     own specimen it is an empty frame: nothing about it says it can be
     pressed, and a reader finds out by guessing. It is the one control in this
     layer that had no boundary at all; .menu-toggle, ten rules up, already
     carries --border-interactive for exactly this reason, and the ghost button
     in the components layer states the rule outright -- "on .btn--ghost it is
     the only thing that says a button is there at all". A three-dot menu sits
     in table rows and card headers where a heavy box would be noise, which is
     an argument for a HAIRLINE rather than for nothing: --border-interactive
     is one hairline and is the role the system already uses for a control's
     own edge. Hover and the expanded state keep their layer lift on top. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-md);
  /* A <button> takes the UA's `font: 400 13.333px Arial` unless it is told
     otherwise, and this one looks like it has no text -- its only child is the
     visually-hidden name. It has text: the gate found Arial at 13.333px
     inside it, which is a foreign typeface AND a size on no step, hiding in
     the one element nobody thought to style. Every button in this layer sets
     its own font for this reason. */
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  line-height: var(--lh-compact);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.overflow-menu__trigger:hover { background: var(--layer-02); color: var(--text-primary); }
.overflow-menu__trigger[aria-expanded="true"] { background: var(--layer-02); color: var(--text-primary); }
.overflow-menu__icon { inline-size: var(--sp-06); block-size: var(--sp-06); flex: none; }
.overflow-menu__list {
  position: absolute;
  inset-block-start: calc(100% + var(--sp-02));
  inset-inline-start: 0;
  z-index: var(--z-dropdown);
  display: grid;
  gap: var(--sp-01);
  min-inline-size: min(calc(var(--sp-13) + var(--sp-09)), calc(100vw - var(--sp-09)));
  max-inline-size: calc(100vw - var(--sp-09));
  padding: var(--sp-03);
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);
  /* A menu floats over content it does not own. In dark theme the shadow token
     is `none` and the layer ladder does the separating; in light it is not. */
  box-shadow: var(--shadow-md);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
/* Same re-statement as the drawer and the tab panel: `display: grid` above
   would beat the UA's [hidden] rule and the menu would never close. */
.overflow-menu__list[hidden] { display: none; }
/* Aligned to the trigger's END edge, for a trigger that sits at the right of a
   row -- a table row, a card header. Which alignment overflows the viewport
   depends on where the trigger is, and CSS cannot know that without anchor
   positioning; below the medium breakpoint both forms fall back to the phone
   treatment, which is the same answer .nav__menu reached (defect D20b). */
.overflow-menu--end .overflow-menu__list { inset-inline-start: auto; inset-inline-end: 0; }
/* THE PHONE FORM IS A SHEET AT THE FOOT OF THE VIEWPORT, and the reason is a
   defect that only appears once the menu is actually OPENED on a phone -- so
   no rendered specimen and no assertion had ever seen it.

   This block set `position: fixed` and `inset-block-start: auto` and stopped
   there. `auto` on a fixed box means "use the STATIC position" -- wherever the
   panel would have sat in flow, just under the trigger -- and then freezes it
   against the VIEWPORT. So the panel is placed by where the trigger happened
   to be when it opened, and it is fixed, which means the page cannot be
   scrolled to reach any part of it that falls off the bottom. Driven open at
   360x780: the panel measured top 694, bottom 951. 171px of it, three of its
   six items including the destructive one, were below the fold and
   unreachable by any gesture. The horizontal half of this rule worked -- it
   spanned the gutters as the documentation says -- and that is what made it
   look finished.

   Pinned to the foot instead, the panel is reachable wherever the trigger
   sits, which is the whole point of falling back to a phone treatment; and a
   menu longer than the screen now scrolls INSIDE itself. The height cap is
   the same expression the form layer's floating panel uses. */
@media (max-width: 671px) {
  .overflow-menu__list {
    position: fixed;
    inset-inline: var(--sp-05);
    inset-block-start: auto;
    inset-block-end: var(--sp-05);
    min-inline-size: 0;
    max-inline-size: none;
    max-block-size: min(70vh, calc(100vh - var(--sp-12)));
    overflow-y: auto;
  }
}
.overflow-menu__item {
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  inline-size: 100%;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding-inline: var(--sp-04);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font-family: var(--font-sans);
  font-size: var(--ts-03);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
  text-align: start;
  cursor: pointer;
}
.overflow-menu__item:hover { background: var(--layer-03); color: var(--text-primary); }
.overflow-menu__item[disabled] { color: var(--text-disabled); cursor: not-allowed; }
.overflow-menu__item[disabled]:hover { background: none; color: var(--text-disabled); }
/* Destructive. The word says it; the colour agrees with the word. */
.overflow-menu__item--danger { color: var(--support-error); }
.overflow-menu__item--danger:hover { background: var(--layer-03); color: var(--support-error); }
.overflow-menu__sep {
  block-size: var(--border-w);
  margin-block: var(--sp-02);
  background: var(--border-subtle);
  border: 0;
}
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- media
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. Concatenated into css/senua.css by scripts/build.sh -- never edit
   css/senua.css directly, it is a build artifact.
   =============================================================================

   WHAT THIS LAYER IS FOR

   Everything that puts a PICTURE, a MOVING PICTURE, a SOUND or a PERSON on the
   page, plus the two components that exist because those things are sometimes
   absent: the empty state and the icon.

     .icon          inline SVG, sized off the type scale, painted currentColor
     .ratio         a bare aspect-ratio box for any embed, including an iframe
     .video         the frame around a native <video controls>
     .audio         the frame around a native <audio controls>, plus transcript
     .figure        image + caption + credit
     .avatar        a person or an organisation, with an initials fallback
     .avatar-group  the stacked form
     .empty         nothing here yet, and what to do about it
     .media-object  thumbnail beside text: a result, a row, a notification

   THE ONE DECISION THAT SHAPES THE WHOLE LAYER

   The PLAYER IS NATIVE. <video controls> and <audio controls> ship a control
   surface that is already keyboard operable, already in the accessibility
   tree, already localised into the user's language, already honours the OS
   captions style, already offers picture-in-picture, playback rate and, on a
   phone, the platform's full-screen behaviour and its lock-screen controls. A
   hand-built player throws all of that away and then spends a thousand lines
   re-earning a fraction of it -- and in every audit I have read, fails. So
   this layer styles the FRAME and leaves the controls alone.

   What that costs: the controls will not match the brand, and they will look
   slightly different in Safari, Chrome and Firefox. That is the correct trade
   for a system sold into government, and it is a decision, not an omission.
   A custom control surface belongs in a product that has a person who will own
   its accessibility forever; it does not belong in a design system.

   WCAG 2.1 AA, and which success criterion each part answers:

     1.2.2 Captions (Prerecorded)   -- .video demonstrates <track kind="captions">
                                       and the system considers a video without
                                       one incomplete, not merely unpolished.
     1.2.3 Audio Description or Media Alternative -- .video__transcript and
                                       .audio__transcript carry the text
                                       alternative, in the page, not linked away.
     1.4.2 Audio Control            -- NOTHING in this layer autoplays. There is
                                       no autoplay modifier and there will not
                                       be one. A muted, looping background clip
                                       is the one place autoplay is defensible
                                       and it is .video--ambient, which requires
                                       muted + loop + playsinline and carries no
                                       audio track at all.
     1.4.11 Non-text Contrast       -- every frame edge is --border-subtle on a
                                       layer (decoration) except the facade
                                       button, which is a CONTROL and takes
                                       --border-interactive.
     2.1.1 Keyboard                 -- the facade is a real <button>. Not a div
                                       with a click handler, not an <a href="#">.
     2.4.7 Focus Visible            -- inherited from the global :focus-visible.
                                       Inside a clipped frame the offset flips
                                       to --focus-offset-inset so the ring is
                                       not cut off by overflow:hidden.
     1.1.1 Non-text Content         -- .avatar__initials is aria-hidden and the
                                       accessible name comes from the group, so
                                       a screen reader hears "Rod Higgins", not
                                       "R H".

   NO LEFT-EDGE ACCENT BAR anywhere in this file. Not on the empty state, not
   on the media row, not on the video frame. Status is a LABEL.

   NO ANIMATION that survives prefers-reduced-motion: the only motion here is a
   --dur-fast colour/opacity transition on the facade, which the global
   reduced-motion block in senua-base.css already takes to .01ms.
   ============================================================================= */


/* =============================================================================
   ICON
   =============================================================================
   ONE CONVENTION, AND IT IS A MARKUP CONTRACT AS MUCH AS A CSS ONE.

     <svg class="icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
       <path d="..."/>
     </svg>

   The four parts, and why each is not optional:

   1. NO WIDTH/HEIGHT ATTRIBUTES. Size comes from --icon-size, which is a step
      on the TYPE scale, so an icon beside 14px text is 14px and an icon beside
      20px text is 20px. Attributes would win over nothing (presentation
      attributes have zero specificity) but they duplicate the decision in two
      places and the two drift.
   2. viewBox="0 0 16 16" ON EVERY ICON. One grid, so stroke weights match when
      two icons sit side by side. An icon drawn on a 24 grid at 1.5 stroke is
      visibly lighter than one drawn on a 16 grid at 1.5, and a set that mixes
      grids reads as a set someone assembled rather than drew.
   3. currentColor, NEVER a colour. The icon takes the colour of the text it
      sits in, which means it is correct in both themes, on a layer, on the
      accent fill and inside a disabled control, for free and forever.
   4. aria-hidden="true" focusable="false" WHEN DECORATIVE -- which is almost
      always, because an icon beside a word is decorative and the word is the
      label. When the icon is the ONLY content of a control, the control takes
      an aria-label and the svg stays hidden; a <title> inside the svg is the
      fallback for a standalone informative graphic, and then role="img" goes
      with it. focusable="false" is for IE/legacy Edge, which put SVGs in the
      tab order; it costs two attributes and removes a phantom tab stop.

   NO ICON FONT. A font ligature icon renders as a letter when the font fails,
   is announced by some screen readers as a private-use codepoint, and cannot
   be two-tone. NO EMOJI: emoji are a typeface decision made by the OS vendor,
   they differ per platform, and they are not a system.
   ============================================================================= */
.icon {
  /* Step 03 (16px) is the default because body copy is step 03. */
  --icon-size: var(--ts-03);
  display: inline-block;
  inline-size: var(--icon-size);
  block-size: var(--icon-size);
  /* img/svg/video in senua-base.css carries height:auto; an icon has a fixed
     square box, so the block-size above must be stated and this stops a flex
     or grid parent stretching it. */
  flex: 0 0 auto;
  /* THE ALIGNMENT RULE. text-bottom sits the icon's bottom edge on the text's
     bottom edge, which for an icon the same size as its text puts the glyph
     optically on the baseline -- the descender depth and the icon's own
     bottom padding cancel. `middle` (the common choice) aligns to the x-height
     midpoint and rides visibly high next to capitals; a negative em offset
     (the other common choice) is a magic number that has to be retuned for
     every size. This keyword needs no tuning and no token.
     It follows that --icon-size should MATCH the step of the text beside it.
     An icon deliberately larger than its text is a graphic, not an icon: put
     it in a .figure or an .empty__icon, both of which centre it instead. */
  vertical-align: text-bottom;
  /* The default drawing style of this set: stroked, not filled. An icon that
     wants a filled shape sets fill="currentColor" on its own path. */
  fill: none;
  stroke: currentColor;
  /* GEOMETRY, NOT A LENGTH. 1.5 is in viewBox user units on the 16-unit grid
     above -- it scales with the icon and is never 1.5 CSS pixels. It is the
     one number in this file that is not a token because there is no token that
     could express it: the spacing scale is page space, this is drawing space.
     Changing it changes the weight of the whole set, which is the point of it
     being declared once. */
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* SIZES, each named for the type step it tracks. There is no --icon-lg that
   means "bigger": every size is a step, so an icon can always be matched to
   the text it accompanies. */
.icon--12 { --icon-size: var(--ts-01); }   /* 12 -- eyebrow, badge, meta */
.icon--14 { --icon-size: var(--ts-02); }   /* 14 -- button sm, helper text */
.icon--16 { --icon-size: var(--ts-03); }   /* 16 -- body, default */
.icon--20 { --icon-size: var(--ts-05); }   /* 20 -- lede, button lg */
.icon--24 { --icon-size: var(--ts-06); }   /* 24 -- h3, a list row */
.icon--32 { --icon-size: var(--ts-08); }   /* 32 -- an empty state's mark */
/* A filled icon. Some marks (a solid play triangle, a caution sign) read
   wrong as outlines at 16px because the counters close up. */
.icon--solid { fill: currentColor; stroke: none; }
/* An icon on its own line or centred in a box has no baseline to sit on, and
   text-bottom then leaves it a descender's worth low. */
.icon--block { display: block; vertical-align: baseline; }
/* An icon whose colour must NOT follow the text -- a status glyph in a row of
   prose. Still a role, never a raw colour. */
.icon--quiet  { color: var(--text-helper); }
.icon--accent { color: var(--accent-text); }


/* =============================================================================
   RATIO -- the bare aspect-ratio box
   =============================================================================
   For any embed whose markup you do not control: a YouTube iframe, a map, a
   third-party dashboard, a <canvas>. .video and .figure use the same mechanism
   internally; this is it with nothing on top.

   max-inline-size: 100% IS THE WHOLE POINT AND IT IS NOT DECORATION. An
   aspect-ratio box derives its HEIGHT from its width, so the moment anything
   gives it an intrinsic width larger than its container -- a 560px iframe
   attribute, a 640px video, a grid track with no floor -- the box widens the
   document and the page scrolls sideways at every width below that. The
   element that overflows reports a sane width, so the fault is found in the
   parent, which is why this class of defect survives so long. Two lines stop
   the entire class: max-inline-size here, min-inline-size:0 on the children.
   ============================================================================= */
.ratio {
  --ratio: 16 / 9;
  position: relative;
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  aspect-ratio: var(--ratio);
  overflow: hidden;
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);

  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* The child fills the box whatever it is. `inset: 0` plus 100%/100% rather
   than aspect-ratio on the child, because an iframe has no intrinsic ratio to
   preserve and a <video> has one that is usually NOT the box's. */
.ratio > img,
.ratio > svg,
.ratio > video,
.ratio > iframe,
.ratio > embed,
.ratio > object,
.ratio > canvas,
.ratio > picture,
.ratio > .ratio__fill {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  min-inline-size: 0;
  max-inline-size: 100%;
  border: 0;
  /* cover for a photograph, which is meant to fill; see --contain below. */
  object-fit: cover;
}
.ratio--16x9   { --ratio: 16 / 9; }
.ratio--4x3    { --ratio: 4 / 3; }
.ratio--3x2    { --ratio: 3 / 2; }
.ratio--square { --ratio: 1 / 1; }
/* Cinematic, for a page-width band. Not for a card. */
.ratio--21x9   { --ratio: 21 / 9; }
/* A diagram, a screenshot or a logo must not be cropped: letterbox it onto the
   layer instead. Cropping a screenshot removes information; cropping a
   photograph removes scenery. */
.ratio--contain > * { object-fit: contain; }
/* No frame -- the media is the edge (a full-bleed band, a card header). */
.ratio--bare { border: 0; background: transparent; }


/* =============================================================================
   VIDEO
   =============================================================================
   Structure:

     <figure class="video video--16x9">
       <div class="video__frame">
         <video class="video__el" controls preload="metadata" poster="...">
           <source src="..." type="video/mp4">
           <track kind="captions" src="...vtt" srclang="en-AU"
                  label="English captions" default>
         </video>
       </div>
       <figcaption class="video__caption">What this shows.</figcaption>
     </figure>

   preload="metadata", NEVER preload="auto". auto pulls the whole file for a
   reader who may never press play -- on a metered connection that is money,
   and on a government SOE it is a proxy complaint. metadata fetches the
   duration and the first frame index and stops. For anything large, use the
   FACADE below and fetch nothing at all.
   ============================================================================= */
.video {
  --ratio: 16 / 9;
  display: block;
  margin: 0 0 var(--sp-06);
  max-inline-size: 100%;
}
/* The ratio modifiers the brief names, on the block itself, because the
   author thinks "this is a 4:3 video", not "this frame is 4:3". */
.video--16x9   { --ratio: 16 / 9; }
.video--4x3    { --ratio: 4 / 3; }
.video--square { --ratio: 1 / 1; }
.video--21x9   { --ratio: 21 / 9; }

.video__frame {
  position: relative;
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  aspect-ratio: var(--ratio);
  overflow: hidden;
  /* --background, not a layer: a video letterboxes, and the bars should read
     as the darkest ground the page owns rather than as a lighter panel behind
     the picture. */
  background: var(--background);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);

  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
.video__el {
  position: absolute;
  inset: 0;
  display: block;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: 100%;
  /* contain, NOT cover. Cropping a video crops the subject out; a 4:3 clip in
     a 16:9 frame letterboxes and that is correct. */
  object-fit: contain;
  background: var(--background);

  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* The native control surface draws its own focus ring inside the element, so
   the frame's clip must not eat the element's own ring when the whole video
   takes focus. */
.video__el:focus-visible { outline-offset: var(--focus-offset-inset); }

/* --- the caption, the title and the meta line ----------------------------- */
.video__caption {
  /* figcaption. Step 02 with the step's own leading, and --ts-code declared
     beside it so a <code> in a caption matches the caption -- the contract
     senua-base.css asks consumers to honour whenever they set a size below
     step 03. */
  --ts-code: var(--ts-02);
  margin: var(--sp-04) 0 0;
  max-inline-size: var(--measure);
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
}
.video__title {
  margin: var(--sp-05) 0 0;
  font-size: var(--ts-04);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
.video__meta {
  --ts-code: var(--ts-01);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-02) var(--sp-05);
  margin: var(--sp-03) 0 0;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: 0;
  color: var(--text-helper);
}

/* --- STATUS: a LABEL, never a stripe and never a spinner -------------------
   Loading and error are words in the frame. A spinner says "something is
   happening" and nothing else; "Loading -- 6 s clip" says what and how much,
   survives prefers-reduced-motion untouched because it does not move, and is
   readable by a screen reader when the region is a live one. */
.video__status {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  margin: 0;
  padding: var(--sp-02) var(--sp-04);
  background: var(--scrim-strong);
  color: var(--text-on-media-2);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  --ts-code: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  /* Shown only in the state it names. The default is "no status", so a working
     player carries no furniture. */
  display: none;
}
/* Each status shows only in the state that names it, so "Loading" and "Could
   not load" can both sit in the markup and neither is ever a lie. */
.video.is-loading .video__status--loading { display: block; }
.video.is-error   .video__status--error {
  display: block;
  color: var(--support-error);
}

/* --- THE FACADE: click to load ---------------------------------------------
   For a heavy clip. The <video> carries preload="none" and NO source, so the
   page fetches the poster and nothing else; activating the button attaches the
   source and plays. On a 30 MB explainer that is 30 MB not spent on a reader
   who scrolled past.

   It is a real <button>: keyboard operable by definition, in the tab order by
   definition, announced as a button by definition. The other two ways this is
   commonly built -- a div with onclick, an <a href="#"> -- each fail 2.1.1 or
   4.1.2 and neither can be fixed without re-implementing what <button> does.

   The poster is set with the POSTER ATTRIBUTE on the video element. Never with
   a style attribute, and never with a background-image written into the page:
   the attribute is markup the browser already understands, it is what the
   native player shows in its own idle state, and it keeps this stylesheet free
   of per-page content. */
.video__facade {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-04);
  inline-size: 100%;
  padding: var(--sp-06);
  /* A scrim, so the label clears AA over whatever the poster happens to be.
     Bottom-weighted: the poster's subject is usually centred and the label
     sits under it. The gradient is the IMAGE and the hover darkening is the
     COLOUR underneath it -- two properties rather than one, because a
     background-image cannot be transitioned between two gradients and a
     transition that silently does nothing is a lie in the stylesheet. */
  background-image: linear-gradient(to top, var(--scrim-strong), var(--scrim-mid));
  background-color: transparent;
  color: var(--text-on-media);
  /* 1.4.11: this is a control, so its edge is the interactive border, not the
     subtle one. Inset so the clip cannot shave it. */
  border: 0;
  box-shadow: inset 0 0 0 var(--border-w) var(--border-interactive);
  border-radius: var(--r-md);
  font-family: var(--font-sans);
  text-align: center;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.video__facade:hover { background-color: var(--scrim-mid); }
.video__facade:focus-visible { outline-offset: var(--focus-offset-inset); }
/* Once the source is attached the facade is removed from the DOM by the page's
   own script; this rule is the belt to that braces, so a facade left in place
   by a failed script cannot sit over a playing video and swallow its controls. */
.video.is-ready .video__facade { display: none; }
/* The play mark. A circle with a solid triangle, on the same 16 grid. */
.video__play {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sp-09);
  block-size: var(--sp-09);
  border: var(--border-w) solid var(--text-on-media);
  border-radius: var(--r-pill);
  color: var(--text-on-media);
}
.video__facade-label {
  font-size: var(--ts-04);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-on-media);
}
.video__facade-meta {
  --ts-code: var(--ts-01);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--text-on-media-3);
}
/* At a phone width the mark, the label and the meta line fill a 16:9 frame
   edge to edge -- measured at 412px, where the meta wrapped onto a second
   line and sat a few pixels off the frame's bottom edge. The furniture comes
   down one step each; the label does not, because it is the thing being read. */
@media (max-width: 671px) {
  .video__facade { gap: var(--sp-03); padding: var(--sp-05); }
  .video__play { inline-size: var(--sp-08); block-size: var(--sp-08); }
}

/* --- ambient video ---------------------------------------------------------
   The ONLY autoplay this system permits, and it is permitted because 1.4.2 is
   about AUDIO: a clip with muted + loop + playsinline and no audio track makes
   no sound to control. It still must not be the only way information is
   conveyed, and a reader who has asked for reduced motion should not be handed
   a moving background -- so it is hidden there and the poster stands in.
   There is no .video--autoplay, and adding one would be a defect. */
.video--ambient .video__el { object-fit: cover; }
@media (prefers-reduced-motion: reduce) {
  .video--ambient .video__el { display: none; }
}

/* --- the transcript --------------------------------------------------------
   1.2.3 wants an alternative for the visual content; a transcript in the page
   is the one that also serves the reader who cannot play media at their desk,
   the reader on a metered connection, and the search index. A <details> keeps
   it out of the way without putting it on another page. */
.video__transcript,
.audio__transcript {
  margin: var(--sp-05) 0 0;
  border-block-start: var(--border-w) solid var(--border-subtle);
}
.video__transcript > summary,
.audio__transcript > summary {
  padding: var(--sp-04) 0;
  font-size: var(--ts-02);
  font-weight: 500;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
  cursor: pointer;
}
.video__transcript > summary:hover,
.audio__transcript > summary:hover { color: var(--text-primary); }
.video__transcript > summary:focus-visible,
.audio__transcript > summary:focus-visible { outline-offset: var(--focus-offset); }
.video__transcript-body,
.audio__transcript-body {
  --ts-code: var(--ts-02);
  margin: 0 0 var(--sp-05);
  max-inline-size: var(--measure);
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
}
/* A cue line: a mono timestamp and the text beside it. */
.video__cue,
.audio__cue {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-02) var(--sp-05);
  margin: 0 0 var(--sp-03);
}
.video__cue > time,
.audio__cue > time {
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-body);
  letter-spacing: 0;
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
}
.video__cue > span,
.audio__cue > span {
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
  min-inline-size: 0;
}
@media (max-width: 671px) {
  /* At a phone width a max-content timestamp column plus a sentence is two
     cramped columns; stack them. */
  .video__cue,
  .audio__cue { grid-template-columns: 1fr; gap: 0; }
}

/* --- the caption track's own painting --------------------------------------
   ::cue is the ONE part of the native control surface that is stylable, and
   the default is a translucent black box with the UA's font. Matching the
   system's face costs two declarations and the OS-level captions preference
   still overrides it, which is the correct precedence: a reader who has set
   their captions to yellow-on-black at 200% has done so deliberately.
   No font-size here: the UA scales cues with the video box and with the user's
   own caption settings, and pinning a step would fight both. */
.video__el::cue {
  font-family: var(--font-sans);
  background: var(--scrim-strong);
  color: var(--text-on-media);
}


/* =============================================================================
   AUDIO
   =============================================================================
     <figure class="audio">
       <div class="audio__head">
         <div>
           <p class="audio__title">Title</p>
           <p class="audio__byline">Who, and when</p>
         </div>
         <p class="audio__duration">9 s</p>
       </div>
       <audio class="audio__el" controls preload="metadata" src="..."></audio>
       <details class="audio__transcript"> ... </details>
     </figure>

   NO AUTOPLAY. 1.4.2 says audio that plays for more than three seconds must
   have a way to stop it -- the native controls are that way, but the criterion
   is only satisfied if the reader can find them, and a sound starting from an
   unknown place on a long page is the failure the criterion was written about.
   There is no autoplay modifier here either.

   A TRANSCRIPT IS PART OF THE COMPONENT, not a nicety bolted on. An audio-only
   recording with a transcript satisfies 1.2.1; without one it fails, and no
   amount of styling changes that. The component therefore ships the disclosure
   and the documentation demonstrates it filled in.
   ============================================================================= */
.audio {
  display: block;
  margin: 0 0 var(--sp-06);
  padding: var(--sp-05) var(--sp-06);
  max-inline-size: 100%;
  background: var(--layer-01);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.audio > :last-child { margin-block-end: 0; }
.audio__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-02) var(--sp-05);
  margin: 0 0 var(--sp-05);
}
.audio__head > * { min-inline-size: 0; }
.audio__title {
  margin: 0;
  font-size: var(--ts-04);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
.audio__byline {
  --ts-code: var(--ts-02);
  margin: var(--sp-02) 0 0;
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-helper);
}
.audio__duration {
  --ts-code: var(--ts-01);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.audio__el {
  display: block;
  inline-size: 100%;
  /* The native audio element has an intrinsic width (300px in Chromium) and no
     max-width in the base reset -- img/svg/video are covered there, audio is
     not. Without this it is the element that pushes a 320px page sideways. */
  max-inline-size: 100%;
  /* Chromium refuses to render its control surface below roughly 200px and
     clips instead; on the narrowest supported viewport the component's own
     padding leaves more than that, so the floor is never reached. Stated so the
     next person does not add a min-width and reintroduce the overflow. */
  margin: 0;
}
.audio__el:focus-visible { outline-offset: var(--focus-offset); }
/* A row of them -- an episode list, an evidence bundle. */
.audio--flush { padding: 0; background: transparent; border: 0; }


/* =============================================================================
   FIGURE -- image, caption, credit
   =============================================================================
     <figure class="figure">
       <img class="figure__img" src="..." alt="" width="1600" height="900">
       <figcaption class="figure__caption">
         What the reader should take from it.
         <span class="figure__credit">Source, or photographer</span>
       </figcaption>
     </figure>

   ALT AND DIMENSIONS ARE A MARKUP CONTRACT THE CSS CANNOT SUPPLY.
   - alt="" is correct when the caption already says everything the image says;
     a decorative image with a caption and a repeated alt is announced twice.
     An image carrying information the caption does not needs a real alt.
   - width and height ATTRIBUTES (the intrinsic pixel dimensions, unitless) are
     what let the browser reserve the box before the bytes arrive. Without them
     the page reflows as each image lands, which is a layout-shift defect and a
     reading-position defect for anyone using magnification. They are not a
     size decision and they are not a token: they are the file's own shape.
   ============================================================================= */
.figure {
  display: block;
  margin: 0 0 var(--sp-07);
  max-inline-size: 100%;
}
/* The browser default is `margin: 1em 40px`: a 16px block margin on no scale
   and a 40px INLINE indent that silently narrows every figure on the page. The
   margin above replaces both. */
.figure__img,
.figure > img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  max-inline-size: 100%;
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);
  background: var(--layer-02);
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* A figure whose media is a ratio box or a video frame rather than a bare img. */
.figure > .ratio,
.figure > .video__frame { margin: 0; }
.figure__caption,
.figure > figcaption {
  --ts-code: var(--ts-02);
  margin: var(--sp-04) 0 0;
  max-inline-size: var(--measure);
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
}
.figure__credit {
  --ts-code: var(--ts-01);
  display: block;
  margin-block-start: var(--sp-02);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: 0;
  color: var(--text-helper);
}
/* The caption beside the image rather than under it, for a wide figure in a
   generous column. Below the md breakpoint it stacks, because a 12ch caption
   column is not a caption. */
@media (min-width: 672px) {
  .figure--aside {
    display: grid;
    grid-template-columns: 1fr minmax(0, calc(var(--sp-13) + var(--sp-11)));
    gap: var(--sp-06);
    align-items: start;
  }
  .figure--aside > * { min-inline-size: 0; }
  .figure--aside .figure__caption,
  .figure--aside > figcaption { margin: 0; }
}
/* No frame around the image -- for a logo, a mark or a diagram drawn on the
   page's own ground, where a border would read as a second box. */
.figure--bare .figure__img,
.figure--bare > img { border: 0; background: transparent; }
/* A figure that centres a small mark rather than filling the column. */
.figure--mark { text-align: center; }
.figure--mark .figure__img,
.figure--mark > img {
  display: inline-block;
  inline-size: auto;
  max-inline-size: 100%;
  block-size: var(--sp-12);
}
.figure--mark .figure__caption,
.figure--mark > figcaption { margin-inline: auto; }


/* =============================================================================
   AVATAR
   =============================================================================
     <span class="avatar avatar--40">
       <img class="avatar__img" src="..." alt="" width="80" height="80">
     </span>

     <span class="avatar avatar--40" role="img" aria-label="Rod Higgins">
       <span class="avatar__initials" aria-hidden="true">RH</span>
     </span>

   THE ACCESSIBLE NAME LIVES ON THE CONTAINER, NOT ON THE INITIALS. "RH" read
   out is two letters, and some screen readers spell it while others attempt to
   pronounce it. aria-hidden on the initials plus a real name on the box is the
   only arrangement that reads correctly in both.

   SQUARE BY DEFAULT, because this system is square: --r-sm is 0 on every
   surface and control, and the tokens file names the status pill as the single
   exception where shape carries meaning. .avatar--round is offered on exactly
   that argument and no other: a circular mask is the near-universal convention
   for A PERSON, so in a list mixing people with organisations or systems the
   shape is the fastest available signal of which is which. Use it when that
   distinction is present in the data. Do not use it because circles look
   friendlier -- that is preference, and preference is what square exists to
   overrule.
   ============================================================================= */
.avatar {
  /* 40px, the row height a list of people reads best at. */
  --avatar-size: var(--sp-08);
  --avatar-type: var(--ts-03);
  /* How much of its neighbour a stacked avatar covers. See .avatar-group. */
  --avatar-lap: var(--sp-03);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  inline-size: var(--avatar-size);
  block-size: var(--avatar-size);
  overflow: hidden;
  background: var(--layer-03);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-sm);
  color: var(--text-secondary);
  vertical-align: middle;
  user-select: none;
}
.avatar__img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: 100%;
  object-fit: cover;
}
.avatar__initials {
  font-family: var(--font-sans);
  font-size: var(--avatar-type);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-body);
  /* Two capitals with the system's default tabular figures look wrong; this is
     text, not data. */
  font-variant-numeric: normal;
  text-transform: uppercase;
}
/* SIZES. Each pairs a box on the SPACING scale with type on the TYPE scale --
   both steps, neither interpolated. */
.avatar--24 { --avatar-size: var(--sp-06); --avatar-type: var(--ts-01); --avatar-lap: var(--sp-01); }
.avatar--32 { --avatar-size: var(--sp-07); --avatar-type: var(--ts-02); --avatar-lap: var(--sp-02); }
.avatar--40 { --avatar-size: var(--sp-08); --avatar-type: var(--ts-03); --avatar-lap: var(--sp-03); }
.avatar--48 { --avatar-size: var(--sp-09); --avatar-type: var(--ts-04); --avatar-lap: var(--sp-03); }
.avatar--64 { --avatar-size: var(--sp-10); --avatar-type: var(--ts-06); --avatar-lap: var(--sp-04); }
.avatar--96 { --avatar-size: var(--sp-12); --avatar-type: var(--ts-08); --avatar-lap: var(--sp-05); }
.avatar--round { border-radius: var(--r-pill); }
/* The initials ground, when a set of them needs to be told apart at a glance.
   Two, and only two: the accent tint for "this is you", the layer for everyone
   else. A palette of eight pastel avatar backgrounds is a colour-coding scheme
   nobody documented and no reader can decode. */
.avatar--self {
  background: var(--accent-tint);
  border-color: var(--accent);
  color: var(--accent-text);
}
/* An avatar for a THING rather than a person -- a service, a source, a sensor.
   It carries an icon instead of initials and stays square by definition. */
.avatar--system {
  background: var(--layer-02);
  color: var(--text-helper);
  border-radius: var(--r-sm);
}

/* --- the stacked group -----------------------------------------------------
   The overlap is a NEGATIVE MARGIN DERIVED FROM A TOKEN, never an invented
   number -- and it is PER SIZE, because a single overlap cannot be right at
   both ends of the range. Measured on the rendered page: --sp-03 (8px) is a
   third of a 24px avatar, and at that depth the ring eats the second initial,
   so a stack of initials reads as one smear. Each size therefore names its
   own step -- 2, 4, 8, 8, 12, 16 -- all of them on the spacing scale.

   The ring is the page's own ground colour, so each avatar stays a distinct
   silhouette against its neighbour; without it a row of dark squares reads as
   one long bar. */
.avatar-group {
  display: inline-flex;
  align-items: center;
  /* The group is the accessible unit: "4 reviewers", not four unlabelled
     images. The markup carries role="group" and an aria-label. */
}
.avatar-group > .avatar { border-color: var(--background); }
.avatar-group > .avatar + .avatar { margin-inline-start: calc(var(--avatar-lap) * -1); }
/* Later avatars sit ON TOP of earlier ones, so the stack reads left to right.
   --z-raised, not a bare 1. */
.avatar-group > .avatar { position: relative; z-index: var(--z-raised); }
/* The overflow chip: "+3". It is an avatar, so it inherits every size
   modifier; it is not a badge, because a badge is a status. */
.avatar-group__more {
  background: var(--layer-02);
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
}
/* A group beside a label -- "4 reviewers", the form that actually appears in a
   table cell or a card footer. */
.avatar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-04);
}
.avatar-row__label {
  --ts-code: var(--ts-02);
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-helper);
}


/* =============================================================================
   EMPTY STATE
   =============================================================================
     <div class="empty">
       <svg class="icon icon--32 icon--block empty__icon" ...></svg>
       <p class="empty__title">No sources blocked yet</p>
       <p class="empty__body">One sentence saying why it is empty and what
          fills it.</p>
       <div class="empty__action"><button class="btn btn--primary">...</button></div>
     </div>

   THE THREE THINGS AN EMPTY STATE MUST DO, in this order:
   1. Say the list is empty rather than looking broken. A blank panel and a
      loading panel are indistinguishable, and the reader assumes the second.
   2. Say WHY, in a sentence. "No results" is not why. "No sources have been
      blocked since this policy was applied" is.
   3. Offer the action that ends the state -- or say plainly that none is
      needed, because an empty state is frequently GOOD NEWS (no incidents, no
      errors, no outstanding items) and a page that scolds the reader for it is
      wrong about its own product.

   It is a grid so the children need no margins of their own, which is also why
   nothing here fights the spacing scale.
   ============================================================================= */
.empty {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--sp-04);
  padding: var(--sp-09) var(--sp-06);
  max-inline-size: 100%;
  text-align: center;
  background: var(--layer-01);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.empty > * { margin: 0; min-inline-size: 0; }
.empty__icon {
  color: var(--text-helper);
  margin-block-end: var(--sp-02);
}
.empty__title {
  font-size: var(--ts-05);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
.empty__body {
  --ts-code: var(--ts-02);
  max-inline-size: var(--measure);
  font-size: var(--ts-03);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  color: var(--text-helper);
}
.empty__action {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-04);
  margin-block-start: var(--sp-02);
}
/* Inside a table, a card or a panel that already has padding and a border. */
.empty--flush {
  padding: var(--sp-08) var(--sp-05);
  background: transparent;
  border: 0;
}
/* A short form for a narrow column or a sidebar: left-aligned, no mark. */
.empty--compact {
  justify-items: start;
  gap: var(--sp-03);
  padding: var(--sp-06);
  text-align: start;
}
.empty--compact .empty__title { font-size: var(--ts-04); }
.empty--compact .empty__body  { font-size: var(--ts-02); }
.empty--compact .empty__action { justify-content: flex-start; }


/* =============================================================================
   MEDIA OBJECT -- thumbnail beside text
   =============================================================================
   A search result, a notification, a document in a listing and a person in a
   directory are all the same row: a fixed-width piece of media, a flexible
   body, and optionally an action or a timestamp at the end. Building four
   components for that produces four rows that do not line up.

     <li>
       <article class="media-object">
         <span class="media-object__media"> ...img / avatar / icon... </span>
         <div class="media-object__body">
           <p class="media-object__title">...</p>
           <p class="media-object__text">...</p>
           <p class="media-object__meta">...</p>
         </div>
         <div class="media-object__aside">...</div>
       </article>
     </li>

   min-inline-size: 0 on the body is load-bearing: a flex item refuses to
   shrink below its content, so one long unbreakable token in a result snippet
   widens the row, the list, and then the document.
   ============================================================================= */
.media-object {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-05);
  max-inline-size: 100%;
}
.media-object__media {
  flex: 0 0 auto;
  display: block;
  inline-size: var(--sp-09);
  max-inline-size: 100%;
}
.media-object__media > img,
.media-object__thumb {
  display: block;
  inline-size: 100%;
  block-size: auto;
  max-inline-size: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-sm);
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* Thumbnail widths, on the spacing scale. */
.media-object--sm .media-object__media { inline-size: var(--sp-07); }
.media-object--lg .media-object__media { inline-size: var(--sp-11); }
/* A 16:9 thumbnail, for a video result. */
.media-object--wide .media-object__media { inline-size: var(--sp-13); }
.media-object--wide .media-object__media > img,
.media-object--wide .media-object__thumb { aspect-ratio: 16 / 9; }
.media-object__body {
  /* display:block, not just flex sizing. Inside an <a> the parts are written
     as <span> (a <p> is legal there but a span is what people reach for), and
     an inline span ignores every block margin below it -- measured on the
     rendered page: the title and the snippet ran together on one line, which
     looked like a missing line break rather than a missing display value.
     Every part therefore declares its own box. */
  display: block;
  /* BASIS ZERO, not auto. With `flex: 1 1 auto` the item's base size is its
     CONTENT width, and a wrapping row wraps on base sizes before it shrinks
     anything -- so at 412px the body dropped entirely below the 48px
     thumbnail and the row stopped being a media object at all, which is the
     one thing the narrow rule above promises will not happen. Basis zero lets
     the body take whatever is left instead of demanding what it wants. */
  flex: 1 1 0;
  min-inline-size: 0;
}
.media-object__body > * { margin: 0; }
.media-object__body > * + * { margin-block-start: var(--sp-02); }
.media-object__title {
  display: block;
  font-size: var(--ts-04);
  font-weight: 500;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
.media-object__text {
  --ts-code: var(--ts-02);
  display: block;
  max-inline-size: var(--measure);
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  color: var(--text-secondary);
  overflow-wrap: break-word;
}
.media-object__meta {
  --ts-code: var(--ts-01);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-02) var(--sp-04);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: 0;
  color: var(--text-helper);
  overflow-wrap: anywhere;
}
.media-object__aside {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  min-inline-size: 0;
}
/* Centred, for a notification where the media is a small avatar or icon and
   the body is one line. */
.media-object--center { align-items: center; }
/* The media on the trailing side -- a listing where the thumbnail is
   secondary. `row-reverse` would reverse the READING order for a keyboard and
   a screen reader as well; `order` on the one child does not. */
.media-object--reverse .media-object__media { order: 2; }
/* Below the md breakpoint a fixed media column plus a body plus an aside is
   three columns in 320px. The aside drops under the body; the media stays,
   because losing the thumbnail loses the reason the row is a media object. */
@media (max-width: 671px) {
  .media-object { flex-wrap: wrap; gap: var(--sp-04); }
  .media-object__aside { inline-size: 100%; }
  .media-object--wide .media-object__media { inline-size: var(--sp-12); }
}

/* --- the list of them ------------------------------------------------------
   Hairline dividers BETWEEN rows, never around each one: a row is not a card,
   and a list of bordered boxes reads as a set of unrelated things.

   MARKUP CONTRACT: the <ul> must also carry `list--bare`. senua-base.css
   underlines any link inside an <li> that is not in a nav and has not opted
   out with that class, which would otherwise underline a row-wrapping
   <a class="media-object">. The opt-out is the system's own hook; the rule
   below is the same intent expressed in this layer so a consumer who forgets
   it still gets a readable row rather than a lavender underlined block. */
.media-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-inline-size: 100%;
}
.media-list > li { margin: 0; }
.media-list > li + li { border-block-start: var(--border-w) solid var(--border-subtle); }
.media-list > li > .media-object { padding-block: var(--sp-05); }
/* THE ROW AS A LINK, and the specificity arithmetic that makes it hold.

   Same reasoning as a.card: the whole surface is the target, nothing inside it
   is coloured or underlined, and the focus ring wraps the row. But a link
   inside an <li> is exactly what senua-base.css's prose rule underlines, and
   that rule measures (0,3,4):

     :where(li):not(nav li):not(.list--bare li) a:not(.btn):not([class*="nav"])
      0 classes    2 elements      1 class+1 el   1 el  1 class   1 class

   So `.media-list a` (0,1,1) loses, `.media-list > li > a` (0,1,2) loses, and
   the underline survives every obvious repair. The selector below measures
   (0,4,2) -- four classes beats three -- and it wins on class count rather
   than on source order, which is what stops the next edit to the base layer
   quietly reinstating a lavender underlined row.

   The markup should ALSO carry `list--bare` on the <ul>, which is the base
   layer's own opt-out hook and removes the contest entirely. This rule is the
   belt to that braces, for a consumer who does not know the hook exists. */
.media-list > li > a.media-object:not(.btn):not([class*="nav"]),
a.media-object:not(.btn):not([class*="nav"]) {
  display: flex;
  color: inherit;
  text-decoration: none;
}
.media-list > li > a.media-object { padding-block: var(--sp-05); padding-inline: var(--sp-04); }
.media-list > li > a.media-object:hover .media-object__title,
a.media-object:hover .media-object__title { color: var(--accent-text); }
/* The hover ground for a whole row. --layer-01 on the page ground; a row
   inside a card would need --layer-02 and says so with .media-list--onlayer. */
.media-list > li > a.media-object {
  transition: background-color var(--dur-fast) var(--ease);
}
.media-list > li > a.media-object:hover { background: var(--layer-01); }
.media-list--onlayer > li > a.media-object:hover { background: var(--layer-02); }

/* --- media card --------------------------------------------------------------
   A card whose subject is the media rather than the copy: a case study, a
   recorded session, a datasheet with a cover. It is not .card with a picture
   glued on -- the image is the first thing read, so it sits flush to the
   card's edge and the copy is what is padded. */
.media-card {
  display: flex; flex-direction: column; min-inline-size: 0;
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg);
  overflow: hidden;          /* so the media's corners follow the card's */
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
a.media-card:hover { border-color: var(--border-strong); background: var(--layer-03); }
.media-card__media {
  position: relative; display: block;
  aspect-ratio: 16 / 9; inline-size: 100%;
  background: var(--layer-03);
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
.media-card__media > img, .media-card__media > video {
  inline-size: 100%; block-size: 100%; object-fit: cover; display: block;
}
/* A duration or a kind, over the media. It sits on a scrim rather than on the
   photograph, because a white label on an unknown image is a coin toss. */
.media-card__tag {
  position: absolute; inset-block-end: var(--sp-04); inset-inline-end: var(--sp-04);
  padding: var(--sp-02) var(--sp-04);
  background: var(--scrim-strong); color: var(--text-on-media);
  font-family: var(--font-mono); font-size: var(--ts-01);
  letter-spacing: var(--tr-label);
}
.media-card__body { display: flex; flex-direction: column; gap: var(--sp-04);
  padding: var(--sp-06); flex: 1 1 auto; min-inline-size: 0; }
.media-card__eyebrow {
  margin: 0; font-family: var(--font-mono); font-size: var(--ts-01);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-helper);
}
.media-card__title { margin: 0; font-size: var(--ts-04); font-weight: 600;
  color: var(--text-primary); letter-spacing: var(--tr-tight); }
.media-card__text { margin: 0; color: var(--text-secondary); font-size: var(--ts-02);
  line-height: var(--lh-body); }
.media-card__meta { margin-block-start: auto; padding-block-start: var(--sp-04);
  color: var(--text-helper); font-size: var(--ts-01); }
.media-card--wide { aspect-ratio: auto; }
@media (min-width: 672px) {
  .media-card--wide { flex-direction: row; }
  .media-card--wide .media-card__media { inline-size: 40%; aspect-ratio: 4 / 3; flex: none; }
}

/* --- diagram -----------------------------------------------------------------
   AN INLINE SVG THAT EXPLAINS A MECHANISM: a request path, a deployment, a
   sequence. Every technical product has them and this system had nothing to
   say about them, so each one was drawn with hardcoded hex and a hardcoded
   typeface -- one on a consuming site carried ten colours and TWO foreign
   families, `system-ui` and `ui-monospace`, on a page in a system whose first
   rule is one typeface. It rendered as the only place on that site where the
   type was not Geist, and nothing caught it because an SVG presentation
   attribute is not a CSS rule.

   The parts take CLASSES, not fill and stroke attributes, and that is the
   whole mechanism: a presentation attribute cannot hold `var(--layer-02)`, so
   a diagram drawn in attributes can never follow a theme. Drawn in classes it
   follows every one of the six, including a scoped block.

   FONT SIZE IS NOT IN HERE, deliberately, and it is the one thing an author
   still sets. A diagram is drawn on a viewBox and scaled to its column, so its
   text has to scale with the drawing -- which means user units, unitless, in
   the markup beside the coordinates it was laid out against. Set it from the
   type scale instead and the labels stay at 14px while the boxes around them
   halve, and the diagram breaks at exactly the width nobody tested. The
   TYPEFACE is the system's; the size belongs to the drawing. */
.diagram {
  display: block;
  inline-size: 100%;
  block-size: auto;
  font-family: var(--font-sans);
  color: var(--text-primary);
}
/* Text takes the colour it is given rather than SVG's black default. */
.diagram text { fill: currentColor; }
/* A box: a labelled thing in the drawing. */
.diagram__box {
  fill: var(--layer-02);
  stroke: var(--border-subtle);
  stroke-width: var(--border-w);
}
/* The one box the diagram is about. */
.diagram__box--accent { fill: var(--layer-01); stroke: var(--accent); }
/* A box for something outside the reader's control -- a third party, the
   internet, a thing the product deliberately does not touch. */
.diagram__box--quiet { fill: none; stroke: var(--border-subtle); }
/* A boundary around a set of boxes: a perimeter, a host, a region. */
.diagram__zone {
  fill: none;
  stroke: var(--border-subtle);
  stroke-width: var(--border-w);
  stroke-dasharray: 7 7;
}
/* A connection between two boxes, and the arrow head that ends it. */
.diagram__edge { fill: none; stroke: var(--border-strong); stroke-width: var(--border-w); }
.diagram__edge--accent { stroke: var(--accent); }
.diagram__edge--good   { stroke: var(--support-success); }
.diagram__arrow { fill: var(--border-strong); stroke: none; }
/* The three text roles, matching the page's: a title on a box, the line under
   it, and a label on the drawing itself. */
.diagram__title { fill: var(--text-primary); font-weight: 600; }
.diagram__sub   { fill: var(--text-secondary); }
.diagram__label {
  fill: var(--text-helper);
  font-family: var(--font-mono);
  letter-spacing: var(--tr-label);
}

/* --- gallery -----------------------------------------------------------------
   A grid of media. auto-fill rather than auto-fit: with auto-fit a gallery of
   two items stretches each to half the page, which is not a gallery, it is two
   enormous photographs. auto-fill keeps the cell size and leaves the gap. */
.gallery {
  display: grid; gap: var(--sp-05); margin: 0; padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
}
.gallery--tight { gap: var(--sp-03); grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr)); }
.gallery--wide  { grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr)); }
.gallery__item { min-inline-size: 0; }
/* A button, not a div: opening a lightbox is an action, and it has to be
   reachable by Tab and fire on Enter and Space without a keydown handler.

   AND THEREFORE `font: inherit`, WHICH WAS MISSING AND SHOWED. A <button> does
   not inherit the page's font: it carries the UA's control font, and everything
   inside it inherits that instead. The caption below sets a size and a colour
   but not a family, so it rendered in Arial -- the one place in the system
   where a third typeface reached the screen. The gate caught it because it
   reads the COMPUTED family of every leaf on every page, which is the only way
   this is visible: the rule that was wrong is the one nobody wrote. */
.gallery__trigger {
  display: block; inline-size: 100%; padding: 0;
  background: none; border: var(--border-w) solid transparent; cursor: pointer;
  font: inherit; letter-spacing: inherit; color: inherit;
  transition: border-color var(--dur-fast) var(--ease);
}
.gallery__trigger:hover { border-color: var(--border-strong); }
.gallery__trigger:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset);
}
.gallery__img { display: block; inline-size: 100%; aspect-ratio: 4 / 3;
  object-fit: cover; background: var(--layer-03);  /* `inherit` rather than a colour, stated so it is a decision and not an   omission: nothing here renders text of its own. See tests/surface.js. */  color: inherit; }
.gallery__caption { display: block; padding-block-start: var(--sp-03);
  color: var(--text-helper); font-size: var(--ts-01); line-height: var(--lh-body);
  text-align: start; }

/* --- lightbox ----------------------------------------------------------------
   A dialog with one image in it. It is NOT a new overlay: the focus trap,
   Escape and the inert background come from <dialog>, exactly as the modal's
   do. Building a second one would mean maintaining a second focus trap, and a
   second focus trap is a second thing to get wrong. */
.lightbox { inline-size: min(92vw, 72rem); max-block-size: 92vh; padding: 0; }
.lightbox__frame { display: grid; gap: var(--sp-05); padding: var(--sp-06); }
.lightbox__img { display: block; inline-size: 100%; max-block-size: 72vh;
  object-fit: contain; background: var(--layer-03);  /* `inherit` rather than a colour, stated so it is a decision and not an   omission: nothing here renders text of its own. See tests/surface.js. */  color: inherit; }
/* THE BAR WRAPS, AND IT DID NOT. A caption, two controls and a counter on one
   flex row with nothing allowed to wrap: at 320px the caption was squeezed to
   a few characters and `overflow-wrap` shattered ordinary words across lines --
   "team", "work", "office" each broken mid-word, measured on the specimen the
   moment one existed. The row was written before anything rendered it, which
   is how it survived. Wrapping is the right answer rather than truncation: a
   caption is the only thing saying what the photograph is. */
.lightbox__bar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-04) var(--sp-05); min-inline-size: 0;
}
/* Far enough to hold real words before the row wraps, and still able to shrink
   inside its own line. */
.lightbox__caption {
  flex: 1 1 var(--panel-01);
  margin: 0; color: var(--text-secondary); font-size: var(--ts-02);
  min-inline-size: 0;
}
.lightbox__count { margin-inline-start: auto; color: var(--text-helper);
  font-family: var(--font-mono); font-size: var(--ts-01); white-space: nowrap; }
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- overlay
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. Concatenated into css/senua.css by scripts/build.sh -- never edit
   css/senua.css directly, it is a build artifact.
   =============================================================================

   WHAT IS IN THIS LAYER
   ---------------------
   The things that appear OVER the page, and the things that tell a reader the
   page is busy or that something happened:

     modal        a dialog, on the native <dialog> element
     drawer       an edge panel, also on <dialog>
     tooltip      a short label on hover AND on keyboard focus
     popover      a small panel that may contain controls
     toast        a transient notification, announced by a live region
     progress     determinate and indeterminate
     spinner      an indeterminate busy indicator
     skeleton     a loading placeholder
     accordion    on <details>/<summary>

   WHAT IS DELIBERATELY NOT IN IT
   ------------------------------
   There is no .inline-notification. The system already ships .notice in
   css/senua-components.css -- a padded layer-02 surface, one uniform hairline,
   a .notice__kind label that carries the status in a WORD, a .notice__title
   and body copy. That is the persistent in-page message, and a second
   component for the same job is the problem this pass exists to fix, not a
   feature. Three genuine gaps in .notice are recorded in /feedback/
   under "Inline notification" and belong in the file that owns the component:
   a success/info kind modifier, a dismiss control, and an actions row.

   FOUR RULES THAT SHAPE EVERYTHING BELOW
   --------------------------------------
   1. NO LEFT-EDGE ACCENT BAR, anywhere, on anything. Not on a toast, not on a
      notification, not on a drawer. Status is carried by a LABEL -- the word
      Error, Warning, Success -- and by the colour of that label's text. A
      coloured stripe hung off one edge is not in this system, is not Carbon,
      and fails the one reader who most needs the status: the one who cannot
      distinguish the colour. Every variant below recolours a __kind label and
      nothing else.

   2. VISIBILITY NEVER TRANSITIONS WITH A DURATION. `transition: visibility
      var(--dur)` keeps a panel hidden for the whole duration after focus
      reaches it, and a hidden element has no focusable children: driven with
      real keys, a fast Tab walks straight past the panel. Opening is
      `visibility 0s`; closing may carry a delay so the fade is still seen.
      This cost the dropdown a defect in the same week this layer was written
      and every hidden panel here is written the other way round.

   3. NOTHING TRAPS FOCUS EXCEPT A MODAL, and a modal traps it deliberately
      and releases it on close. .modal and .drawer are <dialog> elements
      opened with showModal(), which is where the trap comes from. .popover
      and .tooltip do NOT trap: Tab leaves them, and leaving closes them.

   4. MOTION IS OPTIONAL. senua-base.css reduces every transition and
      animation to .01ms under prefers-reduced-motion, so the drawer slide,
      the toast entrance and the accordion chevron are handled globally. The
      two components that would become INVISIBLE under that rule rather than
      merely still -- the spinner and the skeleton, whose whole appearance is
      an animation -- carry their own reduced-motion block so they still read
      as "busy" when they cannot move.

   TOKENS THIS LAYER NEEDED -- THREE RAISED, TWO SHIPPED AND ADOPTED, ONE OPEN
   ---------------------------------------------------------------------------
   Recorded rather than invented, because a number typed into a component is
   how a scale stops being one. Recording them is what closed them, and this
   block states where each one stands NOW -- a gap list that goes on naming a
   token as missing after it ships is the same defect as a component that
   documents a behaviour it does not have:

     --dur-slow   SHIPS (1440ms) and is USED, raised from this layer and added
                  to the tokens while it was being written. A LOOP is not a
                  TRANSITION: --dur is 180ms, which is correct for a state
                  change and strobes as a repeating animation -- a spinner at
                  180ms is a blur and a shimmer at 180ms flickers. The two
                  shimmers take --dur-slow directly.
     --dur-loop   SHIPS (720ms) and is USED, by the spinner, which is the whole
                  reason it was asked for: a ring at 1440ms reads as stalled and
                  the step between --dur and --dur-slow did not exist. It was
                  `calc(var(--dur-slow) / 2)` until the token landed. There is
                  no arithmetic over a duration left in this file.
     --z-toast    SHIPS (120) and is ADOPTED by .toast-region below. The region
                  has to sit above a menu (--z-dropdown, 60) and below the skip
                  link (--z-skip, 999), and the ladder had no step between them.
                  The harder half was never a z-index: any z-index loses to the
                  top layer, so the region is also promoted into the top layer
                  with the Popover API when it is available -- see the
                  .toast-region comment.
     --panel-*    SHIPS (seven steps, 200 to 640) and is ADOPTED by every
                  panel in this file. This is the gap that was recorded here as
                  --col-min-*, under a name borrowed from a grid-column gap in
                  senua-base.css that has since been WITHDRAWN -- the columns
                  step discretely at the breakpoints and never had the raw
                  floors that entry described. The complaint underneath the
                  wrong name was real and is what closed: a panel width is not
                  a spacing value, and every one in this file was a multiple of
                  --sp-13, three of them fractional. No panel changed size. The
                  scale is the seven widths that were already shipping, named,
                  and there is no arithmetic over a spacing step left in this
                  file.
   ============================================================================= */


/* =============================================================================
   1. MODAL
   =============================================================================
   ON THE NATIVE <dialog>, AND WHERE IT EARNS ITS KEEP.

   showModal() gives four things that are genuinely hard to write correctly and
   are wrong in most hand-rolled modals:
     - focus moves INTO the dialog on open (the first focusable child, or the
       element carrying autofocus);
     - focus is TRAPPED inside it while it is open -- Tab cannot reach the page
       behind, because the rest of the document is inert;
     - Escape closes it, with a cancelable `cancel` event so an unsaved-changes
       guard has somewhere to live;
     - it renders in the TOP LAYER, above every stacking context on the page,
       so it needs no z-index at all and cannot be trapped inside a transformed
       ancestor -- which is the single most common way a hand-rolled modal ends
       up behind something.
   Closing returns focus to the element that was focused when it opened. That
   is the behaviour, not an aspiration: it is driven with real key presses in
   tests/ and stated on /feedback/ only because it was verified.

   WHERE IT DOES NOT EARN ITS KEEP, PLAINLY:
     - It does NOT lock background scrolling. A modal dialog makes the document
       inert, not unscrollable, so a wheel or a two-finger drag still moves the
       page behind the panel. The fix is one declaration on the document and it
       has to be toggled by the consumer's own script: .is-scroll-locked is
       shipped at the bottom of this section for exactly that, because a
       utility every consumer would otherwise reinvent belongs in the system.
     - It has NO entry or exit animation of its own. Adding one needs
       @starting-style plus `transition-behavior: allow-discrete` on display
       and overlay, which is written out below and is not obvious.
     - `show()` -- the non-modal form -- gives NONE of the four guarantees
       above: no trap, no Escape, no backdrop, no inertness. This system's
       modal is always showModal(). If what you want is a non-modal panel,
       that is .popover, further down.
     - The close button must be a real control. A <form method="dialog"> is
       what closes it without script, which is why the panel below is a form.
   ------------------------------------------------------------------------- */

/* The <dialog> is the POSITIONER: transparent, sized, centred. The surface is
   .modal__panel inside it. Splitting the two is what lets the panel be a
   <form method="dialog"> -- so every button inside can close the dialog and
   report WHICH button did it, with no script at all. */
.modal {
  /* Reset the UA dialog box. It ships `border: solid`, `padding: 1em`,
     `background: canvas` and `color: canvastext` -- four raw values on no
     token, and canvas is a system colour that ignores the theme entirely. */
  margin: auto;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  /* Width is the size modifier's job; this is the default (md). The second
     half of the min() is the phone case: the panel keeps one --sp-06 gutter
     each side and never touches the edge. */
  inline-size: min(var(--panel-06), calc(100% - var(--sp-07)));
  /* UA `dialog:modal` caps these at calc(100% - 6px - 2em), which is a raw
     value that leaves the panel 6px from the viewport edge on a phone. */
  max-inline-size: calc(100% - var(--sp-07));
  max-block-size: calc(100% - var(--sp-07));
  overflow: visible;
}
.modal--sm { inline-size: min(var(--panel-03), calc(100% - var(--sp-07))); }
.modal--lg { inline-size: min(var(--panel-07), calc(100% - var(--sp-07))); }

/* THE BACKDROP IS A ROLE, NOT AN rgba(). --overlay is declared in both themes,
   so the scrim over a light page is a light-theme value and not a dark one
   leaking through.
   ::backdrop inherits from its originating element in current browsers, which
   is what makes var(--overlay) resolve here at all; in a browser old enough to
   treat ::backdrop as a root-level pseudo-element the custom property is
   unresolvable and the backdrop falls back to the UA's own translucent black.
   That degradation is a slightly different grey, not a broken modal, which is
   why it is accepted rather than worked around with a literal. */
.modal::backdrop,
.drawer::backdrop { background: var(--overlay); }

/* THE ENTRY AND EXIT ANIMATION, WHICH THE ELEMENT DOES NOT GIVE YOU.
   A dialog goes display:none -> display:block and enters the top layer, and
   both of those are DISCRETE properties: without allow-discrete the element is
   simply gone before any transition can run, so the exit is never seen. The
   `overlay` line is the one people miss -- it is what keeps the element in the
   top layer for the duration of the exit instead of dropping it on frame one.
   @starting-style supplies the "before it was open" value; without it the
   opening frame starts at the final value and nothing moves.
   prefers-reduced-motion is honoured globally by senua-base.css, which takes
   every duration here to .01ms. */
.modal,
.drawer {
  opacity: 0;
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              display var(--dur) allow-discrete,
              overlay var(--dur) allow-discrete;
}
.modal[open],
.drawer[open] { opacity: 1; }
@starting-style {
  .modal[open],
  .drawer[open] { opacity: 0; }
}
.modal::backdrop,
.drawer::backdrop {
  opacity: 0;
  transition: opacity var(--dur) var(--ease),
              display var(--dur) allow-discrete,
              overlay var(--dur) allow-discrete;
}
.modal[open]::backdrop,
.drawer[open]::backdrop { opacity: 1; }
@starting-style {
  .modal[open]::backdrop,
  .drawer[open]::backdrop { opacity: 0; }
}

/* THE SURFACE. A flex column so the header and the actions hold their place
   and the body is the part that gives -- which is what makes a long modal
   scroll in the middle instead of pushing its own buttons off the screen. */
.modal__panel,
.drawer__panel {
  display: flex;
  flex-direction: column;
  max-block-size: 100%;
  margin: 0;
  background: var(--layer-02);
  /* A panel floats over content it does not own. --border-subtle plus
     --shadow-md is the treatment .nav__menu already uses for the same reason:
     in light theme a 0.05 hairline does not separate a panel from the page
     under it, and in dark --shadow-md resolves to none and the layer ladder
     does the job. */
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md);
  text-align: start;

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}

.modal__header,
.drawer__header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-05);
  padding: var(--sp-06) var(--sp-06) var(--sp-05);
}
/* The title is set at step 05 with heading leading and heading tracking. It is
   an <h2> or <h3> in the markup -- a dialog needs a real heading for
   aria-labelledby -- and the element's own ladder would render it at 48px
   inside a 480px panel from 1056px up, which is why the class re-states all
   four values rather than only the size. */
.modal__title,
.drawer__title {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
  font-size: var(--ts-05);
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
/* THE STATUS LABEL, AND THE ONLY PLACE STATUS LIVES. Same shape as
   .notice__kind: mono, 12px, uppercase, tracked, and coloured by the variant.
   No stripe, no fill, no icon-only signal. */
.modal__kind,
.drawer__kind,
.toast__kind,
.popover__kind {
  --ts-code: var(--ts-01);
  display: block;
  margin: 0 0 var(--sp-02);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  font-weight: 500;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--support-info);
}

/* The body. Its own scroll container, so the panel's header and actions stay
   put: overflow-y:auto here and nowhere else is what "scrollable body" means.
   overscroll-behavior stops a flick at the end of the list from scrolling the
   page behind the modal, which is the one piece of scroll containment CSS can
   do on its own. */
.modal__body,
.drawer__body {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--sp-06) var(--sp-06);
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.modal__body > :last-child,
.drawer__body > :last-child { margin-bottom: 0; }
.modal__body p,
.drawer__body p { margin: 0 0 var(--sp-05); max-width: none; }

/* SCROLLABLE-BODY VARIANT. The body above already scrolls when the panel runs
   out of room; this modifier makes that VISIBLE and predictable: the panel
   takes a definite height, and the two hairlines tell the reader that the
   middle moves and the ends do not. Without the rules, a body that scrolls
   looks exactly like a body that is merely cut off. */
/* THE DEFINITE HEIGHT GOES ON THE DIALOG, NOT ON THE PANEL. It was written as
   `.modal--scroll .modal__panel { block-size: min(480px, 100%) }` and did
   nothing at all: the panel's parent is the dialog, whose own height is auto,
   so a percentage height has nothing to resolve against and falls back to
   auto. The two hairlines appeared, the body never scrolled, and the variant
   documented an affordance it did not have -- visible only in a screenshot at
   a tall viewport, where every paragraph was on screen at once. The dialog is
   fixed-positioned, so ITS percentage resolves against the viewport. */
/* The one panel step used as a HEIGHT. It is the default modal's own width,
   deliberately: a scrolling dialog that is taller than it is wide reads as a
   column, and this keeps it square at the size the panel already is. */
.modal--scroll { block-size: min(var(--panel-06), calc(100% - var(--sp-07))); }
.modal--scroll .modal__panel { block-size: 100%; }
.modal--scroll .modal__header,
.drawer--scroll .drawer__header {
  border-block-end: var(--border-w) solid var(--border-subtle);
  padding-block-end: var(--sp-05);
}
.modal--scroll .modal__body,
.drawer--scroll .drawer__body { padding-block-start: var(--sp-05); }
.modal--scroll .modal__actions,
.drawer--scroll .drawer__actions {
  border-block-start: var(--border-w) solid var(--border-subtle);
  padding-block-start: var(--sp-05);
}

/* ACTIONS. Reverse row so the primary action is written FIRST in the markup --
   which is the order a screen reader and the keyboard get -- and renders at
   the end, which is the order a pointer expects on a dialog. Wrapping is
   allowed because two long labels do not fit 320px side by side, and when they
   wrap the primary ends up on top, which is still the right order. */
.modal__actions,
.drawer__actions,
.popover__actions {
  display: flex;
  flex-wrap: wrap-reverse;
  flex-direction: row-reverse;
  justify-content: flex-start;
  gap: var(--sp-04);
  padding: 0 var(--sp-06) var(--sp-06);
}
/* A dialog's buttons are its most reachable target on a phone; below the
   medium breakpoint they take the full width rather than sitting as two small
   islands in a wide panel. */
@media (max-width: 671px) {
  .modal__actions > .btn,
  .drawer__actions > .btn { flex: 1 1 100%; justify-content: center; }
}

/* ALERT / CONFIRM VARIANT.
   The markup contract, which CSS cannot supply and which is the whole point of
   the variant: role="alertdialog" on the <dialog>, aria-labelledby pointing at
   the title and aria-describedby pointing at the sentence that states the
   consequence. That is what makes a screen reader announce the CONSEQUENCE on
   open rather than just the title.
   Visually it is a small modal whose kind label carries the status. There is
   no red panel, no red border and no stripe: the word is the signal. */
.modal--alert { inline-size: min(var(--panel-05), calc(100% - var(--sp-07))); }
.modal--alert .modal__kind { color: var(--support-warning); }
.modal--alert.modal--destructive .modal__kind { color: var(--support-error); }

/* THE CLOSE CONTROL. Its appearance is NOT in this file. The boundary at rest,
   the hover, the focus ring, the transition and the icon are declared once in
   css/senua-components.css, in a selector list that names these four alongside
   .notice__dismiss, because all five are one control and this layer held the
   second copy of it. The two copies had already drifted -- this one rounded to
   --r-md and the other to --r-sm, both zero today -- which is the argument for
   collapsing them, observed rather than predicted. The reasoning is kept whole
   beside the rule, in that file.

   What stays here is what is genuinely this layer's: how big each panel's
   control is, and how far it is pulled into the panel's own padding. That is
   geometry belonging to the panel, not treatment belonging to the control.

   The markup contract stays here too, because it is what the selector cannot
   supply: a real <button> with a real accessible name -- aria-label="Close" --
   because an SVG with no name is an unlabelled control, a 4.1.2 failure and the
   most common one on this component. */
.modal__close,
.drawer__close,
.popover__close,
.toast__dismiss {
  padding: 0;
}
/* 40px is a spacing step and a legitimate touch target. The negative margin
   pulls the control into the panel's padding so its border aligns with the
   panel's content edge rather than sitting a step inside it. */
.modal__close,
.drawer__close {
  inline-size: var(--sp-08);
  block-size: var(--sp-08);
  margin: calc(var(--sp-02) * -1) calc(var(--sp-04) * -1) 0 0;
}

/* BACKGROUND SCROLL LOCK. Shipped because every consumer needs it and a
   dialog does not provide it: put this on <html> or <body> while a modal or
   drawer is open and take it off when it closes. It is one declaration, it is
   toggled by the consumer's script, and it is here so that four consuming
   sites do not write four slightly different versions of it. */
.is-scroll-locked { overflow: hidden; }


/* =============================================================================
   2. DRAWER
   =============================================================================
   A panel that slides in from an edge. It is a <dialog> for the same four
   reasons the modal is: the focus moves in, the trap holds while it is open,
   Escape closes it, and it renders in the top layer so no z-index is involved
   and no transformed ancestor can swallow it. Its focus obligations are the
   modal's, exactly: focus in on open, focus back to the trigger on close.

   NOT THE MOBILE NAVIGATION MENU. This is the general-purpose drawer -- a
   filter panel, a detail pane, a settings sheet. The mobile navigation lives
   in css/senua-navigation.css and is a different component with a different
   job (it is the site's primary navigation, it is part of the masthead's own
   disclosure, and it has its own current-page and section semantics). If that
   component wants this behaviour, it should COMPOSE .drawer -- `<dialog
   class="drawer drawer--start">` with nav markup inside -- rather than
   reimplement the slide, the backdrop and the trap a second time.
   ------------------------------------------------------------------------- */

.drawer {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  max-inline-size: 100%;
  max-block-size: 100%;
  /* The dialog is pinned to an edge rather than centred. inset-block covers the
     full height for the side variants; the bottom variant overrides it. */
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  inline-size: min(var(--panel-06), calc(100% - var(--sp-08)));
  /* AN EXPLICIT FULL HEIGHT, AND IT IS NOT REDUNDANT WITH inset-block: 0.
     The UA sets `height: fit-content` on a dialog, which beats the stretch a
     top-and-bottom offset would otherwise give -- so `inset-block: 0` alone
     produced a right-hand drawer that stopped level with the end of its own
     text, floating in the middle of the viewport with page visible above and
     below it. Found in a screenshot at 412px; nothing in the stylesheet says
     it and no computed-style check would have asked. */
  block-size: 100%;
}
.drawer .drawer__panel {
  block-size: 100%;
  /* An edge panel is attached to the edge: the two corners that meet it lose
     their radius, and it takes no shadow-side border. With --r-lg at 0 this is
     a no-op today and stays correct if the system ever takes a radius. */
  border-radius: 0;
}

/* The slide. Same allow-discrete / @starting-style machinery as the modal --
   see that comment; this only supplies the transform each edge starts from. */
.drawer { transform: translateX(100%); }
.drawer[open] { transform: translateX(0); }
@starting-style { .drawer[open] { transform: translateX(100%); } }

/* LEFT / INLINE-START. */
.drawer--start {
  inset-inline-start: 0;
  inset-inline-end: auto;
  transform: translateX(-100%);
}
.drawer--start[open] { transform: translateX(0); }
@starting-style { .drawer--start[open] { transform: translateX(-100%); } }

/* RIGHT / INLINE-END -- the default, named so a consumer can say which it
   means rather than relying on the default being what they wanted. */
.drawer--end {
  inset-inline-end: 0;
  inset-inline-start: auto;
  transform: translateX(100%);
}
.drawer--end[open] { transform: translateX(0); }
@starting-style { .drawer--end[open] { transform: translateX(100%); } }

/* BOTTOM. A sheet. Its height is content-driven up to a ceiling, because a
   bottom sheet that is always 60% of the viewport looks broken holding two
   lines. The ceiling leaves one --sp-10 of page visible above it, which is
   what tells the reader the page is still there. */
.drawer--bottom {
  inset-block-start: auto;
  inset-block-end: 0;
  inset-inline: 0;
  inline-size: 100%;
  /* A sheet is as tall as its content -- the opposite of the side variants,
     which is why the height set above is taken back here. */
  block-size: auto;
  max-block-size: calc(100% - var(--sp-10));
  transform: translateY(100%);
}
.drawer--bottom[open] { transform: translateY(0); }
@starting-style { .drawer--bottom[open] { transform: translateY(100%); } }
.drawer--bottom .drawer__panel { block-size: auto; max-block-size: 100%; }

/* A drawer is full-height, so its header wants the hairline by default: there
   is a lot of panel below it and nothing else separates the title from the
   content. The modal earns its hairline only in the --scroll variant. */
.drawer__header { border-block-end: var(--border-w) solid var(--border-subtle); }
.drawer__body { padding-block-start: var(--sp-05); }
.drawer__actions {
  border-block-start: var(--border-w) solid var(--border-subtle);
  padding-block-start: var(--sp-05);
}


/* =============================================================================
   3. TOOLTIP
   =============================================================================
   A SHORT LABEL THAT MUST NOT BE HOVER-ONLY. A tooltip that appears only on
   mouseover is unreachable by a keyboard user and by most touch users, which
   is a WCAG 1.4.13 failure and, less formally, a component that hides its own
   content from a third of its readers. Every rule below is written for :hover
   AND :focus-visible together.

   WCAG 1.4.13 asks for three things, and here is where each one is:
     HOVERABLE   the pointer can travel from the trigger onto the bubble
                 without it vanishing. The bubble is a DESCENDANT of the
                 trigger's wrapper, so hovering it keeps .tooltip:hover true,
                 and ::before bridges the gap between the two so the pointer
                 never crosses dead ground. Without the bridge the gap breaks
                 the hover and the tooltip cannot be read by anyone who needs
                 to move onto it.
     DISMISSIBLE Escape must close it while the pointer or focus stays put.
                 CSS cannot see a key, so this is a SCRIPT CONTRACT: the
                 consumer adds .is-dismissed on Escape and removes it when the
                 trigger is left or blurred. The rule that honours the class is
                 below, written at a higher specificity than the :hover rule so
                 it cannot lose. /feedback/ drives it with real keys.
     PERSISTENT  it does not time out. Nothing here has a timeout.

   CONTENT RULE: a tooltip holds a short LABEL and nothing interactive. It is
   not focusable, it is not reachable by Tab, and it is announced through
   aria-describedby on the trigger. If it needs a link or a button inside it,
   it is a .popover -- that is the whole reason both exist.
   ------------------------------------------------------------------------- */

.tooltip {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.tooltip__bubble {
  position: absolute;
  inset-block-end: calc(100% + var(--sp-03));
  inset-inline-start: 50%;
  translate: -50% 0;
  z-index: var(--z-dropdown);
  /* A tooltip is a label, not a paragraph: capped at the second panel step
     steps (240px) so it wraps to two or three short lines instead of becoming
     a column of prose. The cap is also what keeps a bubble inside the viewport
     at 320px -- a hidden absolutely-positioned box still participates in
     layout, so an uncapped bubble widens the document at EVERY width whether
     or not anyone hovers it. That is the defect .nav__menu records at its own
     min-width and it applies here for the same reason. */
  inline-size: max-content;
  max-inline-size: var(--panel-02);
  padding: var(--sp-03) var(--sp-04);
  background: var(--layer-03);
  color: var(--text-primary);
  /* A chip floating over arbitrary content needs a control-grade edge: 1.4.11
     wants 3:1 on the boundary that identifies it, and --border-strong measures
     1.25:1. Same reasoning as .badge and .doc-flag. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  text-align: start;
  overflow-wrap: break-word;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  /* Rule 2 at the top of this file. Opening is instant visibility; closing
     delays it by one duration so the fade is seen. */
  transition: opacity var(--dur-fast) var(--ease),
              visibility 0s linear var(--dur-fast);
}
/* THE HOVER BRIDGE. The --sp-03 gap between trigger and bubble is dead ground
   a pointer must cross, and crossing it ends :hover -- so the tooltip closes
   just as the reader reaches for it. This pseudo-element fills the gap, is
   part of the bubble for hit-testing, and is invisible. */
.tooltip__bubble::before {
  content: "";
  position: absolute;
  inset-block-start: 100%;
  inset-inline: 0;
  block-size: var(--sp-03);
}
/* ESCAPE IS WRITTEN AS AN EXCLUSION ON THE OPEN RULES, NOT AS A LOUDER RULE
   THAT FOLLOWS THEM -- and that is a correction, not a preference. The first
   version of this was `.tooltip.is-dismissed .tooltip__bubble { visibility:
   hidden }` written after the two rules below, on the reasoning that three
   classes beat two. It does not: `:has()` takes the specificity of its most
   specific argument, so the keyboard rule below counts .tooltip +
   .tooltip__trigger + :focus-visible + .tooltip__bubble = four classes and the
   dismiss rule counted three. Driven with a real Escape key, the label stayed
   on screen and the class did nothing -- a WCAG 1.4.13 failure that was
   invisible in the stylesheet and took a key press to find. Written as
   :not(.is-dismissed) the open rules simply stop matching, and there is no
   specificity race left to lose. */
.tooltip:not(.is-dismissed):hover .tooltip__bubble {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--dur-fast) var(--ease),
              visibility 0s;
}
/* THE KEYBOARD HALF, IN ITS OWN RULE ON PURPOSE. It is written with :has() so
   it is exactly ":the trigger has a VISIBLE focus ring" -- :focus-within would
   also fire after a mouse click and leave a label hanging over the page until
   something else took focus. An invalid selector invalidates the WHOLE rule it
   is written in, so putting :has() in the same selector list as the :hover rule
   above would take the pointer half down with it in a browser that does not
   support it. Separated, the worst case is a browser that loses the keyboard
   half rather than one that loses both. */
.tooltip:not(.is-dismissed):has(.tooltip__trigger:focus-visible) .tooltip__bubble {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--dur-fast) var(--ease),
              visibility 0s;
}
/* The dismissed state needs no rule of its own: with the exclusion above,
   neither open rule matches and the bubble's own hidden default applies. A
   dismissed tooltip therefore stays dismissed with the pointer still on the
   trigger, which is what 1.4.13 means by dismissible -- the reader gets the
   label out of the way without having to move the mouse off the thing they
   were reading about. The consumer adds the class on Escape to the tooltip
   that is SHOWING, and clears it on the way back IN -- pointerenter and
   focusin -- so returning to a trigger always works. Clearing it on the way
   OUT was tried and is wrong: Escape pressed anywhere then leaves every
   untouched tooltip on the page permanently dismissed. */

/* Placements. Top is the default because it is the one that does not cover the
   next control down. */
.tooltip--bottom .tooltip__bubble {
  inset-block-end: auto;
  inset-block-start: calc(100% + var(--sp-03));
}
.tooltip--bottom .tooltip__bubble::before {
  inset-block-start: auto;
  inset-block-end: 100%;
}
.tooltip--end .tooltip__bubble {
  inset-block-end: auto;
  inset-block-start: 50%;
  inset-inline-start: calc(100% + var(--sp-03));
  translate: 0 -50%;
}
.tooltip--end .tooltip__bubble::before {
  inset-block: 0;
  inset-inline-start: auto;
  inset-inline-end: 100%;
  inline-size: var(--sp-03);
  block-size: auto;
}

/* THERE IS NO ROOM BESIDE A TRIGGER ON A PHONE, and a bubble that does not fit
   is not a placement, it is a horizontal scrollbar: measured at 320px, the
   side-placed bubble ended 65px past the right edge of the viewport and took
   the whole document with it. Below the medium breakpoint every placement
   falls back to the default, above the trigger, and the cap comes down with it
   so a bubble anchored anywhere in a 320px row still lands inside it. This is
   the same defect, and the same answer, that the dropdown menu records at its
   own minimum width -- an overlay is anchored to its trigger and its trigger
   can be anywhere. */
@media (max-width: 671px) {
  .tooltip__bubble { max-inline-size: var(--panel-01); }
  .tooltip--end .tooltip__bubble {
    inset-block-start: auto;
    inset-block-end: calc(100% + var(--sp-03));
    inset-inline-start: 50%;
    translate: -50% 0;
  }
  .tooltip--end .tooltip__bubble::before {
    inset-block-start: 100%;
    inset-block-end: auto;
    inset-inline: 0;
    inline-size: auto;
    block-size: var(--sp-03);
  }
}

/* The trigger. A tooltip can sit on any control; when it sits on a word in
   running text that word must still look like something you can interrogate,
   and a dotted underline is the convention for exactly that. It is a BUTTON in
   the markup, never a bare span, so it is focusable and announceable. */
.tooltip__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-02);
  padding: 0;
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-decoration: underline dotted;
  text-decoration-thickness: var(--border-w);
  text-underline-offset: calc(var(--border-w) * 3);
  cursor: help;
}


/* =============================================================================
   4. POPOVER
   =============================================================================
   RICHER THAN A TOOLTIP: it may hold links, buttons, a small form. That single
   difference is why it cannot be built the way the tooltip is built.

     - It must not open on hover alone. Content you can interact with must not
       appear and disappear as the pointer passes over it.
     - Focus MOVES INTO it on open -- to the panel itself, which carries
       tabindex="-1" -- so the keyboard reaches the controls inside without
       Tabbing through the rest of the page to find them.
     - Escape closes it and RETURNS focus to the trigger.
     - It does NOT trap focus. Tab moves out of it, and moving out closes it.
       Only a modal traps.
     - The trigger carries aria-expanded, which is what tells assistive
       technology the panel exists and whether it is open.

   The open state is a CLASS plus that attribute, not a pseudo-class, because
   none of the above can be expressed in CSS alone. The contract for a consumer
   is three lines of script and it is written out on /feedback/.

   WHY NOT THE POPOVER API OR ANCHOR POSITIONING. popover="auto" would give
   light-dismiss, Escape and the top layer for free, and CSS anchor positioning
   would place the panel without a positioned wrapper -- but anchor positioning
   is not yet available in every browser this system supports, and a component
   that silently renders in the wrong place in a browser a government buyer
   uses is worse than one that is placed the old way. The wrapper is relative,
   the panel is absolute, and the whole thing works everywhere.
   ------------------------------------------------------------------------- */

.popover { position: relative; display: inline-flex; }

.popover__panel {
  position: absolute;
  inset-block-start: calc(100% + var(--sp-03));
  inset-inline-start: 0;
  /* A menu over the page. This is the step --z-dropdown names and the panel is
     literally that. The toast wanted a step above it, and the ladder has one
     now: --z-toast at 120. */
  z-index: var(--z-dropdown);
  inline-size: max-content;
  /* --panel-03 is 320px, the system's smallest supported viewport, and
     the second half of the min() keeps it inside one at that width. */
  max-inline-size: min(var(--panel-03), calc(100vw - var(--sp-07)));
  padding: var(--sp-05);
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md);
  text-align: start;
  opacity: 0;
  visibility: hidden;
  transform: translateY(calc(var(--sp-02) * -1));
  /* Rule 2. Opening is `visibility 0s` -- a panel that stays hidden for 180ms
     after it opens has no focusable children for 180ms, and the script's
     focus() call into it lands nowhere. */
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              visibility 0s linear var(--dur);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.popover.is-open .popover__panel {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              visibility 0s;
}
/* The panel takes focus on open, so it takes a focus ring. It is a container
   rather than a control, so the ring sits INSIDE its box where the panel's own
   edge cannot clip it -- the same --focus-offset-inset a field uses. */
.popover__panel:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset-inset);
}
/* Right-aligned to its trigger, for a panel near the end of a row. */
.popover--end .popover__panel { inset-inline-start: auto; inset-inline-end: 0; }
/* Above the trigger, for one near the bottom of a page. */
.popover--top .popover__panel {
  inset-block-start: auto;
  inset-block-end: calc(100% + var(--sp-03));
  transform: translateY(var(--sp-02));
}
.popover--top.is-open .popover__panel { transform: translateY(0); }

/* BELOW THE MEDIUM BREAKPOINT THE PANEL LEAVES ITS TRIGGER AND BECOMES A
   SHEET, AND THAT IS THE ONLY ARRANGEMENT THAT IS ALWAYS CORRECT.

   The problem: an anchored panel's right edge is the TRIGGER'S x plus the
   PANEL'S width, and a component owns only the second of those. At 320px, a
   trigger 57px in with a 288px panel measured 345px -- 25px past the edge of
   the viewport, which is a horizontal scrollbar on the whole document rather
   than a panel that is slightly too wide. Capping the width does not fix it:
   whatever the cap, a trigger further along the row overflows again.

   The first attempt was .nav__menu's D20b treatment -- `position: fixed` with
   `left`/`right` gutters and `top: auto`. That is right for a menu in a bar at
   the top of the page and WRONG here, and the measurement says so plainly:
   `top: auto` on a fixed box resolves to the element's static position in the
   DOCUMENT, so a popover this far down the page computed `top: 10118px` and
   rendered a full viewport-height below the screen. It was invisible, the
   computed style said `visibility: visible`, and only a screenshot found it.
   (The same rule is latent in .nav__menu; it survives there because that menu
   is a few hundred pixels from the top of the document. Recorded for the file
   that owns it.)

   Every offset here is explicit, so there is no static position to resolve and
   no trigger position to overflow from. A panel that becomes a bottom sheet on
   a phone is also the platform convention, so the fallback is not a
   compromise. */
@media (max-width: 671px) {
  .popover__panel,
  .popover--end .popover__panel,
  .popover--top .popover__panel {
    position: fixed;
    inset-block-start: auto;
    inset-block-end: 0;
    inset-inline: 0;
    inline-size: auto;
    max-inline-size: none;
    max-block-size: calc(100% - var(--sp-10));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--sp-06);
    /* Attached to the edge: the edges it meets lose their border. */
    border-inline: 0;
    border-block-end: 0;
    transform: translateY(var(--sp-04));
  }
}

.popover__header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-05);
  margin-block-end: var(--sp-04);
}
.popover__title {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
  font-size: var(--ts-03);
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-tight);
  color: var(--text-primary);
}
.popover__body {
  margin: 0;
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.popover__body p { margin: 0 0 var(--sp-04); max-width: none; }
.popover__body > :last-child { margin-bottom: 0; }
.popover__actions { padding: var(--sp-05) 0 0; }
/* The close control in a popover is smaller than a panel's: the panel itself
   is only 320px wide and a 40px target beside a 16px title unbalances it. 32px
   is still above the 24px minimum of WCAG 2.5.8. This is the only statement of
   this control's size -- see the close control note in section 1. */
.popover__close {
  inline-size: var(--sp-07);
  block-size: var(--sp-07);
  margin: calc(var(--sp-01) * -1) calc(var(--sp-03) * -1) 0 0;
}


/* =============================================================================
   5. TOAST
   =============================================================================
   A TRANSIENT NOTIFICATION, AND THE RULE THAT GOVERNS IT:

     NOTHING THE USER MUST ACT ON MAY AUTO-DISMISS.

   A toast that disappears on a timer is a message the reader may never have
   seen -- they were looking at the other half of the screen, or reading with a
   magnifier, or their screen reader was mid-sentence. So:
     - a toast that merely CONFIRMS something ("Saved") may time out, and four
       to six seconds is the usual figure;
     - a toast carrying an ERROR, a WARNING, or any action the reader has to
       take, MUST NOT. It stays until it is dismissed. That is why every toast
       below ships a dismiss control and why .toast--error and .toast--warning
       are documented as persistent.
   There is no CSS that can enforce this -- a timeout is script -- so it is
   stated at the component, demonstrated on /feedback/, and the
   dismiss control is not optional.

   ANNOUNCEMENT. The REGION carries the live region, not the toast: an element
   that is inserted INTO an existing live region is announced, whereas an
   element that arrives already carrying aria-live frequently is not, because
   the assistive technology never saw it appear. So .toast-region is in the DOM
   from page load with role="status" (which implies aria-live="polite") and
   toasts are appended into it. A region for errors carries
   aria-live="assertive" -- one region each, because the politeness setting is
   a property of the region and cannot be changed usefully per message.

   NO LEFT-EDGE STRIPE. The status is the __kind label -- the word Error,
   Warning, Success, Information -- in the status colour, over a uniform
   hairline. This is the single most common place a system gets it wrong.
   ------------------------------------------------------------------------- */

.toast-region {
  position: fixed;
  inset-block-end: var(--sp-06);
  inset-inline-end: var(--sp-06);
  inset-inline-start: auto;
  /* THE LAYER PROBLEM. A toast sits above everything on the page. The ladder
     offered --z-dropdown (a menu, 60) and --z-skip (the skip link, over all of
     it) and nothing between, so this used --z-dropdown and reported the gap
     rather than typing a number. --z-toast now exists at 120 and IS ADOPTED
     below -- the component and the ladder agree, and every page that said
     otherwise has been corrected.
     The harder half of the problem is not a z-index at all: a modal <dialog>
     renders in the TOP LAYER, which beats every z-index there can be. The
     complete fix is to promote the region into the top layer too, and the
     Popover API does that without making it modal:

         if (region.showPopover) {
           region.setAttribute('popover', 'manual');
           region.showPopover();
         }

     run once at page load, before the first toast. The attribute is
     deliberately NOT in the static markup: a popover is display:none until it
     is shown, so a page whose script failed would have a live region that
     never renders and toasts nobody can see. Without the script the region is
     an ordinary fixed element at --z-toast and everything works except the
     modal case. */
  z-index: var(--z-toast);
  /* NEWEST NEAREST THE CORNER, AND NEVER A REVERSED STACK. A toast is
     appended, so it is last in the DOM; in a region anchored to the bottom,
     plain `column` puts it last down the screen, which is both the convention
     and the corner nearest the reader's attention. `column-reverse` was tried
     and is wrong twice over: it put the NEWEST furthest from the corner
     (verified in a screenshot -- three toasts raised in order rendered
     oldest-at-the-bottom), and it makes the visual order disagree with the
     DOM order a screen reader announces, which is a 1.3.2 failure in exchange
     for nothing. The --top variant keeps `column` for the same reason. */
  display: flex;
  flex-direction: column;
  gap: var(--sp-04);
  inline-size: min(var(--panel-04), calc(100% - var(--sp-07)));
  max-block-size: calc(100% - var(--sp-07));
  overflow-y: auto;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  /* The region covers a strip of the page even when it holds nothing, so it
     must not eat clicks. Each toast takes its own pointer events back. */
  pointer-events: none;
  list-style: none;
}
/* The Popover API's UA styles are a full-viewport inset:0 box; when the region
   is promoted into the top layer it keeps the geometry above. */
.toast-region:popover-open {
  inset-block-start: auto;
  inset-inline-start: auto;
}
.toast-region--start { inset-inline-start: var(--sp-06); inset-inline-end: auto; }
.toast-region--top {
  inset-block-start: var(--sp-06);
  inset-block-end: auto;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-04);
  padding: var(--sp-05);
  background: var(--layer-02);
  /* UNIFORM. All four sides, one role, every variant. The variant recolours
     the LABEL -- no stripe, no fill, no coloured edge.
     THE ROLE IS --border-interactive, AND IT WAS --border-strong, WHICH
     MEASURES 1.25 TO 1. This is the one surface in the layer that arrives
     unasked, is anchored to nothing, and has no scrim under it: a modal and a
     drawer are separated by --overlay, and a popover is attached to a trigger
     the reader has just pressed. A toast is a box that appears in a corner over
     whatever happened to be there. --shadow-md is `none` in four of the six
     themes, so in those the border IS the boundary, and in light it measured
     1.25 to 1 over a 1.05 fill -- a notification the reader has to find, drawn
     with an edge that is not one. 1.4.11 asks 3:1 of the boundary that
     identifies a component, and the tooltip in this same file already took
     --border-interactive for exactly this reason; the toast had the stronger
     case and the weaker edge. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md);
  pointer-events: auto;
  /* The entrance. Reduced motion is handled globally by senua-base.css. */
  animation: senua-toast-in var(--dur) var(--ease) both;

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
@keyframes senua-toast-in {
  from { opacity: 0; transform: translateY(var(--sp-04)); }
  to   { opacity: 1; transform: translateY(0); }
}
.toast__content { flex: 1 1 auto; min-inline-size: 0; }
.toast__title {
  margin: 0;
  font-size: var(--ts-02);
  font-weight: 600;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-primary);
}
.toast__body {
  margin: var(--sp-02) 0 0;
  max-width: none;
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.toast__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-03);
  margin-block-start: var(--sp-04);
}
/* THE STATUS, CARRIED BY THE WORD. Four variants, four label colours, one
   border role. Nothing else changes -- no fill, no stripe, no coloured edge. */
.toast--info    .toast__kind { color: var(--support-info); }
.toast--success .toast__kind { color: var(--support-success); }
.toast--warning .toast__kind { color: var(--support-warning); }
.toast--error   .toast__kind { color: var(--support-error); }
/* The dismiss control sits level with the label rather than hanging below it,
   and takes the popover's 32px for the same reason: a toast is one line, and a
   40px target beside it unbalances the row. This is the only statement of this
   control's size -- see the close control note in section 1. */
.toast__dismiss {
  inline-size: var(--sp-07);
  block-size: var(--sp-07);
  margin: calc(var(--sp-01) * -1) calc(var(--sp-02) * -1) 0 0;
}


/* =============================================================================
   6. PROGRESS
   =============================================================================
   DETERMINATE IS THE NATIVE <progress> ELEMENT, and that is a decision about
   inline styles, not about taste. The width of a fill has to come from
   somewhere, and the two usual answers are a style attribute (banned outright
   in this system) or a custom property set from script (still a style
   attribute). <progress value="60" max="100"> puts the number in the MARKUP
   where it belongs, the browser draws the fill, and the element reports its
   own role, value, minimum and maximum to assistive technology with no ARIA at
   all. A consumer updates one attribute.

   INDETERMINATE CANNOT BE THAT ELEMENT. With appearance:none an indeterminate
   <progress> has no value to paint, so Chrome renders an empty track -- a
   component that says "busy" by showing nothing. The indeterminate variant is
   therefore a div with role="progressbar" and NO aria-valuenow, which is
   precisely how ARIA expresses "the value is unknown", plus an indicator that
   travels. Both variants take .progress so the track is the same object.
   ------------------------------------------------------------------------- */

.progress {
  display: block;
  inline-size: 100%;
  block-size: var(--sp-03);
  background: var(--layer-03);
  border: var(--border-w) solid var(--border-subtle);
  /* SQUARE, like everything else. It was --r-pill, which is the radius the
     tokens reserve for a status pill "where the shape carries meaning" -- a
     progress track carries no meaning in its shape and a rounded bar in a
     square system reads as imported from somewhere else. The spinner below
     keeps --r-pill because a ring is a circle by geometry, not by decoration. */
  border-radius: var(--r-sm);
  overflow: hidden;
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* The native element, unstyled, is an OS control: a rounded gradient in the
   platform's own blue, which is neither the brand nor the theme. */
progress.progress {
  appearance: none;
  -webkit-appearance: none;
  color: var(--accent);
}
progress.progress::-webkit-progress-bar { background: var(--layer-03); }
progress.progress::-webkit-progress-value {
  background: var(--accent);
  transition: inline-size var(--dur) var(--ease);
}
progress.progress::-moz-progress-bar { background: var(--accent); }

/* The indeterminate indicator. A third of the track, travelling across it, on
   a loop slow enough to read: --dur is a TRANSITION duration and strobes as a
   loop, so the period is --dur-slow (1440ms) -- a token that did not exist when
   this was written and does now. */
.progress--indeterminate { position: relative; }
.progress__indicator {
  display: block;
  position: absolute;
  inset-block: 0;
  inline-size: 34%;
  background: var(--accent);
  animation: senua-progress-slide var(--dur-slow) var(--ease) infinite;
}
@keyframes senua-progress-slide {
  from { inset-inline-start: -34%; }
  to   { inset-inline-start: 100%; }
}

/* The label row. A bare bar is not a status: a progress indicator needs to say
   what is progressing and, when it is determinate, where it has got to. The
   percentage is TEXT, so it is available to a reader who cannot see the bar,
   and it is tabular so the figure does not jitter as it counts. */
.progress-group { display: grid; gap: var(--sp-03); }
.progress__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-05);
}
.progress__label {
  margin: 0;
  font-size: var(--ts-02);
  font-weight: 500;
  line-height: var(--lh-compact);
  color: var(--text-primary);
}
.progress__value {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  line-height: var(--lh-compact);
  letter-spacing: 0;
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
}


/* =============================================================================
   7. SPINNER
   =============================================================================
   The indeterminate busy indicator, for the moments too short for a progress
   bar to mean anything.

   IT IS NEVER ALONE AND NEVER SILENT. A spinning ring with no text is a
   component that tells a screen reader nothing at all, so the markup contract
   is a spinner plus a label -- visible, or .visually-hidden if the layout
   genuinely cannot carry one -- inside a container carrying role="status".
   .spinner-inline below is that pairing, shipped so nobody has to invent it.

   REDUCED MOTION IS THE INTERESTING CASE. senua-base.css reduces every
   animation to .01ms, which for a component whose entire appearance IS an
   animation means it stops dead -- and a stopped spinner reads as a frozen
   interface, which is the opposite of what it is for. So the reduced-motion
   block below replaces the spin with a static ring, and the LABEL is what
   carries "busy" in that mode. That is also the honest answer for anyone using
   a screen reader, who never saw the animation to begin with.
   ------------------------------------------------------------------------- */

.spinner {
  display: inline-block;
  flex: 0 0 auto;
  inline-size: var(--sp-06);
  block-size: var(--sp-06);
  /* A ring with one quarter in the accent: the gap is what makes rotation
     visible. Two hairlines, derived from --border-w so the system's line
     weight moves together. */
  border: calc(var(--border-w) * 2) solid var(--border-strong);
  border-block-start-color: var(--accent);
  border-radius: var(--r-pill);
  animation: senua-spin var(--dur-loop) linear infinite;
}
@keyframes senua-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.spinner--sm { inline-size: var(--sp-05); block-size: var(--sp-05); }
.spinner--lg {
  inline-size: var(--sp-08);
  block-size: var(--sp-08);
  border-width: calc(var(--border-w) * 3);
}
/* On an accent fill -- a primary button mid-submit -- the ring has to be drawn
   in the on-accent role or it disappears into the button. */
.spinner--on-accent {
  border-color: var(--text-on-media-3);
  border-block-start-color: var(--text-on-accent);
}

.spinner-inline {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-04);
  font-size: var(--ts-02);
  line-height: var(--lh-compact);
  color: var(--text-secondary);
}


/* =============================================================================
   8. SKELETON
   =============================================================================
   A placeholder with the SHAPE of the content that is coming. Its value is
   that the layout does not jump when the content lands, which means a skeleton
   that does not match what replaces it is worse than a spinner: it promises a
   shape and then moves everything.

   ACCESSIBILITY CONTRACT: a skeleton is decoration. The container is
   aria-hidden="true" or aria-busy="true", and the "Loading" message a screen
   reader needs comes from a live region beside it -- not from the placeholder
   boxes, which would announce as a row of empty elements.

   REDUCED MOTION: same reasoning as the spinner. The global rule stops the
   shimmer; the block below makes the resting appearance a deliberate flat
   surface rather than a frozen gradient caught mid-sweep.
   ------------------------------------------------------------------------- */

/* THE TWO ENDS OF THE SWEEP ARE NAMED, NOT SPELT OUT IN THE GRADIENT. A
   placeholder has to sit one rung above whatever is behind it, and inside a
   card that is a different pair of rungs from the page -- so the pair is two
   custom properties and the card variant re-points them. Written as literal
   var(--layer-02)/var(--layer-03) stops, the only way to move a skeleton up a
   rung was to overwrite `background`, which is a SHORTHAND: it resets
   background-image to none and takes the shimmer with it. That is exactly what
   the card fix did -- the lines became visible and stopped moving, so a
   loading card was four static bars while the same component elsewhere swept.
   Nothing failed; one of the two things the component does had quietly gone. */
.skeleton {
  --skeleton-rest: var(--layer-02);
  --skeleton-sweep: var(--layer-03);
  display: block;
  inline-size: 100%;
  block-size: var(--sp-05);
  border-radius: var(--r-sm);
  /* The sweep is two layer roles, so it is visible in every theme without a
     single literal colour. 200% background with the position animated is the
     cheapest shimmer there is: no layout, no paint of anything but this box. */
  background-color: var(--skeleton-rest);
  background-image: linear-gradient(90deg,
                    var(--skeleton-rest) 0%,
                    var(--skeleton-sweep) 50%,
                    var(--skeleton-rest) 100%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: senua-skeleton var(--dur-slow) var(--ease) infinite;
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
@keyframes senua-skeleton {
  from { background-position: 100% 0; }
  to   { background-position: -100% 0; }
}
/* A line of text. --sp-05 is 16px, which is the body step, so a run of these
   occupies the space the sentences will. */
.skeleton--text  { block-size: var(--sp-05); }
/* A heading line. */
.skeleton--title { block-size: var(--sp-06); }
/* A block: an image, a chart, a map. */
.skeleton--block { block-size: var(--sp-12); border-radius: var(--r-md); }
/* THE LAST LINE OF A PARAGRAPH IS SHORT. A stack of identical full-width bars
   reads as a table, not as prose; one short line is what makes it read as text
   and it costs one class. */
.skeleton--short { inline-size: 60%; }
.skeleton--half  { inline-size: 50%; }

/* A stack of lines with the paragraph rhythm between them. */
.skeleton-lines { display: grid; gap: var(--sp-04); }

/* A CARD-SHAPED PLACEHOLDER. It carries the real .card geometry -- the same
   surface, hairline and --sp-07 padding -- because a skeleton whose padding
   differs from the component it stands in for moves the page when the content
   arrives, which is the one thing a skeleton exists to prevent. */
.skeleton--card {
  display: grid;
  gap: var(--sp-05);
  padding: var(--sp-07);
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg);
  /* The container is a card, not a shimmering box: the shimmer belongs to the
     lines inside it. */
  background-image: none;
  animation: none;
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
/* A placeholder inside a card has to sit a layer ABOVE the card, not on it.
   Both were --layer-02, so the card rendered as an empty box: four
   placeholder lines painted in exactly the colour behind them. The skeleton
   was there the whole time and nothing could see it, which is the same class
   of defect as a swatch painting a token that does not exist.

   THE WHOLE SWEEP MOVES UP, NOT JUST THE FILL. The first fix was `background:
   var(--layer-03)` -- correct about the rung and wrong about the property: the
   shorthand reset background-image, so the lines became visible and stopped
   moving. A loading card was four static bars while the identical component on
   the page beside it swept, which reads as a card that has already finished
   loading and is simply empty. Re-pointing the two named stops keeps both.

   The sweep runs DOWN the ladder here because there is no rung above --layer-03
   to run up to. Its amplitude is one rung either way, so a card skeleton and a
   page skeleton shimmer by the same amount; the ground is the far end of the
   sweep rather than outside it, which is the narrowest crossing available and
   the reason it is this rung rather than --layer-01 two rungs down. A fifth
   surface rung is the same shape of gap as --col-min-* at the top of this
   file: recorded, not invented. */
.skeleton--card .skeleton {
  --skeleton-rest: var(--layer-03);
  --skeleton-sweep: var(--layer-02);
}

@media (prefers-reduced-motion: reduce) {
  /* The global rule in senua-base.css already stops these. What it cannot do
     is decide what they should look like when stopped -- a gradient frozen
     mid-sweep is an artefact, not a design. A flat layer-03 surface reads as
     "something will be here", which is the whole message.
     --layer-03 rather than either end of the sweep, deliberately: it is the
     one rung that separates from BOTH grounds a skeleton sits on -- the page
     at --layer-01 and the card at --layer-02 -- so the still frame is legible
     in both arrangements and in all six themes. Taking var(--skeleton-sweep)
     here would paint a card's placeholder in the card's own colour, which is
     the defect this section was fixed for, reintroduced for the readers who
     can least afford it. */
  .skeleton {
    background-image: none;
    background-color: var(--layer-03);
  }
  /* A stopped ring still has to say "busy". The full ring in --border-strong
     with one quarter in the accent is legible as an indicator at rest, and the
     label beside it -- which the markup contract requires -- carries the
     meaning for everyone who cannot see it anyway. */
  .spinner { border-block-start-color: var(--accent); }
  /* The travelling indicator stops. It is pinned to the START of the track
     rather than filled across it: a full-width accent bar reads as COMPLETE,
     which is the one thing an indeterminate indicator must never say. A third
     of a track, stationary, beside the label the markup contract requires,
     says "running, position unknown" and cannot be misread. */
  .progress__indicator { inset-inline-start: 0; }
}


/* =============================================================================
   9. ACCORDION
   =============================================================================
   ON <details>/<summary>, because the element already does the work: the
   summary is a button in the accessibility tree, it reports expanded and
   collapsed, Enter and Space toggle it, the content is found by in-page search
   in browsers that support it, and it prints open. A div with a click handler
   reproduces none of that and gets the keyboard wrong more often than not.

   SINGLE-OPEN IS AN ATTRIBUTE, NOT A SCRIPT. Give every <details> in the group
   the same `name` and the browser enforces one-at-a-time, with no JavaScript
   and no listener to get wrong. Omit the name and the group is multi-open.
   That is the entire difference between the two variants, which is why there
   is no .accordion--single class: a class that only tells you to add an
   attribute is a class that will be out of step with the attribute.

   THE MARKER IS OURS. The UA triangle is a platform glyph in a platform
   colour, so it is removed -- `list-style: none` plus the WebKit pseudo, both
   of which are needed -- and replaced with an inline SVG chevron that rotates.
   No unicode arrow: an arrow glyph is a character a screen reader may read
   aloud, and its shape is whatever font happens to be loaded.
   ------------------------------------------------------------------------- */

.accordion { display: block; }

.accordion__item {
  display: block;
  border-block-end: var(--border-w) solid var(--border-subtle);
}
.accordion__item:first-of-type { border-block-start: var(--border-w) solid var(--border-subtle); }

.accordion__summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-05);
  padding: var(--sp-05) 0;
  cursor: pointer;
  font-size: var(--ts-03);
  font-weight: 500;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-primary);
  /* Both are required to remove the UA marker: Safari and older WebKit ignore
     the first, Firefox and Chrome ignore the second. */
  list-style: none;
  transition: color var(--dur-fast) var(--ease);
}
.accordion__summary::-webkit-details-marker { display: none; }
.accordion__summary::marker { content: ""; }
.accordion__summary:hover { color: var(--accent-text); }
/* The ring sits inside the row so it is not clipped by the hairline above it
   and does not overlap the item before. */
.accordion__summary:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset-inset);
}
.accordion__title {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}
.accordion__icon {
  flex: 0 0 auto;
  inline-size: var(--sp-05);
  block-size: var(--sp-05);
  /* The chevron sits on the cap height of a 16px row rather than on its top. */
  margin-block-start: var(--sp-01);
  fill: none;
  stroke: currentColor;
  stroke-width: var(--border-w);
  color: var(--text-helper);
  transition: transform var(--dur) var(--ease);
}
.accordion__item[open] > .accordion__summary .accordion__icon { transform: rotate(180deg); }
.accordion__item[open] > .accordion__summary { color: var(--text-primary); }

.accordion__body {
  padding: 0 0 var(--sp-06);
  font-size: var(--ts-02);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.accordion__body > :last-child { margin-bottom: 0; }
.accordion__body p { margin: 0 0 var(--sp-05); }

/* BORDERED. The group is a framed object rather than a run of rules, for a
   panel that sits on its own. The item hairlines become the dividers between
   rows and the frame supplies the outer edges, so nothing doubles.

   THE SURFACE IS --layer-02, AND IT WAS --layer-01, WHICH IS THE GROUND. Every
   other framed surface in this layer -- the modal panel, the drawer panel, the
   popover panel, the toast, the skeleton card -- is --layer-02, and this one
   was a rung lower. Against a page it was one faint rung; inside anything that
   is itself --layer-01, which includes a docs example frame and any consumer's
   sectioned page, it measured 1.00 to 1: a panel painting exactly the colour
   behind it, with only a --border-subtle hairline to say it was there at all.
   Found by rendering it open in all six themes rather than by reading it -- the
   same defect as four skeleton lines on a card that matched the card. */
.accordion--bordered {
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-lg);
  background: var(--layer-02);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.accordion--bordered .accordion__item { border-block-end: var(--border-w) solid var(--border-subtle); }
.accordion--bordered .accordion__item:first-of-type { border-block-start: 0; }
.accordion--bordered .accordion__item:last-of-type { border-block-end: 0; }
.accordion--bordered .accordion__summary { padding: var(--sp-05) var(--sp-06); }
.accordion--bordered .accordion__body { padding: 0 var(--sp-06) var(--sp-06); }

/* FLUSH. No rules at all, for a sidebar or a footer where the surrounding
   layout already separates things and a run of hairlines would be noise. */
.accordion--flush .accordion__item,
.accordion--flush .accordion__item:first-of-type { border: 0; }
.accordion--flush .accordion__summary { padding-block: var(--sp-04); }
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- forms
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. Concatenated into css/senua.css by scripts/build.sh -- never edit
   css/senua.css directly, it is a build artifact.
   ============================================================================= */

/* WHAT THIS LAYER OWNS, AND WHAT IT DELIBERATELY DOES NOT.
   senua-components.css already ships the text field (.field and its label,
   hint, error, required and disabled parts), the checkbox and radio row
   (.check), the segmented control (fieldset.seg), the status label (.badge),
   the data table (.table, .table-wrap, .num, .table--fixed) and the button.
   NONE of those is rebuilt here. This layer adds the controls a real product
   form needs that the system had no answer for -- switch, slider, search, file,
   date and time, tag, stepper -- plus the modifiers that turn .table into a
   working data grid, the keyboard key, and the layout rules for a form.

   Two rules govern every override below, and both come from defects measured
   this week:

   1. SPECIFICITY IS ARITHMETIC, NOT INTENT. `.field input:not([type="checkbox"])
      :not([type="radio"])` scores (0,3,1) and sets the padding, background,
      colour, font and border SHORTHANDS. Any rule here that has to change one
      of those on a field input must score above (0,3,1) or it will be typed,
      shipped, and never render. Every such selector below carries a comment
      with its count. The failure mode is silent: an errored input once rendered
      byte-identical to a valid one for exactly this reason.

   2. NO LEFT-EDGE ACCENT BARS, ANYWHERE. Not on a drop zone, not on a selected
      table row, not on a stepper step. State is carried by a word and by a
      uniform border, which is what survives a monochrome print and a
      colour-blind reader.

   A vendor prefix is not decoration here: a range input is drawn by two
   different engines through two different pseudo-element trees, and a selector
   list containing one engine's pseudo-element is INVALID AS A WHOLE in the
   other. That is why the WebKit and Firefox blocks below are written out twice
   rather than merged -- merging them silently deletes the styling in both. */


/* --- switch ----------------------------------------------------------------
   A toggle with IMMEDIATE effect. The distinction is not cosmetic:

     switch    the change happens when you flip it. There is no Save. Use it
               for "stream telemetry to the console" or "enforce the policy" --
               something the system acts on at once and can be flipped back.
     checkbox  the change is DEFERRED and submitted with the form. Use .check
               for "I accept", "email me the release notes", anything that only
               takes effect on Submit.

   Put a switch in a form that has a Save button and the reader cannot tell
   whether the setting is already live. That is the whole rule.

   It is built on a real <input type="checkbox" role="switch">, not a div: the
   native control keeps Space, keeps the checked state in the accessibility
   tree, keeps the label association and keeps form submission. role="switch"
   only changes how it is ANNOUNCED -- on/off instead of checked/unchecked.

   PSEUDO-ELEMENTS DO NOT RENDER ON AN <input>, so the thumb cannot be a
   ::before. It is a background image -- a gradient of a single role painted at
   a fixed size and moved by background-position, which is the only technique
   that keeps the control one element and therefore keeps it native. */
.switch {
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  margin-bottom: var(--sp-05);
}
/* The settings-row form: label first, control pushed to the end. Same markup
   order as it is read in, which is why there is no `order` here -- reordering
   in CSS separates the tab order from the reading order. */
.switch--row { justify-content: space-between; }
.switch__input {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  margin: 0;
  cursor: pointer;
  inline-size: var(--sp-08);   /* 40 */
  block-size: var(--sp-06);    /* 24 */
  /* SQUARE, like every other control in this system. Carbon's own toggle is a
     pill and this one is not: the tokens file names exactly one exception to
     zero radius -- a status pill, where the silhouette carries meaning -- and
     inventing a second exception for a switch would make the rule a preference
     again. The state is carried by the thumb's POSITION and the track's fill,
     both of which survive in monochrome; the silhouette is not doing work. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
  background-color: var(--field);
  background-image: linear-gradient(var(--text-helper), var(--text-helper));
  background-repeat: no-repeat;
  background-size: var(--sp-05) var(--sp-05);   /* 16 thumb */
  background-position: var(--sp-02) center;     /*  4 inset */
  transition: background-color var(--dur) var(--ease),
              background-position var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
  /* `inherit` rather than a colour, stated so it is a decision and not an
     omission: nothing here renders text of its own. See tests/surface.js. */
  color: inherit;
}
.switch__input:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image: linear-gradient(var(--text-on-accent), var(--text-on-accent));
  background-position: calc(100% - var(--sp-02)) center;
}
/* The field convention, stated on interactive.html: a FIELD shows its ring on a
   mouse click as well as on a Tab, a BUTTON only on a Tab. A switch is a field.
   The ring is inset so it is never clipped by the row beside it. */
.switch__input:focus {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
}
.switch__input:disabled {
  cursor: not-allowed;
  border-color: var(--border-strong);
  background-color: var(--layer-01);
  background-image: linear-gradient(var(--text-disabled), var(--text-disabled));
}
/* A bare <label> inside .switch matches no rule in the system -- `.field label,
   .label` needs one of those two hooks -- so it is declared here, exactly as
   .check declares its own. Same 14px/500 as every other label in the system. */
.switch label,
.switch__label {
  display: block;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--ts-02);
  font-weight: 500;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-primary);
  cursor: pointer;
}
.switch__input:disabled ~ label,
.switch__input:disabled ~ .switch__label { color: var(--text-disabled); cursor: not-allowed; }
.switch__label--disabled { color: var(--text-disabled); cursor: not-allowed; }
/* A switch may carry a description; it sits under the label and is referenced
   by aria-describedby, so it is announced with the control rather than lost. */
.switch__hint {
  margin: var(--sp-02) 0 0;
  font-size: var(--ts-01);
  color: var(--text-helper);
}


/* --- slider ----------------------------------------------------------------
   A range input, for a value whose EXACT number does not matter as much as its
   position between two ends. If the exact number matters, ship a number field:
   nobody drags to 47.

   The value is displayed in an <output>, which is announced live. The system
   cannot update it -- CSS cannot read a control's value -- so wiring
   input -> output is a CONSUMER CONTRACT, one line of script, and a slider
   shipped without it is a slider whose value nobody can read.

   THE FILLED TRACK, in two engines and no script:
     Firefox   ::-moz-range-progress is a real element and is filled directly.
     WebKit    there is no progress pseudo-element. The thumb carries a
               box-shadow wider than any viewport and the track clips it, so
               everything to the left of the thumb is painted. --bp-max (1584px)
               is the widest width this system supports, so a shadow that wide
               can never fall short.
   The two blocks CANNOT be merged: a selector list containing an unknown
   pseudo-element is dropped in its entirety by the other engine. */
.slider { margin-bottom: var(--sp-06); }
.slider__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-05);
  margin-bottom: var(--sp-03);
}
.slider label,
.slider__label {
  display: block;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--ts-02);
  font-weight: 500;
  line-height: var(--lh-compact);
  letter-spacing: var(--tr-body);
  color: var(--text-primary);
}
.slider__value {
  font-family: var(--font-mono);
  font-size: var(--ts-02);
  font-variant-numeric: tabular-nums;   /* the number must not jump as it ticks */
  color: var(--text-primary);
}
.slider__scale {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-05);
  margin-top: var(--sp-02);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  color: var(--text-helper);
}
.slider__input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  inline-size: 100%;
  block-size: var(--sp-06);     /* room for a 16px thumb on a 4px track */
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
}
.slider__input:focus {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
}
.slider__input:disabled { cursor: not-allowed; }

/* WebKit / Blink.
   PHYSICAL PROPERTIES ONLY IN THESE BLOCKS. The logical forms -- inline-size,
   block-size, margin-block-start -- are IGNORED inside the range's UA shadow
   tree, and the failure is silent: the rule is typed, the pseudo-element is
   matched, and the thumb renders at its intrinsic 2px sliver instead. Found by
   looking at the rendered slider, not by reading the stylesheet; every other
   rule in this file uses the logical forms. */
.slider__input::-webkit-slider-runnable-track {
  height: var(--sp-03);
  background: var(--border-strong);
  border-radius: var(--r-sm);
  /* This is what makes the fill possible: it clips the thumb's shadow. */
  overflow: hidden;
}
.slider__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  /* A PADDLE, NOT A SQUARE, AND THE HEIGHT IS NOT A PREFERENCE. The track has
     to clip -- that is what turns the thumb's shadow into the filled portion --
     and a clip does not distinguish the shadow from the thumb that casts it. A
     16px thumb on a 4px track therefore rendered as a 4px sliver: the rule was
     right, the geometry was impossible. Found by looking at the slider, not by
     reading the stylesheet. So the thumb is as TALL as the rail and twice as
     WIDE, which is a handle the eye reads at a glance and the clip cannot
     truncate. Firefox does not clip, and takes the same geometry so the control
     is one control in both engines. */
  width: var(--sp-05);
  height: var(--sp-03);
  background: var(--background);
  border: var(--border-w) solid var(--text-primary);
  border-radius: var(--r-sm);
  box-shadow: calc(-1 * var(--bp-max)) 0 0 var(--bp-max) var(--accent);
}
.slider__input:disabled::-webkit-slider-runnable-track { background: var(--border-subtle); }
.slider__input:disabled::-webkit-slider-thumb {
  background: var(--layer-03);
  border-color: var(--text-disabled);
  box-shadow: calc(-1 * var(--bp-max)) 0 0 var(--bp-max) var(--border-strong);
}

/* Firefox. Written out separately -- see the note above.

   VERIFIED, AND IT WAS NOT BEFORE. These four rules were written against the
   documentation and never rendered in the engine they exist for, which is the
   same standing as not having written them: the WebKit block above is a list of
   three separate defects that only a Gecko-shaped rendering would have caught,
   and all three were found by looking at a picture rather than by reading the
   rule. So the gap was closed the only way it can be -- by installing Gecko and
   putting the control in front of it.

   Driven in Playwright Firefox 155 against the built site, beside the same page
   in Chromium, and compared as pixels rather than as an impression:

     rail          y 70..77 in both      8px, which is --sp-03
     fill          x 33..369 in both     the progress ends where the thumb does
     thumb         same paddle, same white border, same position at 0 and at 40
     disabled      grey progress, grey thumb, same as the WebKit block

   The two engines agree to the pixel, which is what "one control in both
   engines" has to mean to be worth saying. `getComputedStyle` cannot confirm
   any of it: the UA shadow tree returns the HOST's style in Chromium and
   nothing at all in Firefox, so a scripted assertion on these pseudo-elements
   reads as a pass whether the rule applied or not. The render is the evidence.

   THAT NOTE IS NOW A TEST. It read: "Firefox is not in the repo's browser set,
   so nothing re-checks this on the next change. It was verified on 2026-09-22
   and will silently rot the first time the geometry moves." A verification
   nothing re-runs is a screenshot somebody took once. tests/slider-gecko.js
   renders this control in both engines on every gate run and compares the four
   numbers above between them, recovering each one from the pixels rather than
   from a style -- for the reason in the paragraph above, a scripted assertion
   on these pseudo-elements cannot fail. If the geometry moves in one engine
   and not the other, the gate says so. */
.slider__input::-moz-range-track {
  height: var(--sp-03);
  background: var(--border-strong);
  border-radius: var(--r-sm);
  border: 0;
}
.slider__input::-moz-range-progress {
  height: var(--sp-03);
  background: var(--accent);
  border-radius: var(--r-sm);
}
.slider__input::-moz-range-thumb {
  width: var(--sp-05);
  height: var(--sp-03);
  background: var(--background);
  border: var(--border-w) solid var(--text-primary);
  border-radius: var(--r-sm);
}
.slider__input:disabled::-moz-range-progress { background: var(--border-strong); }
.slider__input:disabled::-moz-range-thumb {
  background: var(--layer-03);
  border-color: var(--text-disabled);
}


/* --- search ----------------------------------------------------------------
   A search field is a .field with an icon gutter, a clear control and a loading
   state. It is NOT a second field component: the markup is
   `<div class="field search">`, so the label, the hint, the fill, the bottom
   border and the focus ring are the system's existing field, unchanged.

   type="search" is deliberate: it gets Escape-to-clear on some platforms, the
   right virtual keyboard with a Search key, and the right announcement. And a
   placeholder is NOT a label -- it disappears on the first keystroke, it fails
   1.3.1 and 3.3.2, and it is the single most common accessibility defect in a
   search field. Every search here carries a real <label>. */
.search .search__box { position: relative; display: block; }
.search__icon {
  position: absolute;
  inset-inline-start: var(--sp-05);
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: var(--sp-05);
  block-size: var(--sp-05);
  color: var(--text-helper);
  /* The icon is decoration over a control; a click on it must reach the input. */
  pointer-events: none;
}
/* SPECIFICITY, COUNTED. `.field input:not([type="checkbox"]):not([type="radio"])`
   is (0,3,1) and sets the padding SHORTHAND, so a (0,1,0) `.search__input`
   would be typed and never render -- the exact defect that let an errored input
   ship looking valid. This selector is .search + .search__box + .search__input
   + input = (0,3,1) plus .field on the same element = (0,4,1), which wins on
   the class count rather than on source order. Do not "simplify" it. */
.field.search .search__box input.search__input {
  padding-inline-start: calc(var(--sp-05) + var(--sp-05) + var(--sp-04));  /* icon + gutter */
  padding-inline-end: calc(var(--sp-08) + var(--sp-03));                   /* clear control */
}
/* Chromium paints its own cancel glyph inside a non-empty search input. Ours is
   a real button with a real accessible name and a real focus ring, and two
   clear controls in one field is one too many. */
.search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; display: none; }
.search__clear {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sp-08);    /* 40px, the system's own .btn--sm target */
  block-size: var(--sp-08);
  padding: 0;
  color: var(--text-helper);
  background: transparent;
  border: var(--border-w) solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.search__clear:hover { color: var(--text-primary); background: var(--layer-03); }
/* A button, so the ring is keyboard-only -- the system's stated convention. */
.search__clear:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset-inset);
}
.search__clear[hidden] { display: none; }
.search__clear:disabled { color: var(--text-disabled); cursor: not-allowed; }
/* LOADING. An indeterminate bar on the field's own bottom edge, in the accent,
   over the bottom border it replaces. A rotating glyph was the alternative and
   is worse: it needs a shape that carries no meaning, and at 16px it reads as
   an artefact. The duration is derived from the motion token rather than typed
   -- --dur is 180ms and a loop at 180ms is a strobe -- and the tokens file has
   no --dur-slow to name this, which is raised in the report. */
/* 0 to 300% of a 25%-wide bar: it starts flush with the field's left edge and
   finishes flush with its right one, so it can never leave the box. It WAS
   -100% to 400%, which put a quarter of the bar outside the field at each end
   -- visible in a screenshot, invisible to every check, because a check that
   walks elements cannot see a pseudo-element. */
@keyframes senua-search-loading {
  from { transform: translateX(0); }
  to   { transform: translateX(300%); }
}
.search--loading .search__box::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  block-size: calc(var(--border-w) * 2);
  inline-size: 25%;
  background: var(--accent);
  animation: senua-search-loading calc(var(--dur) * 8) var(--ease) infinite;
}
/* The bar is motion, and motion is not a status. The word is, and it is in a
   live region so it is announced rather than only seen. */
.search__status {
  margin: var(--sp-03) 0 0;
  font-size: var(--ts-01);
  color: var(--text-helper);
}


/* --- date and time ---------------------------------------------------------
   NATIVE type="date", type="time" and type="datetime-local", inside .field,
   with no picker of our own. That is a deliberate and load-bearing decision:

     - a hand-built date picker is a grid widget with roving tabindex, arrow,
       Home/End, PageUp/PageDown and Escape handling, a live-announced month,
       and a text alternative for every cell. It is the single largest
       accessibility liability a design system can take on, and every one this
       team has seen in the wild fails at least three of those.
     - the native control is already localised, already keyboard-operable
       segment by segment, and already works with the platform's own date
       assistance.

   WHAT NATIVE COSTS, stated honestly rather than hidden:
     - the ORDER of the segments and the separator come from the user's locale,
       not from our markup. A field in an Australian browser reads dd/mm/yyyy
       and the same page in a US browser reads mm/dd/yyyy. Never place a format
       hint in the label that contradicts what the browser will draw.
     - the picker PANEL is the browser's. It cannot be themed, and it is not the
       same panel in Chromium, Firefox and Safari.
     - Safari's support is the weakest of the three; it still renders a usable
       text-and-stepper field, but not the same calendar.
     - min, max and step are enforced by the browser, not by anything here.
   If a product needs a range, ship two fields in a .form-row and label them
   "From" and "To" -- not one field parsing a string.

   The fill, the bottom border, the focus ring and the disabled state all come
   from the existing .field rule, which already matches these types: the :not()
   chain there excludes only checkbox and radio. Nothing is restyled here.
   Only the indicator, which the field rule does not reach, is. */
.field input[type="date"]::-webkit-calendar-picker-indicator,
.field input[type="time"]::-webkit-calendar-picker-indicator,
.field input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
}
/* color-scheme on the root is what makes the native glyph and the native panel
   follow the theme; without it a dark field draws a black indicator on a dark
   fill. It is set in senua-base.css, so this is a dependency note, not a rule. */


/* --- file ------------------------------------------------------------------
   A real <input type="file">, made invisible and stretched over the drop zone
   rather than clipped away in a corner. That one decision buys three things
   with no script at all: the whole zone opens the picker on a click, the whole
   zone accepts a DROPPED file because the input itself is the drop target, and
   the control keeps its native keyboard behaviour (Tab to it, Space or Enter
   opens the picker).

   What still needs the consumer's script, and there is no CSS substitute:
     - the DRAG-OVER state. A drag does not fire :hover, so the consumer adds
       .file__drop--dragover on dragenter and removes it on dragleave/drop.
     - the selected-file LIST. A browser will not render one, and the file names
       are not available to CSS.
     - enforcing accept and size. The attributes filter the picker; they do not
       validate a drop. Check both again on the server.

   The trigger is the system's own .btn, not a second button skin. Its hover is
   forwarded from the zone because the transparent input sits over it. */
.file { margin-bottom: var(--sp-06); }
.file__drop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-04);
  padding: var(--sp-07);
  text-align: center;
  background: var(--layer-01);
  /* Dashed is the drop-zone affordance and it is the one place in this system
     that a dashed border means "put something here" rather than "disabled".
     They never appear together: a disabled zone loses the dash below. */
  border: var(--border-w) dashed var(--border-interactive);
  border-radius: var(--r-md);
  transition: border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.file__input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: pointer;
}
/* The input is transparent, so what a keyboard user must see is the ZONE's
   ring. The input keeps its own declaration as well: if a consumer ever stops
   overlaying it, the control is still never ringless. */
.file__input:focus { outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset-inset); }
.file__drop:has(.file__input:focus-visible) {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
}
.file__drop:hover { border-color: var(--text-primary); }
.file__drop:hover .file__trigger { background: var(--layer-03); }
/* DRAG-OVER. Two signals, because colour alone is not one: the border goes
   SOLID as well as accent, and the ground takes the accent tint. */
.file__drop--dragover {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-tint);
}
.file__drop--disabled,
.file__drop:has(.file__input:disabled) {
  border-style: solid;
  border-color: var(--border-strong);
  background: var(--layer-01);
  cursor: not-allowed;
}
.file__drop:has(.file__input:disabled) .file__input { cursor: not-allowed; }
/* The trigger is a span carrying .btn, so it cannot take the disabled
   attribute and .btn[disabled] never matches it. Without this the zone read
   disabled and the button inside it read live -- the one thing a disabled
   control must not do. Same --opacity-disabled the button itself uses. */
.file__drop:has(.file__input:disabled) .file__trigger,
.file__drop--disabled .file__trigger { opacity: var(--opacity-disabled); }
.file__note { margin: 0; font-size: var(--ts-02); color: var(--text-secondary); }
/* The constraints are not decoration: they are what stops a reader discovering
   the limit by having a 40 MB upload rejected. Reference them from the input
   with aria-describedby so they are announced, not only seen. */
.file__constraint { margin: 0; font-size: var(--ts-01); color: var(--text-helper); }

.file__list {
  margin: var(--sp-05) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--sp-03);
}
.file__item {
  display: flex;
  align-items: center;
  gap: var(--sp-04);
  padding: var(--sp-03) var(--sp-04);
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-subtle);
  font-size: var(--ts-02);
  /* A surface with text on it states its foreground. See tests/surface.js. */
  color: var(--text-secondary);
}
.file__name {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-primary);
  /* A file name has no spaces to break at, so it needs the stronger rule -- and
     it is exactly what `anywhere` is for. It is NEVER put on a table cell,
     where it collapses min-content to one character and shatters ordinary
     words; that defect is fixed and does not come back through this file. */
  overflow-wrap: anywhere;
}
.file__size {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
}
.file__remove {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sp-07);
  block-size: var(--sp-07);
  padding: 0;
  color: var(--text-helper);
  background: transparent;
  border: var(--border-w) solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.file__remove:hover { color: var(--support-error); background: var(--layer-03); }
.file__remove:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset-inset);
}
/* A remove control whose only content is a glyph MUST carry the file's name in
   its accessible name -- "Remove" nine times in a row names nothing. */


/* --- tag -------------------------------------------------------------------
   A TAG IS NOT A BADGE, and the system ships both on purpose. The difference is
   who owns the value:

     .badge   OUTPUT. A status the system computed -- ACTIVE, BLOCKED, DEGRADED.
              Mono, uppercase, one word, never interactive. The reader cannot
              change it and there is nothing to remove.
     .tag     INPUT. A token the USER put there -- a filter they applied, a
              recipient they added, a label they attached. Sentence case in the
              sans, because it carries arbitrary content and not an enumerated
              state, and it can carry a remove control.

   Collapsing them was considered and rejected: a badge that can be removed
   invites a reader to try to remove a status, and a tag set in uppercase mono
   mangles the user's own words. Two jobs, two components. The test for which
   one to reach for is a single question -- can the reader delete it? */
.tag-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-03);
  /* A stacked component owns the space under it, like .field and .switch do.
     It was margin:0, which is correct inside a toolbar and wrong everywhere
     else: in the light-theme block the tag row and the stepper under it
     touched, because nothing between them owned the gap. A grid or flex parent
     takes it back below. */
  margin: 0 0 var(--sp-05);
  padding: 0;
  list-style: none;
}
.tag {
  /* A single-line label inside a border. 1.5 leading is space the text cannot
     use and the border still has to contain, which took the clearance to 2px.
     `line-height: 1` was already written here and had no effect, because
     `line-height: var(--lh-compact)` was declared again SEVEN LINES BELOW it in
     this same block and won on order. That is the whole reason the clearance
     never moved: the fix was present, readable, and losing to its own rule. One
     declaration per property per block, and the leading is set here beside the
     reason for it.

     Three things carry the clearance and all three are needed. The leading is
     1, so the line box is the glyph box and nothing invisible has to be
     contained. The block padding is a step up from --sp-02 to --sp-03, so the
     space between the text and its border is a spacing step rather than a
     rounding remainder. And --sp-07 is the floor, so a tag is 32px whatever it
     holds and a row of them does not jitter by a pixel per label. */
  display: inline-flex;
  align-items: center;
  gap: var(--sp-02);
  max-inline-size: 100%;
  padding: var(--sp-03) var(--sp-04);
  min-block-size: var(--sp-07);
  font-family: var(--font-sans);
  font-size: var(--ts-02);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--tr-body);
  color: var(--text-primary);
  background: var(--layer-02);
  /* The boundary of a token IS the token, so it is --border-interactive at
     3:1 and not the decorative --border-strong at 1.25:1. Same reasoning as
     .badge and .doc-flag. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
}
/* A removable tag keeps its own left padding and gives the right side to the
   control, so the label and the control do not crowd. */
.tag--removable { padding-inline-end: var(--sp-01); }
/* The small tag takes the step below on both axes: --sp-02 of block padding
   against --sp-06 as the floor, which is 24px and 5px of clearance. It was
   --sp-01, which is 2px, and 2px was the measured clearance -- the number the
   badge was condemned for. Small is a step down the scale, not a squeeze. */
.tag--sm {
  padding: var(--sp-02) var(--sp-03);
  min-block-size: var(--sp-06);
  font-size: var(--ts-01);
}
.tag--sm.tag--removable { padding-inline-end: var(--sp-01); }
.tag--accent { color: var(--accent-text); border-color: var(--accent-text); background: var(--accent-tint); }
.tag--disabled { color: var(--text-disabled); border-color: var(--border-strong); }
.tag__label { min-width: 0; overflow-wrap: anywhere; }
.tag__remove {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sp-06);
  block-size: var(--sp-06);
  padding: 0;
  color: var(--text-helper);
  background: transparent;
  border: var(--border-w) solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.tag--sm .tag__remove { inline-size: var(--sp-05); block-size: var(--sp-05); }
.tag__remove:hover { color: var(--support-error); background: var(--layer-03); }
.tag__remove:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset-inset);
}
.tag__remove:disabled { color: var(--text-disabled); cursor: not-allowed; }
/* A 24px control is below the 44px comfortable target, which is the price of a
   token that sits in a line of text. The tag's own label must therefore never
   be the only way to remove it -- a list of removable tags needs a "clear all"
   at a full target size beside it, which is why .tag-group holds one. */


/* --- stepper ---------------------------------------------------------------
   Where the reader is in a sequence that has an order. Four states, and the
   state is carried by a WORD in every one of them -- "Complete", "Current",
   "Not started", "Unavailable" -- because a marker that differs only in colour
   tells a colour-blind reader nothing, and aria-current tells a sighted reader
   nothing. Both are present, in the markup, on every step.

   It is an <ol>: the order is the content, not the styling.

   Horizontal by default, and it STACKS below the medium breakpoint rather than
   scrolling -- four steps with real labels cannot fit 320px, and a stepper that
   has to be scrolled sideways to find out where you are is not answering the
   question it exists to answer. .stepper--vertical is vertical at every width,
   for a sidebar. */
.stepper {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0 0 var(--sp-06);
  padding: 0;
  list-style: none;
  counter-reset: none;
}
.stepper__step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--sp-04);
  align-items: start;
  /* A step is a flex item and is therefore stretched to the tallest step in the
     row; its own auto rows then share that free space and the body drifts DOWN.
     Measured: the one step with no detail line sat 12px lower than its four
     neighbours, so five titles rendered at two different baselines. Pin the
     rows to the top and the step grows at the bottom instead. */
  align-content: start;
}
/* A step title is often a link back to a completed step. It carries no class,
   so the prose rule marked it -- and a stepper rendered two of its five steps
   as underlined lavender text in the middle of an interface. The component
   owns its own links, the way the masthead and the footer do. */
.stepper .stepper__step .stepper__title a { color: inherit; text-decoration: none; }
.stepper .stepper__step .stepper__title a:hover,
.stepper .stepper__step .stepper__title a:focus-visible {
  color: var(--text-primary); text-decoration: underline;
}
/* Two class selectors, not one. A step is an <li>, so the prose-list rule
   matches at (0,2,1) and `.stepper__title a` at (0,1,1) loses to it -- which
   is the fourth time in this system that a component class has lost to a
   context rule, and the reason the link page tells a consumer to use a class
   rather than trust where a link sits.

   THREE class selectors, and the count is not arbitrary. :not() carries the
   specificity of its argument, so the prose rule is not the (0,2,1) it looks
   like -- `:not(nav li)` adds two elements and `:not(.list--bare li)` adds a
   class and an element, making it (0,2,4). Two classes lost to it on element
   count. This is exactly why a component should not be fighting a context rule
   at all: the number you have to beat is not the one you can see. */

.stepper__marker {
  /* MEASURED BEFORE CHANGED, because this one is not the tag's defect wearing
     a different class and the same fix would have been wrong.

     The marker is a FIXED 32px square holding a 12px digit, so its real
     clearance is (32 - 2 border - 12 line) / 2 = 9px. It was never cramped.
     What it was was UNMEASURABLE: with `block-size` fixed and no padding, the
     content box is 30px against a 12px line box, so anything counting lines as
     round(content / leading) reads 2.5 as two or three lines of text and
     reports 2px, or -3px on the current step where the border doubles. A
     32px box was being condemned for text it does not contain.

     So the size is not the answer and neither is more space. The answer is to
     say what the box is actually made of: --sp-03 of padding as the declared
     minimum clearance, and --sp-07 as a FLOOR rather than a fixed height. The
     rendered square is identical at 32px -- and a two-digit step number now
     grows the marker instead of colliding with its own border, which the fixed
     inline-size would have done at step 10 of a ten-step flow. */
  grid-column: 1;
  grid-row: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-inline-size: var(--sp-07);
  min-block-size: var(--sp-07);
  padding: var(--sp-03);
  line-height: 1;
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text-helper);
  background: transparent;
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
}
/* The tick REPLACES the digit, so it is the digit's size. It was --sp-05, which
   is 16px, and 16px no longer fits: the marker now declares --sp-03 of padding,
   leaving a 14px content box, so a 16px tick pushed the complete steps to 34px
   while their neighbours stayed at 32 -- five markers at two different heights,
   which is the same defect as the titles at two baselines that align-content
   fixed above. --sp-04 is 12px, the marker's own font-size, so the tick and the
   number occupy one optical size and every marker in the row is 32px. */
.stepper__marker svg { inline-size: var(--sp-04); block-size: var(--sp-04); }
.stepper__body {
  grid-column: 2;
  grid-row: 1 / span 2;
  min-width: 0;
  padding-block-end: var(--sp-06);
}
.stepper__title {
  display: block;
  font-size: var(--ts-02);
  font-weight: 600;
  line-height: var(--lh-compact);
  color: var(--text-primary);
}
.stepper__state {
  display: block;
  margin-block-start: var(--sp-01);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--text-helper);
}
.stepper__detail {
  display: block;
  margin-block-start: var(--sp-02);
  font-size: var(--ts-02);
  color: var(--text-secondary);
}
/* The connector, drawn in the marker column under the marker. It is a hairline
   in the BLOCK direction here and in the inline direction in the horizontal
   form below -- one element, two orientations, no extra markup. */
.stepper__step::after {
  content: "";
  grid-column: 1;
  grid-row: 2;
  justify-self: center;
  inline-size: var(--border-w);
  block-size: 100%;
  min-block-size: var(--sp-05);
  margin-block: var(--sp-02);
  background: var(--border-strong);
}
.stepper__step:last-child::after { content: none; }
.stepper__step:last-child .stepper__body { padding-block-end: 0; }

.stepper__step--complete .stepper__marker {
  color: var(--text-on-accent);
  background: var(--accent);
  border-color: var(--accent);
}
.stepper__step--complete::after { background: var(--accent); }
.stepper__step--current .stepper__marker {
  color: var(--text-primary);
  border-color: var(--accent);
  border-width: calc(var(--border-w) * 2);
}
.stepper__step--current .stepper__state { color: var(--accent-text); }
.stepper__step--disabled .stepper__marker { color: var(--text-disabled); border-color: var(--border-strong); }
.stepper__step--disabled .stepper__title { color: var(--text-disabled); }
.stepper__step--disabled .stepper__state { color: var(--text-disabled); }
.stepper__step--disabled .stepper__detail { color: var(--text-disabled); }

/* Horizontal, from the medium breakpoint up. The step becomes a row, the
   connector becomes the rule that fills what the label does not use, and the
   last step's connector is already gone. */
@media (min-width: 672px) {
  .stepper:not(.stepper--vertical) { flex-direction: row; }
  .stepper:not(.stepper--vertical) .stepper__step {
    flex: 1 1 0;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-items: start;
  }
  .stepper:not(.stepper--vertical) .stepper__body {
    grid-column: 1 / -1;
    grid-row: 2;
    padding-block: var(--sp-04) 0;
    padding-inline-end: var(--sp-05);
  }
  .stepper:not(.stepper--vertical) .stepper__step::after {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    justify-self: stretch;
    inline-size: auto;
    block-size: var(--border-w);
    min-block-size: 0;
    margin-block: 0;
    margin-inline: var(--sp-04);
  }
}
/* A stepper is a map, not a menu. If a step is reachable the title is a link
   and the link is the target; a step nobody can jump to is plain text. Nothing
   here makes a whole step clickable, because a 300px click target whose edges
   nobody can see is a target people miss. */
.stepper__title a { color: inherit; text-decoration: underline; text-decoration-color: var(--border-interactive); text-underline-offset: calc(var(--border-w) * 3); }
.stepper__title a:hover { text-decoration-color: var(--accent); }


/* --- data table: the modifiers that make .table a data grid ----------------
   All of it is built ON .table, never beside it. The base component keeps its
   hairlines-only treatment; everything below is opt-in, so a marketing
   comparison table is unchanged.

   Every one of these selectors is written at (0,2,1) or better -- `.table.table
   --dense th` rather than `.table--dense th` -- so it does not depend on this
   layer being concatenated after senua-components.css. A tie broken by file
   order is a defect waiting for someone to reorder the build. */

/* CAPTION. A data table needs a name, and the name belongs in a <caption>: it
   is announced when the table is entered and it is the only label that travels
   with the table when a screen reader lists the page's tables. Use
   .visually-hidden on it when the heading above already says it. */
.table > caption {
  caption-side: top;
  text-align: start;
  padding-block-end: var(--sp-04);
  font-size: var(--ts-02);
  font-weight: 600;
  color: var(--text-primary);
}
.table > caption .table__caption-note { display: block; margin-block-start: var(--sp-01); font-weight: 400; font-size: var(--ts-01); color: var(--text-helper); }

/* DENSITY. The default is comfortable: 12px/16px. Dense is 8px/12px and is for
   an operator staring at 200 rows, not for squeezing a marketing table. */
.table.table--dense th,
.table.table--dense td { padding: var(--sp-03) var(--sp-04); }
.table.table--dense { font-size: var(--ts-01); }

/* SORTABLE HEADER. The affordance is a real <button> inside the <th>, because
   a th with a click handler is not reachable by keyboard and a th with
   tabindex is a focusable thing with no role. The button gets Enter and Space
   for free. aria-sort goes on the TH -- it is a property of the column, not of
   the control -- and it is what assistive technology announces. The arrow is
   driven from that same attribute, so the picture and the announcement can
   never disagree. */
.table th .table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-02);
  margin: 0;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: inherit;
  letter-spacing: inherit;
  background: transparent;
  border: var(--border-w) solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.table th .table__sort:hover { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: calc(var(--border-w) * 3); }
.table th .table__sort:focus-visible {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
}
.table__sort-icon { inline-size: var(--sp-05); block-size: var(--sp-05); flex: 0 0 auto; color: var(--text-helper); }
/* UNSORTED shows BOTH triangles in the helper role, which is what says the
   column can be sorted at all; SORTED shows the one direction it is in, in the
   accent's text role. Never the reverse -- a column that is not sorted must not
   claim a direction, and a column that is must not offer both. */
.table th[aria-sort="ascending"] .table__sort-desc,
.table th[aria-sort="descending"] .table__sort-asc { display: none; }
.table th[aria-sort="ascending"] .table__sort-icon,
.table th[aria-sort="descending"] .table__sort-icon { color: var(--accent-text); }
.table th[aria-sort="ascending"],
.table th[aria-sort="descending"] { color: var(--text-primary); }

/* SELECTION. The checkbox column is fixed width and the header checkbox is the
   select-all. `indeterminate` is a DOM property and cannot be written in
   markup: some-but-not-all selected has to be set from script, and a header
   checkbox that only ever shows checked or unchecked is lying about the state
   of the page. */
.table th.table__select,
.table td.table__select {
  inline-size: var(--sp-09);
  padding-inline-end: var(--sp-03);
  vertical-align: middle;
}
.table .table__select input[type="checkbox"] {
  inline-size: var(--sp-05);
  block-size: var(--sp-05);
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.table .table__select input[type="checkbox"]:focus {
  outline: var(--focus-w) solid var(--border-focus);
  outline-offset: var(--focus-offset);
}
/* A SELECTED ROW, AND NO LEFT-EDGE BAR. The row takes a surface step; the
   checkbox is the signal that survives without colour. */
.table tbody tr[aria-selected="true"] { background: var(--layer-02);  /* A surface with text on it states its foreground. See tests/surface.js. */  color: var(--text-primary); }
.table tbody tr[aria-selected="true"] td { color: var(--text-primary); }

/* STICKY HEADER. It only sticks inside a container that scrolls in the block
   direction, which .table-wrap does not do by itself -- hence the paired
   modifier below. The header also needs an opaque fill, or the rows scroll
   underneath and show through, and border-collapse drops a sticky cell's
   border, so the rule is drawn as an inset shadow standing in for it. */
.table.table--sticky thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-raised);
  background: var(--layer-02);
  box-shadow: inset 0 calc(var(--border-w) * -1) 0 var(--border-subtle);
  /* A surface with text on it states its foreground. See tests/surface.js. */
  color: var(--text-primary);
}
/* The height is two --sp-13 steps, so it is derived from the scale rather than
   picked; a consumer that wants a different height sets max-block-size itself.
   This is the ONLY place the system asks a table to scroll vertically -- the
   horizontal scroll in .table-wrap is the prescribed treatment for a wide
   table and is unchanged. */
.table-wrap--scroll { overflow-y: auto; max-block-size: calc(var(--sp-13) * 2); }

/* TOOLBAR. Search, count and actions above the table. It is not inside the
   .table-wrap: a toolbar that scrolls sideways with the table is a toolbar
   nobody can reach at 320px. */
.table-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-05);
  margin-block-end: var(--sp-05);
}
.table-toolbar__search { flex: 1 1 calc(var(--sp-13) + var(--sp-10)); min-width: 0; margin-bottom: 0; }
/* The search inside a toolbar is a .field, and .field carries a bottom margin
   for stacked forms. In a toolbar the gap is the flex container's job. */
.table-toolbar .field,
.table-toolbar .tag-group { margin-bottom: 0; }
.table-toolbar__count {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  color: var(--text-helper);
  font-variant-numeric: tabular-nums;
}
.table-toolbar__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-03); }

/* EMPTY STATE. A table with no rows must say why it has none and what to do
   about it. A blank frame reads as a page that failed to load, and the two are
   indistinguishable to the reader unless one of them says so. */
.table .table__empty { padding-block: var(--sp-09); text-align: center; }
.table__empty-title { display: block; font-size: var(--ts-04); font-weight: 600; color: var(--text-primary); }
.table__empty-body { display: block; margin-block-start: var(--sp-03); font-size: var(--ts-02); color: var(--text-helper); }
.table__empty-action { display: block; margin-block-start: var(--sp-05); }


/* --- kbd -------------------------------------------------------------------
   A keyboard key. Any page that documents a shortcut needs one, and without it
   the key is either a <code> (which reads as a value) or bare prose (which
   reads as nothing).

   senua-base.css already gives `kbd` the mono family at the inherited code
   step; this adds the KEY -- the box that makes it read as something you press.
   It is 12px in a 14px context, which is deliberate: a key inside a sentence
   must not outweigh the sentence.

   Write the key as the platform writes it, in ASCII words: Ctrl, Shift, Enter,
   Esc, Tab, Space, Up, Down, Home, End, PageUp, PageDown. No arrow glyphs and
   no command symbol: a glyph is unreadable to a screen reader, and "->" or the
   word is what the reader can search for. */
.kbd {
  /* Same duplicate-declaration defect as .tag, and the same consequence:
     `line-height: 1` was written at the top of this block and overridden by
     `line-height: var(--lh-compact)` below it, so the key cap stayed 27px with
     3.93px of clearance. One declaration, set here.

     The size is NOT floored to --sp-06 the way the tag is, and that is the one
     deliberate difference. A key cap sits INSIDE a sentence, and prose here is
     16px on --lh-body, which is a 24px line box. A 24px cap fills that line box
     exactly, so caps on consecutive lines touch; at the 27px it used to be they
     overlapped, which is visible in the two-line shortcut paragraph on the
     keyboard page. Leading 1 plus --sp-02 of block padding gives 22px -- one
     spacing step of clearance, and 2px of air between wrapped lines. The cap
     must not drive the leading of the sentence it sits in. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--sp-06);
  padding: var(--sp-02) var(--sp-03);
  font-family: var(--font-mono);
  font-size: var(--ts-01);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
  color: var(--text-primary);
  background: var(--layer-02);
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
}
.kbd--disabled { color: var(--text-disabled); border-color: var(--border-strong); }
/* A combination. The separator is a real character between two keys, so it is
   read aloud as "plus" and copies as text. */
.kbd-combo { display: inline-flex; align-items: center; gap: var(--sp-02); white-space: nowrap; }
.kbd-combo__sep { color: var(--text-helper); font-size: var(--ts-01); }


/* --- form layout -----------------------------------------------------------
   The rules for a form, which the system had demonstrated and never owned: the
   pages showed fields stacked in a narrow frame and nothing said what happens
   at 1312px, where a 100%-width input is 1248px of text box for a six-character
   postcode.

   Stacked is the default and it is the right default. Two columns are for
   fields a reader fills in pairs and reads across -- given name and family
   name, from and to. NEVER put two unrelated fields side by side: the eye has
   to choose an order, and half the readers choose the wrong one. */
.form-layout { display: grid; gap: var(--sp-06); }
.form-layout > * { min-width: 0; }
/* .field ships a bottom margin for a form with no layout class. Inside a grid
   the gap is the container's job, and the two together double it. */
.form-layout .field,
.form-layout .switch,
.form-layout .slider,
.form-layout .file,
.form-layout .tag-group,
.form-layout .stepper,
.form-layout fieldset { margin-bottom: 0; }
/* Two columns from the medium breakpoint -- the same 672px the rest of the
   system steps at, not a width invented here. Below it there is no room for
   two fields and a label, so it stacks. */
@media (min-width: 672px) {
  .form-layout--two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* A field that must keep the full width in a two-column form: an address line,
   a textarea, a file drop zone. */
.form-layout__full { grid-column: 1 / -1; }

/* A ROW of fields that wraps. Unlike the two-column grid this is for fields of
   UNEQUAL weight -- a long street and a short postcode -- so each carries its
   own basis and the row reflows when they no longer fit. The basis is a token
   pair (160 + 64 = 224px) rather than a picked number. */
.form-row { display: flex; flex-wrap: wrap; gap: var(--sp-05); }
.form-row > * { flex: 1 1 calc(var(--sp-13) + var(--sp-10)); min-width: 0; margin-bottom: 0; }
.form-row > .form-row__narrow { flex: 0 1 calc(var(--sp-13) - var(--sp-07)); }

/* A GROUP. A fieldset with a visible legend, for a set of fields that belong
   together -- and the only correct container for a set of radios, because the
   legend is what names the group when a screen reader reaches the third one.
   senua-components.css resets `fieldset` to no border at all, which is right as
   a default; a group that wants a frame opts into it here. */
.form-group {
  margin: 0;
  padding: var(--sp-06);
  border: var(--border-w) solid var(--border-subtle);
  border-radius: var(--r-md);
}
.form-group > legend {
  /* A legend is painted INTO the border by the browser, which cuts a notch in
     the frame's top edge. Floating it takes it out of that flow and lets the
     frame close, and the legend stays a legend in the accessibility tree. */
  float: left;
  inline-size: 100%;
  margin: 0 0 var(--sp-05);
  padding: 0;
  font-size: var(--ts-02);
  font-weight: 600;
  color: var(--text-primary);
}
.form-group > legend + * { clear: both; }
.form-group__hint { margin: calc(-1 * var(--sp-04)) 0 var(--sp-05); font-size: var(--ts-01); color: var(--text-helper); }
.form-group > :last-child { margin-bottom: 0; }

/* THE ACTIONS ROW. It uses the system's existing .btn-group for the buttons --
   there is no second row component here, only the separation above it. The
   primary action goes LAST in a row that ends at the right, which is the
   platform convention on the web and in Windows, and the reader's eye finishes
   there. */
.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-05);
  margin-block-start: var(--sp-07);
  padding-block-start: var(--sp-06);
  border-block-start: var(--border-w) solid var(--border-subtle);
}
.form-actions--end { justify-content: flex-end; }
/* A destructive action is separated from the rest of the row rather than
   coloured red and put beside Save. Distance is what prevents the mis-click;
   colour only labels it after the fact. */
.form-actions__spacer { flex: 1 1 auto; }
/* The note that belongs with the actions -- "changes apply immediately",
   "this cannot be undone". It obeys the measure, like all prose. */
.form-note { margin: var(--sp-04) 0 0; max-width: var(--measure); font-size: var(--ts-01); color: var(--text-helper); }

/* --- select ------------------------------------------------------------------
   A STANDARDISED select, because the native one cannot be made consistent.
   `appearance: none` styles the closed control and nothing else: the dropped
   list is drawn by the operating system, in the operating system's colours,
   at the operating system's size. On a dark page in macOS Chrome it opens as a
   light panel with a blue selection and a tick -- none of which is ours, and
   none of which changes with the theme.

   So the list is ours: a button, and a listbox that is real markup. The cost
   is that the ARIA and the keyboard have to be implemented rather than
   inherited, which is why this is one component and not a pattern each page
   reinvents. The native <select> stays in the system for a form field inside a
   form -- it is more robust on a phone, where the platform's own picker is
   better than anything a page can draw. This is for a CONTROL: a theme
   switcher, a filter, a sort order.

   Keyboard, implemented in js/select.js and verified with real presses:
   Enter/Space/Down opens, Up/Down move, Home/End jump, typing jumps to a
   matching option, Enter or Space chooses, Escape closes and returns focus,
   Tab closes and moves on. Type-ahead buffers for 700ms, so "vi" seeks Vitals
   and a letter that matches nothing keeps the option you were on -- the same
   contract the native control has always had.

   DO NOT NAME THIS SCRIPT IN A PAGE HEADER. The site shell loads
   /js/select.js on every page, because the theme control in the masthead is a
   .select. The select's own documentation page also carried `script: select`,
   so the file loaded TWICE; it registers its click and keydown handlers on the
   document from inside an IIFE, with nothing that makes a second registration
   a no-op. Every key and every click was then handled twice, and the symptoms
   were not the ones "it runs twice" would lead you to expect:

     Enter and Space   opened the list, and the button's own activation click
                       then closed it -- the control could not be opened with
                       the two keys its own documentation table lists.
     Down              moved the active option two at a time.
     Type-ahead        took every character twice, so "p" found Pulse and the
                       buffer then held "pp", which matches nothing.
     Enter to choose   dispatched senuaselect twice for one choice, which a
                       consumer would have seen as a double submit.
     Click             opened and immediately closed.

   None of that is visible in a screenshot, none of it fails a contrast, focus
   or specificity check, and the page rendered a correct-looking control
   throughout. It surfaced only under real key presses, which is the argument
   for driving this component rather than asserting about it. The remaining
   exposure is in js/select.js and is recorded in the report: the IIFE should
   refuse a second registration. */
.select { position: relative; display: inline-flex; min-inline-size: 0; }

.select__button {
  display: inline-flex; align-items: center; gap: var(--sp-04);
  inline-size: 100%;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding: var(--sp-03) var(--sp-05);
  background: var(--layer-02); color: var(--text-secondary);
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
  font-family: var(--font-sans); font-size: var(--ts-02);
  line-height: var(--lh-compact);
  text-align: start; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.select__button:hover { background: var(--layer-03); color: var(--text-primary);
  border-color: var(--border-strong); }
.select__button:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: var(--focus-offset);
}
.select__value { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; margin-inline-end: auto; }
/* Drawn from borders, not typed as a character: a glyph is read aloud by a
   screen reader and changes shape with the typeface. */
.select__chevron {
  inline-size: var(--sp-04); block-size: var(--sp-04); flex: none;
  border-inline-end: var(--border-w) solid currentColor;
  border-block-end: var(--border-w) solid currentColor;
  transform: translateY(-25%) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease);
}
.select__button[aria-expanded="true"] .select__chevron { transform: translateY(25%) rotate(225deg); }

.select__list {
  position: absolute; inset-block-start: calc(100% + var(--sp-02));
  inset-inline-start: 0;
  /* SIZED TO ITS CONTENTS, never narrower than the control that opened it.
     `inline-size: 100%` alone pinned the list to the button's width, so a
     long option was cramped or clipped by a panel sized for a short one. */
  inline-size: max-content;
  min-inline-size: 100%;
  max-inline-size: min(22rem, calc(100vw - var(--sp-07)));
  z-index: var(--z-dropdown);
  margin: 0; padding: var(--sp-02); list-style: none;
  /* SHOW THE WHOLE LIST. A fixed 18rem cap put a scrollbar on seven options
     with six hundred pixels of empty page beneath them -- a scrollbar that
     exists because of a number in a stylesheet rather than because anything
     ran out of room. The only real limit is the viewport, so that is the only
     one stated; a list long enough to reach it still scrolls. */
  max-block-size: min(70vh, calc(100vh - var(--sp-12)));
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
  background: var(--layer-02);
  /* --border-interactive, NOT --border-strong, and the tokens file decides it
     rather than taste: "subtle and strong are DECORATION -- a card edge, a
     table rule, a divider ... --border-interactive is the boundary of a
     CONTROL, which WCAG 1.4.11 requires to reach 3:1."

     This panel is a role="listbox" of operable options. It is a control, not a
     card. At --border-strong it measured 1.33:1 dark, 1.25:1 light, 1.35:1
     midnight, 1.36:1 violet, 1.28:1 paper -- and its fill is only 1.06:1
     against the page behind it, because --layer-02 over --layer-01 is meant to
     be a whisper. A 1.06:1 surface inside a 1.3:1 edge is not a panel; it is
     the page with a shadow on it, and --shadow-md was the only thing left
     saying where the listbox ended. Measured, not eyeballed: the raw token
     value looks like a 19:1 white, and it is only when you composite the 0.1
     alpha over the layer beneath that the real number appears.

     The button that opens this panel was already --border-interactive, so the
     control read and the list it dropped did not. It is also the exact defect
     the tokens file records as fixed elsewhere -- ".btn--ghost /
     .btn--secondary / fieldset.seg / .badge had been drawing their only
     visible edge with --border-strong at 1.25:1" -- surviving in one more
     place. Now 3.3:1 to 4.0:1 across the five themes, 21:1 in contrast.

     The fieldset at .form-group keeps --border-subtle on purpose: a group box
     IS a card edge, which is the decoration role, and .card and .evidence draw
     theirs the same way. Faint there is correct. */
  border: var(--border-w) solid var(--border-interactive);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.select__list::-webkit-scrollbar { inline-size: var(--sp-03); }
.select__list::-webkit-scrollbar-track { background: transparent; }
.select__list::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: var(--r-sm);
}
/* A panel that would open below the fold opens upward instead. Without this a
   control near the foot of a page drops its list off the screen entirely. */
.select--up .select__list {
  inset-block-start: auto; inset-block-end: calc(100% + var(--sp-02));
}
/* display:none, not visibility:hidden. A hidden-but-laid-out panel still
   widens the document and still cannot hold focus -- both measured on the
   dropdown in this system. */
.select__list[hidden] { display: none; }

.select__option {
  display: flex; align-items: center; gap: var(--sp-04);
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  padding: var(--sp-03) var(--sp-05);
  color: var(--text-secondary); cursor: pointer;
  border-radius: var(--r-sm);
}
.select__option:hover { background: var(--layer-03); color: var(--text-primary); }
/* The ACTIVE option is the one the keyboard is on; the SELECTED one is the
   current value. They are different states and a listbox needs both, because
   arrowing through a list without moving focus is the whole point of
   aria-activedescendant. */
.select__option.is-active { background: var(--layer-03); color: var(--text-primary); }
.select__option[aria-selected="true"] { color: var(--text-primary); font-weight: 600; }
/* The tick is drawn, and it is not the only thing marking the selection --
   the weight changes too, so the state does not rest on a shape alone. */
.select__option[aria-selected="true"]::after {
  content: ""; margin-inline-start: auto;
  inline-size: var(--sp-03); block-size: var(--sp-05);
  border-inline-end: calc(var(--border-w) * 2) solid var(--accent-text);
  border-block-end: calc(var(--border-w) * 2) solid var(--accent-text);
  transform: rotate(45deg) translateY(-15%);
}
.select--block { display: flex; inline-size: 100%; }

/* --- search results ----------------------------------------------------------
   A result is a LINK, a location and a reason it matched. The reason is the
   part most result lists drop, and it is the one that saves the reader opening
   three wrong pages.

   The whole result is one anchor. A title-only link means a reader has to hit a
   target the height of one line; the snippet beneath it is part of the same
   answer and should be part of the same target. */
.results { display: grid; gap: var(--sp-02); margin: 0; padding: 0; list-style: none; }
.result {
  display: grid; gap: var(--sp-03);
  padding: var(--sp-06);
  border-block-end: var(--border-w) solid var(--border-subtle);
  text-decoration: none; min-inline-size: 0;
  transition: background var(--dur-fast) var(--ease);
}
.result:hover { background: var(--layer-01); }
.result:focus-visible {
  outline: var(--focus-w) solid var(--border-focus); outline-offset: calc(-1 * var(--focus-w));
}
/* Where the page LIVES, above its name -- the same order a breadcrumb uses,
   because a reader checks the section before the title. */
.result__path {
  color: var(--text-helper); font-size: var(--ts-01);
  font-family: var(--font-mono); overflow-wrap: anywhere;
}
.result__title { margin: 0; color: var(--text-primary); font-size: var(--ts-04);
  font-weight: 600; letter-spacing: var(--tr-tight); }
.result__snippet { margin: 0; color: var(--text-secondary); font-size: var(--ts-02);
  line-height: var(--lh-body); max-inline-size: var(--measure); }
/* The matched words. A tint behind them rather than a colour on them: recolouring
   the words makes the snippet read as a set of links, and a reader who cannot
   distinguish the hue gets nothing at all. <mark> also carries the meaning to
   assistive technology, which a styled <span> does not. */
.result__snippet mark {
  background: var(--accent-tint); color: var(--text-primary);
  padding-inline: var(--sp-01); border-radius: var(--r-sm);
}
.result__meta { display: flex; flex-wrap: wrap; gap: var(--sp-02) var(--sp-05);
  color: var(--text-helper); font-size: var(--ts-01); }

/* The count, and the query it counted. "12 results" alone is unanchored: a
   reader who mistyped needs to see what was actually searched for. */
.results-summary {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-03) var(--sp-05);
  margin-block-end: var(--sp-06); color: var(--text-helper); font-size: var(--ts-02);
}
.results-summary__count { color: var(--text-primary); font-weight: 600; }

/* The search page itself: the field, then filters, then results. The filters
   sit beside the results on a wide screen and above them on a narrow one --
   never behind a control, because a filter a reader cannot see is a filter
   they will not know is applied. */
.search-page { display: grid; gap: var(--sp-08); }
@media (min-width: 1056px) {
  .search-page { grid-template-columns: 16rem minmax(0, 1fr); align-items: start; }
}
.search-page__filters { display: grid; gap: var(--sp-06); min-inline-size: 0; }
.search-page__results { min-inline-size: 0; }
.search-facet { display: grid; gap: var(--sp-03); }
.search-facet__title { margin: 0; color: var(--text-primary); font-size: var(--ts-02);
  font-weight: 600; }
/* A grid parent owns the space between its children, so the stacked components
   inside it give their own margin back -- the same handoff .form-layout and
   .table-toolbar already make. Without it a facet list stacks --sp-03 of grid
   gap on top of the checkbox's own --sp-04, and the filters read as three
   unrelated controls rather than one list of three. */
.search-facet .check,
.search-facet .field,
.search-facet .tag-group { margin-bottom: 0; }
/* =============================================================================
   SENUA AI DESIGN SYSTEM -- site furniture
   The parts every property has and no page is about: the footer, the
   page-to-page navigation at the foot of a document, the announcement bar.
   Every rule references a semantic role from senua-tokens.css, never a raw
   value. Concatenated into css/senua.css by scripts/build.sh.
   ============================================================================= */

/* --- footer -----------------------------------------------------------------
   A footer is where a reader goes when the page did not answer the question:
   the index, the legal position, the way to reach somebody. It is not a
   decorative band, so it is set at the body step and not at a caption size --
   the same defect the main navigation carried three times.

   THE COMMENT ABOVE SAID THAT WHILE THE VALUE SAID --ts-02, WHICH IS 14px.
   The rule this file states in its own second paragraph is the one the main
   navigation broke three times and the owner rejected twice, at 12px and then
   at 14px, and the footer was sitting at the second of those. A comment that
   describes a rule the declaration under it does not keep is worse than no
   comment: it is what a reader checks INSTEAD of measuring. Measured:
   .footer__link computed 14px, and on every page reached from the sidebar the
   only footer navigation there is computed 12px (see .footer__links below).
   The band is the body step now; the legal sentence is separately --ts-01,
   because fine print is genuinely fine print and a group heading is separately
   --ts-02, because a heading is a label and not a target -- the same division
   .side-nav__section makes in the navigation layer. */
.footer {
  margin-block-start: var(--sp-11);
  border-block-start: var(--border-w) solid var(--border-subtle);
  background: var(--layer-01);
  color: var(--text-secondary);
  font-size: var(--ts-03);
}
.footer__inner {
  width: 100%; max-width: var(--container); margin-inline: auto;
  padding: var(--sp-09) var(--sp-06) var(--sp-08);
  display: grid; gap: var(--sp-10);
}
@media (min-width: 672px)  { .footer__inner { padding-inline: var(--sp-06); } }
@media (min-width: 1056px) {
  .footer__inner { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    padding-inline: var(--sp-07); }
}

/* A GRID STRETCHES ITS CHILDREN, AND ONE OF THEM IS A BUTTON. The default
   `justify-items: stretch` took the call to action in this block to the full
   width of the column -- a 300px "Talk to sales" on every page of a consuming
   site, which reads as a form control rather than as an action. `start` is the
   right default for a column of a brand mark, a sentence and a button: each is
   as wide as it needs to be, and a consumer that wants a full-width button in
   here still has .btn--block to ask for one. */
.footer__brand {
  display: grid; gap: var(--sp-05);
  align-content: start; justify-items: start;
}
.footer__note { margin: 0; max-width: 34ch; color: var(--text-helper);
  line-height: var(--lh-body); }

/* auto-fit with a floor, not a fixed count: a footer with three columns and a
   footer with five are the same component. */
.footer__cols {
  display: grid; gap: var(--sp-08) var(--sp-07);
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
}
.footer__heading {
  margin: 0 0 var(--sp-04); color: var(--text-primary);
  font-size: var(--ts-02); font-weight: 600; letter-spacing: var(--tr-body);
}
.footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-02); }
/* A 44px target, from the scale rather than typed as a number. The links sit
   in a list, so the prose-link rule would otherwise underline them. */
.footer__link {
  display: inline-flex; align-items: center;
  min-height: calc(var(--sp-08) + var(--sp-02));
  color: var(--text-secondary); text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.footer__link:hover { color: var(--text-primary); }

/* THE BAND IS FULL WIDTH; THE SENTENCE IN IT IS NOT. The paragraph used to be
   the box -- container width, its own gutter -- so the legal line ran the whole
   1312px at 12px, about 180 characters to the line. The system's own first rule
   is prose at the measure, and fine print is prose; it is the one place where a
   long line is easiest to justify and hardest to read.

   Found by a CONSUMING SITE, which is worth recording: cyber.senuaai.com has a
   craft test asserting no paragraph exceeds 700px, this documentation had no
   such check, and the rule everyone agreed on was enforced in neither place.
   The band keeps the gutter and the rule; the sentence takes the measure. */
.footer__legal {
  border-block-start: var(--border-w) solid var(--border-subtle);
  /* One step less above and below than beside. Capping the sentence at the
     measure turned a one-line band into a two-line one, and this band is on
     every page in the system -- eighteen pixels of furniture, everywhere, took
     nine pages that were already within forty pixels of the two-viewport rule
     over it. The system's own note says its furniture had been measured at 36%
     of a page's whole height; the answer to a band getting taller is to take
     the height out of the band. */
  padding: var(--sp-05) var(--sp-06);
}
.footer__legal > p {
  width: 100%; max-width: var(--measure); margin: 0 auto;
  color: var(--text-helper); font-size: var(--ts-01);
}
.footer__legal > p + p { margin-block-start: var(--sp-03); }
@media (min-width: 1056px) { .footer__legal { padding-inline: var(--sp-07); } }

/* --- page navigation --------------------------------------------------------
   Previous and next at the foot of a document. It exists because a reference
   split across seventy URLs has an ORDER, and a reader who has finished one
   page should not have to return to the sidebar to find the next. */
/* THE RUN-OUT IS FURNITURE AND IT IS MEASURED AGAINST THE TWO-SCREEN RULE.
   This was --sp-11, 80px, on top of its own 32px of padding and a rule -- 112px
   of separation before the prev/next cards, with the footer's margin arriving
   underneath that. Putting the footer's navigation at the body step, which is
   the rule the band below now keeps, made the compact footer's links wrap onto
   a row of their own and cost 40px on every page; two pages went from inside
   the two-viewport limit to 11px and 9px over it, which is a real regression
   and not a gate to argue with. The height is paid for HERE and in the compact
   footer's own margin, because the argument was already written a few rules
   down: this system's furniture had been measured at 36% of a page's whole
   height. One step down leaves 96px before the cards and 48px before the band,
   both still on the scale and still generous; the navigation keeps its size. */
.page-nav {
  display: grid; gap: var(--sp-05);
  /* One step lower again, and the comment above already made this argument
     once: the height is paid for here and in the footer's own margin, because
     this system's furniture had been measured at 36% of a page's whole height.
     Measured again at 1056px after the legal line was capped at the measure,
     it is 548px of the 1800 a page is allowed. Six pages sat between 1808 and
     1824 with nothing wrong on any of them, which is the furniture's fault
     rather than theirs. 64px still leaves the cards clearly separated. */
  margin-block-start: var(--sp-10);
  padding-block-start: var(--sp-06);
  border-block-start: var(--border-w) solid var(--border-subtle);
}
@media (min-width: 672px) { .page-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.page-nav__link {
  display: grid; gap: var(--sp-02);
  padding: var(--sp-05) var(--sp-06);
  border: var(--border-w) solid var(--border-subtle);
  background: var(--layer-01);
  text-decoration: none; min-width: 0;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);

  /* A SURFACE WITHOUT A FOREGROUND. Unscoped this is invisible: the
     inherited colour is the same theme's. Inside a scoped theme it is the
     OTHER theme's text on this theme's surface -- measured at 1.30:1 on the
     notice, in a light block on a dark page. A rule that paints a surface
     states a foreground. */
  color: var(--text-secondary);
}
.page-nav__link:hover { border-color: var(--border-strong); background: var(--layer-02); }
/* The NEXT link is right-aligned, which is the only thing that tells a reader
   at a glance which way each one goes without reading the label. */
.page-nav__link--next { text-align: right; }
.page-nav__dir { color: var(--text-helper); font-size: var(--ts-01); }
.page-nav__title { color: var(--text-primary); font-size: var(--ts-03); font-weight: 600;
  letter-spacing: var(--tr-tight); }

/* --- announcement bar -------------------------------------------------------
   One line above the masthead. Dismissible, and it must not be the only place
   something important is said -- a reader who dismissed it yesterday will not
   see it today. */
.announce {
  background: var(--layer-02);
  border-block-end: var(--border-w) solid var(--border-subtle);
  color: var(--text-secondary); font-size: var(--ts-02);
}
.announce__inner {
  width: 100%; max-width: var(--container); margin-inline: auto;
  padding: var(--sp-04) var(--sp-06);
  display: flex; align-items: center; gap: var(--sp-05); flex-wrap: wrap;
}
.announce__text { margin: 0; min-width: 0; }
.announce__dismiss { margin-inline-start: auto; }
.announce.is-dismissed { display: none; }

/* --- compact footer ----------------------------------------------------------
   A four-column footer is an index, and a page that already has a sidebar does
   not need a second one. On an inner page it is 321px of furniture plus 160px
   of margin -- measured at 36% of the page's whole height, on a system whose
   own rule is that a page fits in two screens. So the full footer belongs on a
   landing page, and every page reached from the sidebar gets the legal line
   and the few links that are not in the sidebar at all. */
.footer--compact { margin-block-start: var(--sp-09); }
.footer--compact .footer__inner { display: none; }
.footer--compact .footer__legal { border-block-start: 0; }
.footer--compact .footer__legal > p {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-04) var(--sp-06);
  /* One step down from --sp-06, which pays back most of what the 44px targets
     below cost. Measured at 1440: the band was 103px and is 107px. The 4px is
     not the padding -- it is that the links, now at the body step, no longer
     fit beside the legal sentence and take a row of their own, which is the
     better composition anyway. The rest of the height is paid for in
     .page-nav's margin and in .footer--compact's. */
  padding-block: var(--sp-05);
}
/* THE ONLY NAVIGATION IN THE FOOTER OF EVERY INNER PAGE, and it was set at
   --ts-01. The compact footer hides .footer__inner entirely, so these five
   links are the whole of it -- and they sat at 12px, the smallest step in the
   system, inherited from the legal paragraph they share a row with. Two things
   were wrong at once and the screenshot showed both: navigation below the body
   step, which is the defect the main menu carried three times; and links
   rendered at the same size AND the same --text-helper colour as the legal
   sentence beside them, so nothing distinguished the things a reader can press
   from the sentence about Apache-2.0 they cannot. The size is what separates
   them now -- the row is one step of navigation beside one line of fine print,
   which is what it is. */
.footer__links { display: flex; flex-wrap: wrap; gap: var(--sp-04) var(--sp-06);
  margin-inline-start: auto; font-size: var(--ts-03); }
/* WHY THIS CARRIES .footer AND THE ONE ABOVE DOES NOT. These links are built
   without a class (build-docs.js emits a bare <a> inside .footer__links), so
   unlike every other link in this file they are still reachable by the prose
   rule in senua-base.css -- `:where(p, ...) a:not([class])`, which matches
   because the compact footer's legal line IS a <p>. At `.footer__links a` this
   rule was (0,1,1) and so is that one: a TIE, decided only by which file
   scripts/build.sh concatenates second. It renders correctly today for that
   reason and no other, and the failure mode if the layer order is ever changed
   is the exact defect this system has now had five times -- every footer link
   violet and underlined on a dark bar. Qualifying with .footer takes it to
   (0,2,1) and it stops depending on the order of a shell script. The real fix
   is a class on the anchor, in scripts/build-docs.js line 325, which is not
   this layer's to make. */
/* A 44px TARGET, the same one .footer__link takes in the full footer. These
   measured 24px tall, and at 412px they wrap to two rows 36px apart -- five
   phone-sized taps in a 24px band, in the one footer a phone ever sees. Two
   rules in the same file disagreed about the same kind of link, and the one
   that was visible on every inner page was the one that had not been given the
   target. The expression is the scale's, not a typed 2.75rem: --sp-08 + --sp-02. */
.footer .footer__links a {
  display: inline-flex; align-items: center;
  min-block-size: calc(var(--sp-08) + var(--sp-02));
  color: var(--text-helper); text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.footer .footer__links a:hover { color: var(--text-primary); }
