/* ==========================================================================
   nanocosmos Product Theme
   ==========================================================================

   A dark, technical UI theme extracted from player-diagnostics.html
   (nanoStudio Player Diagnostics), re-based 2026-09-03 on captions-workflow's
   in-production redesign (see the :root block below) — panels now carry a
   real shadow rather than being fully flat; the rest of the visual language
   (orange for primary/committing actions, blue for secondary actions and
   status, pill-shaped buttons and badges) is unchanged.

   VERSION
   --------------------------------------------------------------------------
   4.0.0 — 2026-10-20

   CHANGELOG
   --------------------------------------------------------------------------
   Reconstructed 2026-09-22 from the dated comments already in this file. The
   file carried no version of its own before that date, so every entry below
   3.2.0 is a reading of those inline comments after the fact, not a record
   kept at the time — treat the dates as reliable and the version numbers as
   assigned in hindsight. The inline dated comments stay exactly where they
   are: they carry the WHY, this list carries the WHAT and WHEN. Semver as the
   rest of this skill uses it: MAJOR = a change every consuming page sees,
   MINOR = additive components, PATCH = a fix to an existing rule. 3.1.0 and
   3.2.0 are two passes of the same day's work, kept separate because the
   second changed shipped components and the first did not.

   4.0.0 — 2026-10-20
     Changed: --panel-soft is #131d25, the exact midpoint of --bg #0d171e and
       --panel #19222b (direct request). It was #202a32, lighter than the
       panel. MAJOR because every consumer sees it: --panel-soft fills
       (hover states, disabled fills, dropdown options, the range thumb's
       drag fill) now sit DARKER than --panel, between it and --bg, instead
       of lifting above it.

   3.7.2 — 2026-10-07
     Fixed: .form-result's margin is symmetric, with first/last resets. It
       shipped in 3.7.0 as margin-top alone, which was invisible while every
       usage had it LAST in a panel under a caption. Moving it to the top of
       the gate's form made it obvious — measured 0.0px between the error box
       and the label beneath it.
       Measured ripple: the gate gains its 14px (0.0 → 14.0); the moodboard's
       gaps do not move, because its captions' own top margins collapse with
       the new bottom margin (14.0 → 14.0 twice, 11.5 → 14.0 once); and
       :first-child pulls the moodboard's first result flush to its panel
       padding (14.0 → 0.0), which is what .notice-item and .button-row
       already do at that position.
       :last-child is dormant — no usage has a result last. Kept anyway, and
       labelled as dormant in the rule, because unlike the .color-swatch rule
       this file deleted for doing nothing, this one can fire; it just has not
       yet.
       Zero consumers use .form-result, so nothing downstream changes.

   3.7.1 — 2026-10-07
     Changed: .modal-overlay's CONTRACT example is no longer UNVERIFIED. It is
       now taken from tests/archetype-stream.html, which carries a sign-in
       gate shipped hidden — the first reviewed page in this skill to use a
       modal at all, and a standing test of governing rule 2a: .modal-overlay
       sets display: flex and overrides the UA [hidden] rule, so if that
       gating ever breaks again the archetype shows a full-screen backdrop.
       Comment only. No declaration changed, nothing renders differently.

   3.7.0 — 2026-10-06 (the auth investigation: three primitives, two discoveries)
     Investigated three downstream sign-in implementations before writing any
     rule. The headline finding is not that each invented its own — it is that
     MOST OF WHAT THEY INVENTED ALREADY EXISTED HERE:
       .field--stacked is declaration-for-declaration identical to
         live-recorder's hand-rolled `.login-card .field` override, and its
         own CONTRACT already says "a narrow modal".
       .modal-panel__title / __sub carry the same 19px/700 and 13.5px values
         as both consumers' .login-card__title / __sub, and .modal-overlay's
         CONTRACT example is already a sign-in form.
     So the gap was three small primitives, none of them about authentication —
     plus one fix, which this release does NOT pretend is additive:
     Added: .form-result / --attn / --ok — the outcome of a form SUBMISSION,
       as opposed to one field failing (.field .error) or a persistent state
       (.status-strip) or something that happened elsewhere (.toast). All
       three consumers built this; none gave it a live region, so a screen
       reader gets silence on a failed sign-in. role="alert" is REQUIRED here.
     Added: .btn-link — a button that looks like a link, for a real action
       that reads as navigation. Two consumers hand-rolled it, each twice.
     Added: .btn--block — a width modifier on .btn, replacing the inline
       style="width:100%" both consumers reached for.
     Added: NON-RULE — there is no sign-up form here, with the decision
       table. Both product consumers link out to the dashboard; the real
       sign-up needs seven fields plus consent and cross-field validation.
     Changed: note 6 now states the single-orange rule outright. It forbade
       green/red/amber but never said that #F48220 is the ONLY orange, which
       is how two consumers kept the retired #E5883C alive in a hand-typed
       rgba() past the 2026-09-07 unification. See BATCH-FINDINGS item 23.
     Fixed: .field--stacked, and the automatic body:has(.workbench) .stats-row
       rule that shares its declarations, left the CONTROL at its intrinsic
       ~192px instead of filling the width the stacking frees — the exact
       problem the modifier exists to solve, present since 2026-09-22.
       align-items: stretch on both. Measured 192px → 362px in a 420px card
       and 192px → 278px in a stats-row panel.
       THIS RELEASE IS ADDITIVE PLUS THIS ONE FIX, not purely additive. Two
       pages render differently: tests/archetype-workbench.html (Stream name,
       Fit mode and Opacity now fill their panels) and the moodboard. Zero
       consumer pages change — no consumer uses .field--stacked at all, and
       the .workbench and .stats-row page sets are disjoint, re-verified
       2026-10-06 rather than taken from the 2026-09-24 note.
       It is folded in here rather than held for 3.7.1 because this release
       ships a moodboard section whose centrepiece is a sign-in card built
       from .field--stacked. Shipping a showcase that demonstrates a component
       failing at its one job is worse than giving up a clean release note.
     Verified: no retired value survives in this file. Both pre-unification
       accents were grepped as hex AND as RGB triples — the hand-typed rgba()
       case is exactly what a hex grep misses. Zero hits outside history
       comments. The only rgb triples present are white, black and
       rgba(13, 23, 30) on three overlay backdrops, which is --bg's own value
       (logged as item 25 — not a retired colour, but the same hazard class).

   3.6.0 — 2026-10-05
     Changed: .btn padding 10px/22px → 6px/15px and .btn-neutral 9px/21px →
       5px/14px, GLOBALLY. Every button on every consuming page renders 8px
       shorter and 14px narrower. This is the only entry in this list that
       changes rendering everywhere rather than inside one scope.
       The justification is DENSITY, not colour judgement: 10px/22px is a
       marketing-page button size in a tool UI, and that is as true on a log
       viewer as it is beside a preview. The colour-judgement version of this
       same value was correctly rejected twice — 3.5.0 scoped it, 3.5.1
       deleted it. It returns under a different argument that passes the
       review test instead of failing it.
     Changed: .btn-icon's and .btn-chip's "not .btn's size" comments. Both
       argued from a 10px/22px .btn. A .btn is now 28.8px tall against a
       chip's 28.4px and an icon button's 30px, so neither argument survives
       as written. What separates the three is type, weight and role.
     Fixed: the CONTROL PADDING scale's entry for 15px still named
       "[data-color-critical] .btn padding-x" — a rule 3.5.1 deleted. 3.5.1's
       own entry says six places were updated; this was a seventh, and it was
       missed. It now names the global rule.

   3.5.1 — 2026-10-05
     Removed: [data-color-critical] .btn / .btn-neutral padding. The scope
       resized buttons, which no other rule in it does — every other one
       removes accent, while this only made an accented button smaller.
       Buttons are now 10px/22px in both modes. Six places that documented the
       rule were updated in the same change; see the postscript on the scoping
       discipline's worked example for the lesson.
     Removed: .color-swatch from the scope's selector list (2026-10-01) — it
       set the border colour the default already had, so it did nothing.

   3.5.0 — 2026-09-25 (the file becomes its own styleguide)
     This release adds almost no CSS. It makes the file usable without
     reading its declarations, which was the point of the exercise: a product
     page should need no per-page CSS, and nobody should have to reverse-
     engineer a component to know what it is for.
     Added: a CONTRACT comment above all 54 components — what it is, when to
       use it and when NOT to (naming the alternative), required markup with
       ARIA, which states exist and which deliberately don't, constraints, and
       whether it may carry accent. Markup examples say whether they are
       VERIFIED against a reviewed page or not, so a reader knows which to
       trust.
     Added: COMPONENT INDEX, TYPOGRAPHY CONTRACT and STATE VOCABULARY at the
       top, beside the spacing and radius scales. The state block carries the
       variant-vs-state test: does it change during interaction (state — use
       an attribute) or is it chosen once when the markup is written (variant
       — use a class)?
     Added: TWO CORES section — nanoStream (streaming) and nanoStudio
       (production), in the order SKILL.md's naming rules require, with the
       consumer pages of each, why one stylesheet serves both, and why the
       trigger for the colour-critical scope is COLOUR-JUDGEMENT rather than
       product line.
     Added: the SCOPING DISCIPLINE, the condition attached to keeping one
       stylesheet rather than forking. Any change justified solely by
       colour-judgement reasoning belongs in [data-color-critical]. The review
       test: would this change still be right on a log viewer? If no, it is
       scoped. Two worked examples, both real: .status-strip's blue edge
       (passes, stays global) and .btn's padding (fails, scoped).
     Changed: .btn reverted to 10px/22px globally, with the tighter 6px/15px
       moved into [data-color-critical]; .btn-neutral's compensating padding
       moved in step. This was the scoping discipline's first application, and
       it changed a page that had already been approved — the discipline has
       to cost something the first time or it is not one.
     Verified: every documentation write in this release was checked by
       diffing 25 computed properties across all 159 elements of
       product-sample.html, before and after. Twelve writes, zero differences.

   3.4.0 — 2026-09-24 (Phase A: close the component set + the governing rule)
     Added: GOVERNING RULE at the top of this file — styling changes
       APPEARANCE ONLY and must never interfere with behaviour, with the nine
       specific ways CSS breaks a control and a required click-test. Written
       because a CSS-ONLY change to the chroma-key setup tuner made the
       Preview/Live toggle and the Player and Multiplayer buttons unclickable
       and had to be diagnosed with document.elementFromPoint.
     Added: note 10 — text in a button or a tag is ALWAYS centred explicitly,
       never with a compensating offset. Six controls were missing it.
     Added: the last components — .empty-state, progress (both engines,
       determinate-only), meter (its native green/yellow/red bands collapsed
       onto this file's two hues), hr, kbd, code/samp, radio + fieldset/legend,
       .notice-item, .switch-item, .helper promoted out of .field.
     Added: three documented NON-RULES, each with a decision table rather than
       silence — no tooltip, no blockquote, no skeleton loader. A stated
       omission is more useful than a gap somebody fills.
     Fixed: the checkbox was a native control tinted with accent-color, so an
       UNCHECKED box was OS-painted light-on-dark. Now custom, like the radio.
     Fixed: .modal-overlay had no inactive state, so each consumer invented
       one; worse, it sets display: flex, which OVERRIDES [hidden], meaning
       <div class="modal-overlay" hidden> rendered a visible full-screen
       click-blocker. Gated on the hidden state, never on the base rule —
       gating the base would have left multiplayer's modal unclickable.
     Fixed: input[type="range"] was a 402 x 6 drag target (the worst hit area
       in the file) and had no disabled state at all. Now 24px tall with the
       visible 6px track unchanged.
     Fixed: .journey::after and .station::before now declare pointer-events:
       none; the latter's safety had depended on a 70px indent nobody knew was
       load-bearing.
     Fixed: every disabled state moved off opacity 0.5 (which composited
       --text-muted to 2.60:1) to 0.6, and disabled selects take
       --text-primary so the value stays legible. Legible beats compliant.
     Fixed: .field now stacks automatically inside a workbench's panels via
       :has(); the pinch is container-driven, so the media query I first
       proposed could never have fired in the affected range.

   3.3.0 — 2026-09-23 (design review pass)
     Fixed: select was missing from the font-family reset AND declared none of
       its own, AND was still a native control — so the platform could
       substitute its own UI face. appearance: none plus an explicit
       --font-body, with our own chevron replacing the native one.
     Changed: slider thumbs to an outlined ring (dark fill, light stroke,
       13px) after a solid light disc read as too prominent beside a preview.
       The offset arithmetic was also wrong: it was content-box reasoning in a
       border-box file, measured 2px off centre, now derived from the render.
     Changed: .btn tightened to 6px/15px with explicit inline-flex centring
       and a stated line-height; .btn-icon to dark fill with a lighter stroke.
     Added: .switch-item — switch rows were held in a bare <p>, so three
       boxes baseline-aligned with a 6px centre spread.
     Added: panel-level vertical rhythm on .stats-row, .status-strip and
       table, matching .field's 16px; .button-row gained align-items: center
       after its text sat 8.59px high while the BOXES measured perfectly
       centred.

   3.2.0 — 2026-09-22 (second pass)
     Added: parameter controls section — input[type="range"] (both engines;
       neutral track and thumb, accent only on focus and drag; deliberately
       no filled portion, since only Firefox can do it), [role="switch"]
       (neutral in BOTH states), .btn-icon, .color-swatch + __sample,
       .btn-chip. None of these had any rule, so each was being painted in
       the VISITOR'S OS accent colour — observed as pink sliders on a machine
       whose system accent was pink.
     Added: .btn-neutral, a third button tier. .btn-secondary's documented
       meaning narrowed from "secondary/utility" to the positive primary
       path — the old wording was why a rail of six utility buttons all came
       out accent-blue.
     Added: .badge--value, a neutral mono numeric readout. A value is data,
       not an action or a state, and the base .badge has no background at all.
     Added: bare `select` and `input[type="file"]` element rules. Both were
       scoped to .field or absent, so either one outside a form rendered as a
       native OS-white control on a dark page.
     Added: .workbench-main, the vertical rhythm .container already had.
     Added: documented SPACING and RADIUS scales (see the blocks below :root).
     Added: [data-color-critical], an opt-in container-level scope for pages
       where the operator judges colour in the video itself.
     Fixed: the header/footer auto-widen rules overrode max-width, not width —
       dead code since 2026-09-08, because .header-inner's own width caps at
       1552px and the 1616px max-width could never bind. Header content sat
       32px right of the wide layout's edge at every viewport. Measured at
       1920 and 1400 before and after.
     Changed: single-edge accent stripes removed from .status-strip and
       .toast. This file forbids them at the .field rule — added 2026-09-01
       after a consuming page was corrected for exactly that — while shipping
       two of its own. Attention is now a 6px dot; the default state of both
       carries no colour at all.
     Changed: .control-rail's 2px accent top border and .journey::after's 3px
       accent hairline to --border-strong. Both were decoration, signalling
       neither action nor state.
     Changed: .control-rail 9px, .monitor 6px and .status-strip 6px radii to
       4px. All three were dark-neutral-studio's values, which SKILL.md's
       priority note says never to import.
     Changed: 23 literal #ffffff values to --text-primary or --text-on-accent,
       chosen by intent. Only .theme-pill's border stays literal — it is a
       border, and neither token means that.

   3.1.0 — 2026-09-22 (first pass)
     Added: data table section (table/caption/th/td, plus .num for
       right-aligned tabular figures) — the file had no table rules at all.
     Added: textarea support. It was missing from FOUR rules, not one — the
       font-family inherit reset, the outline reset, the :focus-visible list,
       and the .field control selector — plus vertical-only resize.
     Added: error state. border-color on ALL sides via [aria-invalid="true"],
       and .field .error for the message. Orange, never red, per note 6. The
       rule was already written in prose above the field inputs but had never
       been implemented, so pages were inventing their own.
     Added: bare h1–h3 now use --font-heading. They previously had no rule and
       fell back to the BODY font — a brand violation on any page using a
       plain heading.
     Changed: .panel carries its own 20px padding. Padding used to live only
       on .form-panel/.stats-panel/.log-panel, so a bare <div class="panel">
       rendered flush against its border. .panel--flush added to opt out;
       .player-panel's padding: 0 is now load-bearing, not redundant.

   3.0.1 — 2026-09-08
     Fixed: the header/footer auto-widen max-width is content-width + 96px
       (1616px against .workbench's 1520px), not the bare content width. See
       3.2.0 — this fix was itself ineffective and was corrected properly
       2026-09-22.

   3.0.0 — 2026-09-07
     Changed: universal page-margin standard. .container, .workbench,
       .header-inner and .footer-inner share --page-max-width (1520px) and
       --page-margin (48px); .container was 1040px with a 16px margin before.
     Changed: accents unified with the marketing brand hex — --accent-orange
       #F48220, --accent-blue #0F8DC8, replacing the muted product-only pair
       (#e5883c / #408bc3). Hovers stay LIGHTER than base.
     Added: --page-margin relaxes to 16px below 640px; email inputs joined the
       .field selector list; body[data-wide-layout] + --wide-content-width
       generalized the auto-widen past .workbench; the current-station halo is
       derived from its token via color-mix instead of a hand-typed rgba.

   2.1.0 — 2026-09-04
     Added: five optional page-pattern sections — drawer, toast, guided
       journey/station list, snippet viewer with language switch, detail rail
       — plus the AV workbench layout section.
     Added: the .page-version footer stamp, a mandatory brand requirement that
       was missing from this file entirely.
     Changed: .theme-pill takes a solid white border and a hover state; field
       labels 14px against 16px controls; footer gains margin-top: 40px and
       drops always-on link underlines.

   2.0.0 — 2026-09-03
     Changed, re-based on captions-workflow's in-production redesign: 3-tier
       surfaces — --bg and --panel-inset both #0d171e, --panel #19222b (was
       #252a2f), --panel-soft #202a32 deliberately LIGHTER than the panel it
       nests in, reversing the old "recessed = darker" convention.
     Changed: text unified to white (--text-secondary is no longer grey).
     Changed: fonts to Source Sans 3 and JetBrains Mono (were Source Sans Pro
       and Roboto Mono).
     Added: --shadow-sm / --shadow / --shadow-lg, and .panel now carries
       --shadow — superseding this theme's original flat, border-only surface.

   1.0.0 — 2026-09-01
     Baseline, extracted from player-diagnostics.html.
     Added: modal / dialog panel; the native focus-ring reset (Safari painted
       the visitor's OS accent on plain :focus — reported in the wild as
       "green outline on a blue button").
     Established three standing rules still in force: no infinite/looping
       animations (note 5), no green/red/yellow status colours (note 6), and
       no single-edge accent stripe (note the 3.2.0 entry — this file broke
       its own rule in two components for three weeks).

   USAGE
   --------------------------------------------------------------------------
   1. Load fonts in <head> before this stylesheet. Self-hosted is the target
      (see captions-workflow's own styles/fonts.css for a working example —
      GDPR: a Google Fonts request sends the visitor's IP to Google before
      consent is possible) — vendor the same families/weights from Google
      Fonts (OFL-licensed) into your own @font-face rules pointed at
      nanocosmos.net. Until that's done for a given page, the Google Fonts
      <link> below is an acceptable stopgap — this is a known open item, see
      the font-hosting note in SKILL.md:

      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      <link href="https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;700&family=Source+Sans+3:wght@400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
      <link rel="stylesheet" href="/assets/product-theme.css">

   2. Fonts:
      - Lexend Deca (700 Bold) — page/panel titles, buttons.
      - Lexend Deca (400 Regular) — header eyebrow labels only.
      - Source Sans 3 (400 Regular) — all other body text (labels,
        helper text, values, status text). This is the default body font.
        (Was Source Sans Pro — renamed upstream; captions-workflow's
        2026-09 redesign moved product pages onto the current name, so
        product pages no longer diverge from marketing's font-family choice
        here, only in hosting/weights.)
      - JetBrains Mono (400 Regular) — monospace content only (event log,
        raw JSON payload dumps). Was Roboto Mono — swapped 2026-09-03 to
        match captions-workflow, which already self-hosts this family.

   3. Components documented section by section below. Each section can be
      lifted independently — they only depend on the :root tokens.

   3a. Permission / consent gate (.engage-*, near the bottom, before the
      footer section): optional — only include it if the page gates a
      sensitive browser permission request (camera, mic, location, etc.)
      behind an explicit user action. Skip it entirely otherwise.

   3b. Header product identity: use the product line's canonical logo image
      (.logo-lockup) — never spell the product name out as plain eyebrow
      text (e.g. don't type "nanoStream"). See the comment above
      .logo-lockup for the canonical asset URL per product line.

   4. This file is shared by both player-diagnostics.html (the tool itself)
      and diagnostics-theme-moodboard.html (the visual reference page) —
      both link this file directly (`assets/product-theme.css` in that
      repo) rather than embedding a copy; player-diagnostics.html moved
      off its old inline self-contained copy 2026-09-04, once the file
      that would previously have needed manual re-syncing could just be
      linked instead. The final section, "Moodboard / documentation page
      styles", is only used by the moodboard.

   5. No infinite/looping animations (2026-09-01, direct feedback on two
      separate product-mode pages). Status/loading dots, spinners, "breathe"
      or "ping" effects — all static, never `animation: ... infinite`. State
      is communicated by color and label already; motion on an element the
      visitor sits looking at for a while reads as distracting, not
      informative. One-shot ENTRANCE animations (a card fading/sliding in,
      a toast sliding in) are unaffected by this rule — the objection is to
      motion that never stops, not to all motion.

   6. No green/red/yellow status colors, on ANY product/demo page (2026-09-01,
      direct instruction — not brand colors, must not appear even for
      success/error/warning/live semantics). This file was already
      status-token-free for exactly this reason ("Status" section deliberately
      doesn't exist here — a consuming page must not invent green/red/amber
      tokens of its own either, which is what captions-workflow's own
      base.css originally did before this was caught and corrected). If a
      page needs to signal success/error/warning/live states, use ONLY
      --accent-orange (attention: error, warning, live — bucketed together,
      there is no third hue) and --accent-blue (positive: success, done).
      Icons and text labels carry the specific meaning; color only signals
      attention-level. Don't reach for a semantic-but-off-brand hue just
      because it's the conventional choice elsewhere.

      AND THERE IS ONLY ONE ORANGE: --accent-orange, #F48220, on every page,
      everywhere. The muted product-only pair (#e5883c / #408bc3) was retired
      on 2026-09-07 when product and marketing unified — those values are
      history, and the only place they may appear is a comment explaining what
      they were. This is stated because forbidding the wrong HUES was not
      enough: two consumers still carry #E5883C today, hand-typed as
      rgba(229, 136, 60, …) in an error box, which no hex grep would find.
      Same hue, wrong value, invisible to the sweep meant to catch it. If you
      need orange, reference the token.

   7. Panels carry a real shadow (2026-09-03, re-based on captions-workflow's
      in-production redesign — see the :root block below). This SUPERSEDES
      the theme's original "flat, no drop-shadow, border + fill only"
      philosophy for the base .panel surface: captions-workflow's own
      design language leans on shadow-based depth as a core mechanism (a
      "One Lift Rule" — one shadow-level surface per screen — documented in
      that repo), and stripping shadows back out would fight the real page
      this theme is meant to track. The .modal-panel exception noted
      elsewhere in this file is no longer the ONLY shadowed surface, just
      the one with the strongest (--shadow-lg) elevation.

   8. Five more optional, page-pattern sections were added 2026-09-04, all
      extracted from captions-workflow's in-production implementation (the
      reusable structure/behavior only, never that page's own content) —
      each is documented with its own "why" at its section header, use only
      what a given page actually needs:
        - .drawer-overlay / .drawer-panel — a side-sliding panel for
          content too large for a centered modal.
        - .toast-stack / .toast — transient corner notifications.
        - .journey / .station — a guided, one-step-at-a-time setup flow
          (distinct from the AV workbench pattern above, which is for a
          live preview/monitor page instead).
        - .snippet / .lang-switch — a copyable, multi-language code sample
          block, including hand-rolled syntax coloring built from this
          file's own two accents plus its text-color scale (no new hue).
        - .detail-rail — a sticky, wide-viewport-only companion panel for
          reference material that would crowd a narrow page.

   9. Header/footer auto-widen for a product-page pattern WIDER than the
      universal standard (2026-09-07, fixed 2026-09-08, generalized
      2026-09-07, superseded-by-default for the common case 2026-09-07). As
      of the universal page-margin pass (see --page-max-width/--page-margin
      in :root), .header-inner/.footer-inner/.container/.workbench all
      already share the same 1520px content cap / 48px margin by default —
      this item's override is only still needed for a pattern that
      deliberately goes WIDER than that shared baseline (captions-workflow's
      guided-journey dev-view rail row is the one real example, at 1740px).
      Add `data-wide-layout` to <body> and set `--wide-content-width` to that
      wider pattern's own max content width to opt in — no edit to this file
      needed. `.workbench` is pre-registered too, at its own 1520px value,
      which is now redundant with the default (harmless: both converge on
      the same content edge, see the arithmetic below) but left in place
      since removing it buys nothing. Pages that never exceed the 1520px
      default need no override at all.

      Either rule's max-width is content-width + 96px, NOT the bare content
      width — this looks wrong but isn't. A wide layout typically has zero
      side padding of its own (its width IS its visible edge-to-edge box),
      while .header-inner/.footer-inner carry 48px of padding on top of
      their box (so their logo/text starts inset from the box edge, not
      flush with it). Give both the same bare content-width max-width and
      the header/footer content ends up 48px further right than the wide
      layout's edge at any viewport above the breakpoint — confirmed
      empirically at 1920px on live-plus-replay and chromakey-tuner-demo
      (content at x=248 vs. workbench at x=200, screenshot-measured) for
      the .workbench case, and independently re-derived (without this
      note) in captions-workflow's own hand-rolled CSS for its
      guided-journey dev-view row. Padding is content *inside* a
      border-box width, so to get a content-width-wide content area that
      starts flush with the wide layout's edge, the box itself has to be
      content-width + (48 * 2) = content-width + 96px. Do not "simplify"
      this back to the bare content width to match the wide layout's own
      value — that reintroduces the 48px gap.

  10. Text in a button or a tag is ALWAYS centred, explicitly (2026-09-24,
      direct instruction, after this same class of bug was reported three
      separate times on three different controls). Any control carrying a
      label — .btn and its tiers, .btn-icon, .btn-chip, .color-swatch,
      .theme-pill, .engage-btn, .lang-switch__btn, .detail-rail__tab,
      .toast__dismiss, .drawer-panel__close, .station__marker, .badge —
      declares:

        display: inline-flex;      (or flex)
        align-items: center;
        justify-content: center;   (unless the control is a label/value pair
                                    laid out ALONG the row — .raw-toggle's
                                    space-between, .color-swatch's sample +
                                    text — where flex-start is the point)
        line-height: <a number>;   wherever the control holds text

      WHY each part is load-bearing, because "it looks centred" is how this
      kept getting missed:
        - A bare <button> centres its text horizontally by default, but
          VERTICALLY it sits on the default line box, which follows the
          font's own ascent/descent. Lexend Deca 700 has tall ascenders, so
          in a tight pill the label rendered optically high — measured on
          .btn as 8px of space above the ink against 7px below.
        - line-height: normal makes the box height a font-metric outcome, so
          it shifts between faces and sizes. A stated number makes it
          deterministic.
        - In a FLEX ROW, align-items defaults to `stretch`, which stretches a
          non-button sibling to the row's height and leaves its single line
          of text at the TOP of that box. Measured on .button-row: the boxes
          were perfectly centred (0.00px spread) while the text sat 8.59px
          high. That is why .button-row, and every other flex row in this
          file, declares align-items explicitly.

      NEVER centre a label with a compensating margin, a top offset or
      asymmetric padding. If a fix contains a number that exists only to push
      a glyph into place it is wrong — the container should be doing it.

   ========================================================================== */

:root {
  /* Universal page-margin standard (2026-09-07, direct request) — every
     product page, whatever pattern it uses (.container, .workbench, or a
     page's own hand-rolled equivalent like captions-workflow's .cw-column),
     shares these same two numbers so the header/footer bar and the main
     content always start and end at the identical pixel, on every page, not
     just internally on one page. 1520px/48px are .workbench's original
     numbers (chromakey-tuner-demo, the reference this was checked against)
     — .container and the header/footer now match them exactly instead of
     each pattern picking its own width. */
  --page-max-width: 1520px;
  --page-margin: 48px;

  /* Surfaces — 3-tier system re-based 2026-09-03 on captions-workflow's
     in-production redesign (see that repo's styles/base.css for the
     original derivation). Page background AND text/form-input boxes share
     the DARKEST tier (--bg) — a deliberate merge, not a coincidence: an
     input box reads as "a field" via its border + being darker than the
     panel it sits in, not via a separate color of its own. Panels/cards
     (--panel) are a lighter mid-tone. Nested-but-lighter chip content
     (dropdown option hover, disabled-state fills) is --panel-soft — since
     2026-10-20 (4.0.0, direct request) the exact midpoint of --bg and
     --panel, #131d25, so it now sits BETWEEN the two darkest tiers. It was
     #202a32, lighter than --panel, from 2.0.0 until then. --panel-inset is kept as a
     token (rather than merged into --bg outright) for legibility at call
     sites that mean "this is a field-shaped box", even though its value
     now equals --bg exactly. */
  --bg: #0d171e;               /* page background AND text/form-input boxes (darkest tier) */
  --panel: #19222b;            /* card / panel background (was #252a2f) */
  --panel-inset: #0d171e;      /* inputs, log body, code blocks — same darkest tier as --bg (was #1c2228, a separate darker tier) */
  --panel-soft: #131d25;       /* exact midpoint of --bg and --panel (was #202a32, lighter than --panel): disabled fills, hover states, dropdown options */
  --border: #2e343d;           /* 1px borders on panels, inputs, dividers */
  --border-strong: #3a4149;    /* borders that need more contrast than --border (e.g. against --panel-soft) */

  /* Accents */
  /* Unified with the marketing brand hex 2026-09-07 (direct request). These
     were deliberately muted variants (#e5883c / #408bc3) scoped to the dark
     product surface; product and marketing now share one accent pair.
     Hover stays LIGHTER than base — on a dark surface a darker hover reads
     as pressed, so marketing's --orange-dark/--blue-dark are NOT used here. */
  --accent-orange: #F48220;        /* primary / committing actions, footer */
  --accent-orange-hover: #F79A4A;  /* hover/active state for orange elements */
  --accent-blue: #0F8DC8;          /* secondary actions, checked checkboxes */
  --accent-blue-hover: #33A5DD;    /* hover/active state for blue elements */

  /* Text — unified to white 2026-09-03 (was: white outside boxes, grey
     inside them). Box-ness is now carried entirely by --panel-inset being
     darker than --panel, so text no longer has to do that job too —
     --text-secondary equals --text-primary now; kept as a separate token
     only so existing call sites that reference it by name don't need a
     find/replace. --text-muted stays a real grey — reserved strictly for
     placeholder text and disabled state, which must read as inactive. */
  --text-primary: #ffffff;     /* headings, values, primary labels — was #eef1f3 */
  --text-secondary: #ffffff;   /* now equals --text-primary — was #97a1ad (grey, "inside a box") */
  --text-muted: #9aa1a8;       /* placeholders, disabled state, and neutral control handles (scope widened 2026-09-22, see the slider thumb) — was #6b7480 */
  --text-on-accent: #ffffff;   /* text sitting on orange/blue fills */

  /* Fonts — Source Sans 3 / JetBrains Mono as of 2026-09-03 (were Source
     Sans Pro / Roboto Mono) — matches captions-workflow, which self-hosts
     both. See the USAGE note above for hosting. */
  --font-heading: 'Lexend Deca', sans-serif;   /* titles + buttons, weight 700 (400 for header eyebrow) */
  --font-body: 'Source Sans 3', 'Open Sans', sans-serif;  /* everything else, weight 400 */
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;  /* event log, raw payload dumps */

  /* Shadows — added 2026-09-03, re-based on captions-workflow (see note 7
     above). Neutral black, not brand-tinted — a shadow on a dark page just
     needs to read as depth, not carry color. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.45);      /* base .panel elevation */
  --shadow-lg: 0 22px 60px rgba(0, 0, 0, 0.6);   /* .modal-panel — the strongest elevation on the page */
}

/* SPACING SCALE (documented 2026-09-22, not newly invented — this is the
   de facto scale already used throughout this file, written down so a new
   component stops re-picking a number from memory):

     4  6  8  10  12  14  16  18  20  24  40

   Where each is already load-bearing:
     4   badge/button micro-gaps, .panel border-radius
     6   .field-control gap, small radii, .btn padding-y
     8   checkbox/label gap, input border-radius, .toast gap
     10  .button-row gap, input padding-y
     12  .info-row gap, .raw-payload padding
     14  .status-bar padding-y, .drawer-panel__head padding-y, .btn-neutral
         padding-x
     16  .header-inner padding-x, .control-section padding/margin
     18  .control-rail padding, .workbench-layout gap, .header-left gap
     20  .panel padding, .container gap, .stats-row gap
     24  .container padding-top
     40  .container padding-bottom, .footer margin-top

   CONTROL PADDING is a SECOND, finer range (documented 2026-09-29, after an
   audit found the file quietly breaking its own rule in 21 places):

     1  2  3  5  7  9  11  13  15

   The layout scale above governs the space BETWEEN things — gaps, margins,
   section rhythm. It was derived from that job and is deliberately coarse.
   Control INTERIORS need values it cannot supply: a small pill wants 5px of
   vertical padding, where 4 is tight and 6 is loose, and the scale jumps
   4 → 6 → 8 → 10 with nothing usable between. Every off-scale value the audit
   found was a control's own padding in the 1–15px band, never a layout gap.

   Where each is currently load-bearing:
     1   .toast__body / .station__summary margin-top — an optical nudge under
         a title, where 4px would read as a gap
     2   .badge--value padding-y, .mb-swatch-name margin — 4px would double a
         small badge's height
     3   .toast__dismiss padding
     5   .theme-pill / .btn-chip / .color-swatch padding-y; .btn-neutral
         padding-y, deliberately 1px less than .btn to offset this tier's
         border
     7   .status-strip / .info-row padding-y
     9   .header padding-y
     11  .toast padding-y
     13  .badge padding-x
     15  .btn padding-x — globally since 2026-10-05, and no longer scoped.
         This line named the scoped copy for one release after that copy was
         deleted; a scale that points at a dead rule is worse than no scale.

   .btn's 6px padding-y is a LAYOUT-scale value used inside a control, which
   the paragraph below would normally send to this range. Deliberate: the
   control range is odd-numbered and has no 6, 5 sets visibly tight under
   14px Lexend Deca 700, and 7 overshoots. Where the layout scale has the
   step, use it.

   Use the LAYOUT scale for anything between elements. Use CONTROL PADDING
   only inside a control, and only when the layout scale genuinely has no
   usable step — it is a concession to optics, not a free hand.

   TWO NAMED EXCEPTIONS, outside both ranges:
     -4.5px  input[type="range"]::-webkit-slider-thumb margin-top. Derived,
             not chosen: (trackHeight / 2) - (thumbHeight / 2) - 1, which is
             3 - 6.5 - 1 for a 13px thumb on a 6px track. Measured against
             the render, not the spec. Re-derive it if either size changes;
             do not round it to a scale value.
     -26px   .detail-rail__body::after margin, pulling the "more below" fade
             back over the content it covers. It must equal the fade's own
             height (26px) with a negative sign, so the two move together.

   Deliberately NOT tokenised: these are layout constants, not themeable
   values, and turning them into custom properties would invite pages to
   override them and break the shared rhythm. Pick from the appropriate
   range; don't add a step to either without a reason. */

/* COMPONENT INDEX (2026-09-25). Every component in this file, grouped by
   purpose. Each has a CONTRACT comment above its rules — search "CONTRACT —"
   plus the name. Components marked (opt) are optional sections a page
   includes only if it needs them; skip them entirely otherwise.

   STRUCTURE / LAYOUT
     .container ................ standard page column (non-preview pages)
     .workbench ................ page column for a preview-led tool
     .workbench-layout ......... its two-column grid
     .workbench-main ........... the left column's vertical rhythm
     .panel .................... the card used everywhere
     .panel--flush ............. a panel whose content reaches the border
     .stats-row / .stats-panel . two-column grid of panels
     .monitor .................. the workbench's hero preview surface
     .player-panel ............. video panel WITH a status bar (the other one)
     .control-rail ............. the workbench's parameter rack
     .control-section .......... one divided group inside that rail
     .detail-rail (opt) ........ sticky wide-screen reference companion
     .header + parts ........... brand bar; every page, unchanged
     .footer / .page-version ... orange closing bar; every page, unchanged

   FORM
     .field / .field-control ... labelled row, label beside control
     .field--stacked ........... same, label above (narrow containers)
     .control-row .............. label/control/value row for a narrow rail
     .helper ................... explanatory line under a control
     .field .error ............. validation message (with [aria-invalid])
     .form-result / --attn/--ok  outcome of a form SUBMISSION
     .checkbox-row ............. bordered band of checkbox/radio items
     .checkbox-item ............ checkbox + label
     .radio-item ............... radio + label
     .switch-item .............. switch + label + optional affordance
     fieldset / legend ......... semantic grouping, no visual chrome
     input / textarea / select . the field-shaped box
     input[type="file"] ........ pill button + filename
     input[type="range"] ....... slider, 6px track, ring thumb

   ACTION
     .button-row ............... row of controls, with panel rhythm
     .btn + .btn-primary ....... the one committing action
          + .btn-secondary ..... positive primary-path action
          + .btn-neutral ....... plain utility action
     .btn-chip ................. toolbar-scale toggle, token, or tab
     .btn-icon ................. 30px round icon button
     .btn-link ................. a button that looks like a link
     .btn--block ............... width modifier: .btn fills its container
     .color-swatch ............. colour choice (the one inline style)
     .theme-pill ............... header's own button pair, nowhere else
     .raw-toggle ............... the theme's only disclosure control

   DATA
     .badge + variants ......... non-interactive status pill
     .badge--value ............. neutral mono numeric readout
     .info-row / .wrap ......... label/value line
     table / th / td / td.num .. multi-column row list
     .log-panel / .log-box ..... scrolling monospace event log
     .raw-payload .............. machine output block
     code / samp / kbd ......... inline literal, keycap
     progress / meter .......... determinate bar, banded measurement
     .snippet / .lang-switch (opt) copyable multi-language code sample

   FEEDBACK
     .status-strip / --attn .... mode, connection or stream state
     .notice-item .............. local dismissable message in a panel
     .toast-stack / .toast (opt) transient corner notification
     .empty-state .............. "nothing here yet" inside a panel
     .modal-overlay / .modal-panel (opt) centred dialog
     .drawer-overlay / .drawer-panel (opt) side-sliding panel
     .engage-overlay (opt) ..... permission consent gate
     .journey / .station (opt) . step-by-step setup flow

   SCOPE
     [data-color-critical] ..... neutralises accent beside a preview

   NOT A COMPONENT SET
     .mb-* ..................... moodboard presentation only; never on a
                                 product page

   DELIBERATELY ABSENT — see the NON-RULE blocks for what to use instead:
     tooltip, blockquote, skeleton loader, sign-up form */

/* TYPOGRAPHY CONTRACT (2026-09-25). Four faces, four jobs. This is a rule,
   not a palette to choose from.

     Lexend Deca 700 ....... titles and buttons
     Lexend Deca 400 ....... the header eyebrow ONLY (.nav-label .eyebrow).
                             Anywhere else, 400 weight of the heading face is
                             a violation.
     Source Sans 3 400 ..... body text, labels, values. The default.
     JetBrains Mono 400 .... event log, raw payload, and inline literals
                             (code, samp, kbd, .badge--value, .snippet)

   EVERY TEXT-BEARING ELEMENT MUST DECLARE font-family EXPLICITLY.

   Not "should" — this is the rule a real bug came from. Form controls do NOT
   inherit font-family from body the way most elements do: each has to be
   listed in the reset near the top of this file or the browser substitutes
   its own UI font. `select` was missing from that list AND declared none of
   its own AND was still natively rendered, so the platform could choose the
   face. It was reported as "wrong font, letter spacing too tight" and took
   three separate investigations to pin down, because nothing in the CSS
   looked wrong.

   Silent fallback to a browser default is invisible in code review and
   invisible in a screenshot taken on the machine that has the font. Declare
   it. The same applies to any new element this file starts styling: check
   whether it inherits, and if in doubt, declare it. */

/* STATE VOCABULARY (2026-09-25). State is expressed with SEMANTIC ATTRIBUTES,
   not classes, so the visual state cannot drift from the state assistive
   technology is told about.

     [aria-pressed="true"] ..... a toggle is on / a token is selected
                                 (.btn-chip, .btn-icon, .color-swatch)
     [aria-selected="true"] .... a tab is the current one (.btn-chip)
     [aria-invalid="true"] ..... a form control has failed validation
     [aria-expanded] ........... a disclosure is open (.raw-toggle, and a
                                 .btn-icon toggling a .helper)
     [role="switch"] ........... this checkbox is a switch, not a checkbox.
                                 REQUIRED — without it the checkbox rule
                                 applies and you get a box.
     [disabled] / :disabled .... the control is unavailable. CSS styles it;
                                 CSS must never CAUSE it (governing rule 6).
     [hidden] .................. not currently shown. Note governing rule 2a:
                                 a component that sets `display` overrides the
                                 UA [hidden] rule and must handle it
                                 explicitly, as .modal-overlay does.
     [data-state] .............. done / current on .station
     [data-color-critical] ..... page-scope, not a control state
     [data-wide-layout] ........ page-scope, header/footer widening

   KNOWN EXCEPTION, do not copy: .lang-switch__btn.active and
   .detail-rail__tab.active use a CLASS. Both predate this vocabulary and are
   in use downstream, so migrating them means markup changes in every
   consumer — see BATCH-FINDINGS item 6. New components use the attributes
   above.

   CLASSES still express VARIANT, which is not state: .badge.playing,
   .toast--attn, .btn-primary, .field--stacked. The test is whether it changes
   during interaction (state — use an attribute) or is chosen once when the
   markup is written (variant — use a class). */

/* ==========================================================================
   GOVERNING RULE: this file changes APPEARANCE ONLY (2026-09-24)
   ==========================================================================

   Styling must never change, disable, block or otherwise interfere with
   what a control does.

   WHY THIS IS A RULE AND NOT A PLATITUDE: the chroma-key setup tuner
   regression that started this batch was exactly this failure. A CSS-ONLY
   merge request made the Preview/Live toggle and the Player and Multiplayer
   buttons unclickable. No JavaScript changed. The cause was a stacking
   context or a missing pointer-events putting an invisible layer over the
   controls. It shipped to a branch and had to be diagnosed with
   document.elementFromPoint. A broken click target looks completely normal.

   THE SPECIFIC WAYS CSS BREAKS BEHAVIOUR:

   1. pointer-events: none must never be set on an interactive control, or
      on anything that sits above one.
   2. A hidden or collapsed overlay (dropdown, backdrop, modal, drawer,
      toast) must set pointer-events: none while inactive. opacity: 0 or
      visibility: hidden ALONE still intercepts clicks.
   2a. A component that sets `display` OVERRIDES the UA stylesheet's
      [hidden] { display: none } — so marking it hidden the standard way does
      nothing. .modal-overlay sets display: flex, which means
      <div class="modal-overlay" hidden> renders a fully visible,
      full-screen, click-blocking overlay. This is more dangerous than the
      pointer-events half because the markup looks correct. Any component in
      this file that sets `display` must also handle [hidden] explicitly.
   3. transform, opacity < 1, filter, will-change, and a changed position on
      a PARENT each create a new stacking context, which can raise an
      invisible layer over a control that looks completely fine. Any rule
      adding one of these near interactive content must be click-tested.
   4. Decorative ::before / ::after that overlay a control must set
      pointer-events: none.
   5. overflow: hidden must never clip an interactive control out of reach.
   6. CSS must never fake a disabled state. :disabled and [aria-disabled]
      REFLECT state the page owns; styling them is correct, using CSS to
      prevent interaction is not.
   7. appearance: none changes rendering only. Keyboard operation, focus
      order and form semantics must survive it — every custom-rendered
      control here (select, range, switch, radio, checkbox) is still the
      native element underneath and must stay that way.
   8. :hover must never be the only route to a control or its content.
      Touch devices have no hover.
   9. A control's hit area must never drop below 24 x 24 CSS px
      (WCAG 2.5.8), even where the visible element is smaller.

   VERIFICATION, REQUIRED AFTER ANY CHANGE TO THIS FILE: click every
   interactive control on the reference pages, in every relevant state. A
   visual check is NOT sufficient. If a control does not respond, run
   document.elementFromPoint(x, y) at the pointer position to find what is
   actually on top. */

/* RADIUS SCALE (documented 2026-09-22, after an audit found four different
   radii on box-like components with no rule saying which to use):

     4px    structural surfaces — panels, cards, rails, the monitor, status
            strips, modals, drawers, the journey card, the detail rail.
            SKILL.md's "deliberately squared-off, not pill-like" standard.
     8px    inset field-shaped boxes — inputs, select, textarea, .log-box,
            .raw-payload, .info-row.wrap .value, .snippet, .lang-switch,
            .toast. Recessed --panel-inset content, one step softer than the
            structure around it.
     999px  pills — buttons, badges, chips, swatches, switch tracks.
     50%    circles — dots, slider thumbs, markers, swatch samples.

   6px and 9px were dark-neutral-studio's numbers (its monitor and status
   strip are 6px, its control rail 9px), copied in with that skill's layout
   and never reconciled. SKILL.md's dark-neutral-studio priority note is
   explicit that it supplies layout, never values — .monitor, .status-strip
   and .control-rail moved to 4px 2026-09-22. Don't reintroduce either step.

   Note on 8px: SKILL.md's "pill buttons at 8px radius belonged to the old
   App/Demo shells" line is about BUTTONS, not field boxes. The inset group
   above is not legacy and is not migrating to 4px. */

/* --page-margin's 48px is a DESKTOP figure. Left unrelaxed it costs a quarter
   of a 390px phone screen (2 x 48px gutter + 2 x 16px inner padding = 128px of
   390), which measured visibly wrong on a real page (vod-player, 2026-09-07).
   Overriding the token alone is enough: .container, .header-inner,
   .footer-inner, .workbench and .mb-container all derive their width from it,
   so they relax together and stay aligned with each other — which is the whole
   point of the token existing. Do not "fix" a cramped phone layout by
   hand-editing one of those rules instead. */
@media (max-width: 640px) {
  :root { --page-margin: 16px; }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-weight: 400;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main { flex: 1; }

a { color: inherit; }

/* Headings (2026-09-22). Bare h1-h3 had NO rule in this file, so they fell
   back to the browser default — which means the BODY font (Source Sans 3),
   not Lexend Deca: a brand violation on any page using a plain heading
   rather than one of the component title classes.

   Deliberately font/weight/color only — no margin, no font-size. Every
   existing scoped treatment (.stats-panel h2, .log-panel h2, .mb-intro h1)
   is a two-selector rule that already out-specifies this, so nothing
   currently styled changes. Verified 2026-09-22 that no collision is
   possible from the other direction either: every component title class in
   this file (.station__title, .drawer-panel__title, .toast__title,
   .mb-section-title, .modal-panel__title) sits on a div/p/span in the
   reference pages, never on a heading element. */
h1, h2, h3 {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--text-primary);
}

/* Form controls do NOT inherit font-family from body the way most elements
   do — each has to be listed here explicitly or the browser substitutes its
   own UI font. textarea added 2026-09-22 (it was rendering in the browser's
   default monospace); select added 2026-09-23, after a review caught the
   "Cover" dropdown setting in the system UI face with visibly tighter
   letter-spacing than the Source Sans 3 around it.

   Note this was NOT fixed by the select rules further down: neither the bare
   `select` rule nor the `.field select` list declares font-family, so both
   inherited the browser default. One reset line covers every select on every
   page instead. */
button, input, textarea, select {
  font-family: inherit;
}

/* Native browser focus-ring reset (added 2026-09-01, caught on a real
   page — captions-workflow). Without this, a plain mouse CLICK on a
   focusable element with no outline rule of its own falls through to the
   browser's NATIVE outline on plain :focus (most browsers only paint
   :focus-visible for keyboard interaction, not pointer clicks) — and
   Safari's native ring follows the visitor's OS system accent color, NOT
   this theme's --accent-blue. Reported in the wild as "green outline on
   a blue button." This reset makes :focus-visible below the only possible
   source of a visible ring, on every element type, regardless of how
   focus was triggered. */
a, button, select, input, textarea, [tabindex] {
  outline: none;
}

/* Focus states — keep visible, this is a tool meant for keyboard use */
input:focus-visible,
button:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
}
.btn-primary:focus-visible {
  outline-color: var(--accent-orange-hover);
}

/* ==========================================================================
   Header — brand-blue gradient bar, full width, content column-aligned
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .header and its parts
   WHAT IT IS       The page's brand bar: a blue gradient strip with the
                    product lockup, the page name and the company mark.
   WHEN TO USE      Every product page, unchanged. Copy it verbatim.
                    NOT a place for page controls — those belong in the page.
                    The only buttons here are the standard .theme-pill pair.
   MARKUP           <header class="header">
                      <div class="header-inner">
                        <div class="header-left">
                          <img class="logo-lockup" src="..." alt="nanoStudio">
                          <div class="header-divider"></div>
                          <div class="nav-label">
                            <span class="eyebrow">Theme coverage</span>
                            <span class="title">Workbench Archetype</span>
                          </div>
                        </div>
                        <div class="header-right"> ... </div>
                      </div>
                    </header>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not stick — it scrolls with the page. Does not carry
                    navigation, a mobile drawer or an announce bar: those are
                    marketing-tier, and a product page has none. Identify the
                    product with the LOGO IMAGE, never by typing the product
                    name as eyebrow text.
   ACCENT POLICY    The gradient is brand IDENTITY, not accent — the one place
                    in this file where identity colour is the point. Everything
                    on it is white. .nav-label .eyebrow is the ONLY sanctioned
                    use of Lexend Deca 400 in the whole theme.
   -------------------------------------------------------------------------- */
.header {
  /* Dark-to-light blue gradient, weighted toward the darker stop so the
     lighter tone only takes over in the bottom quarter. Adjust the two
     hex values and the 25% stop to retune the balance/smoothness. */
  background: linear-gradient(180deg, #00486c 0%, #00486c 25%, #0f8dc8 100%);
  padding: 9px 0;
  border-bottom: 1px solid #0f8dc8;
}

/* Shares the same width formula as .container / .footer-inner so
   header/body/footer content edges all line up regardless of viewport.
   Unified 2026-09-07 (direct request) with the AV-workbench margin standard
   below — .container used to cap at a narrower 1040px with a 16px-margin
   shrink, .workbench at 1520px with a 48px-margin shrink, and item 9 further
   down existed ONLY to bridge that gap for workbench pages. There is now one
   standard for every product page, workbench or not: content maxes at
   var(--page-max-width) (1520px) and never sits closer than
   var(--page-margin) (48px) to the viewport edge. The width formula bakes in
   this element's own 16px side padding (unlike .workbench, which has none of
   its own) so the padding-free workbench edge and this padded header's
   content still land on the exact same pixel — same reasoning as item 9's
   content-width + 96px math, just folded into the default rule instead of a
   separate override. */
.header-inner {
  width: min(calc(100% - (var(--page-margin) - 16px) * 2), calc(var(--page-max-width) + 32px));
  margin: 0 auto;
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 18px;
}

/* Logo lockup image (brand mark + product name combined asset).
   Standard: identify the product line with its canonical logo image here,
   not with plain eyebrow text (e.g. don't type "nanoStream" as a label —
   use the logo asset below). Canonical URLs by product line:
     nanoStream — https://www.nanocosmos.net/wp-content/uploads/2026/06/nanoStream-logo-V1.webp
     nanoStudio — https://www.nanocosmos.net/wp-content/uploads/2026/06/nanoStudio-logo-V2.webp
   Pair it with .header-divider + .nav-label (below) for the page-specific
   name (e.g. "Instant Test", "Player Diagnostics") — see product-sample.html. */
.logo-lockup {
  display: block;
  height: 30px;
  width: auto;
}

.header-divider {
  width: 1px;
  height: 34px;
  background: rgba(255, 255, 255, 0.22);
}

.nav-label {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.nav-label .eyebrow {
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 400; /* Lexend Deca Regular, not Bold — small caps eyebrow */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-on-accent);
}

.nav-label .title {
  font-family: var(--font-heading);
  font-size: 16px;
  font-weight: 700; /* Lexend Deca Bold */
  color: var(--text-on-accent);
}

.header-right {
  display: flex;
  align-items: center;
  gap: 18px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .theme-pill
   WHAT IT IS       A see-through white-outlined pill for the header's own
                    button pair.
   WHEN TO USE      In .header-right, for "Start for free" and "Contact" —
                    always exactly those two, or neither.
                    NOT anywhere else on the page: on a panel it has no
                    contrast, being designed for the blue gradient. NOT for a
                    page action — that is .btn.
   MARKUP           <a href="https://www.nanocosmos.net/support/" class="theme-pill">Contact</a>
                    (verified example, from product-sample.html — the approved
                    reference omits the pair deliberately, being a test page)
   STATES           default and hover only.
   DOES NOT DO      Never fills solid. Its border is a literal #ffffff, not a
                    token — see the note immediately below.
   ACCENT POLICY    Carries no accent — white on the gradient.
   -------------------------------------------------------------------------- */

/* The border below stays a literal #ffffff, not var(--text-on-accent)
   (2026-09-22): it is a BORDER colour, and --text-on-accent means "text
   sitting on an accent fill". The two values are identical, but using the
   token here would have it say something it doesn't mean. Every other white
   in this file is tokenised.

   Solid white border (2026-09-04, matches captions-workflow/product-sample.html
   exactly) — was a translucent rgba(255,255,255,0.35) border with no hover
   state at all. This is the standard treatment for the header's own button
   pair (see .header-right usage note above .theme-pill's markup in
   product-sample.html: "Start for free" + "Contact", always exactly these
   two, see-through with this white outline — never solid-filled). */
.theme-pill {
  /* Label centring — see note 10 at the top of this file. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  border: 1px solid #ffffff;
  background: transparent;
  color: var(--text-on-accent);
  border-radius: 999px;
  padding: 5px 14px;
  font-size: 12px;
  font-weight: 500;
  text-decoration: none; /* often used on <a> nav links — don't show the default link underline */
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s;
}
.theme-pill:hover { background: rgba(255, 255, 255, 0.14); }

/* Secondary brand mark image (company logo, right side of header) */
.brand-mark {
  display: block;
  height: 26px;
  width: auto;
}

/* ==========================================================================
   Page layout
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .container
   WHAT IT IS       The standard page column for a product page.
   WHEN TO USE      Any product page that is NOT built around a live preview —
                    a form, a list, a report, a settings page.
                    NOT for a page with a monitor/preview and a control rail:
                    that is .workbench. Both share the same outer edge
                    (--page-max-width / --page-margin), so they are
                    interchangeable from the header's point of view; what
                    differs is the inner content shape.
   MARKUP           <main><div class="container"> ... </div></main>
                    (example from product-sample.html — the approved
                    reference tests/archetype-workbench.html is a .workbench
                    page and does not demonstrate this component)
   STATES           None. This is layout.
   DOES NOT DO      Does not set a background — the page does. Does not pad
                    its children; it spaces them with gap: 20px, so direct
                    children need no margins of their own.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Same width formula as .header-inner/.footer-inner (see the note there) —
   was a flat max-width: 1040px before the 2026-09-07 universal margin pass;
   now matches .workbench's content edge on every page, not just its own. */
.container {
  width: min(calc(100% - (var(--page-margin) - 16px) * 2), calc(var(--page-max-width) + 32px));
  margin: 0 auto;
  padding: 24px 16px 40px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .panel
   WHAT IT IS       The card used everywhere: a bordered, shadowed surface
                    with its own padding.
   WHEN TO USE      Any grouped block of content. Pair with a page-type
                    companion where one fits (.form-panel, .stats-panel,
                    .log-panel, .player-panel) — those set padding of their
                    own and override this rule's.
                    NOT for a full-bleed surface: that is .panel--flush.
   MARKUP           <section class="panel stats-panel">
                      <h2>Foreground</h2>
                      ...
                    </section>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None. A panel is a surface, not a control.
   DOES NOT DO      Does not scroll, cap its height, or space itself from its
                    siblings — the parent (.container, .workbench-main) owns
                    vertical rhythm. Does not clip its children.
   ACCENT POLICY    Never carries accent. A panel is structure, and structure
                    is not action or state.
   -------------------------------------------------------------------------- */

/* Base panel: the card used everywhere (form, player, stats, log). Carries
   a shadow as of 2026-09-03 (see top-of-file note 7) — was flat/border-only. */
.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 4px; /* deliberately modest — squared-off, not pill-like */
  box-shadow: var(--shadow);
  /* Padding moved here 2026-09-22. It previously lived ONLY on the three
     page-type-specific companions (.form-panel/.stats-panel/.log-panel, all
     20px), so a bare <div class="panel"> — the obvious thing to write, given
     this is documented as "the card used everywhere" — rendered with its
     content flush against the border. Audited before the move: every .panel
     use in this skill and in every locally-cloned consumer pairs it with a
     companion that declares its own padding, and all of those companions sit
     later in this file at equal specificity, so none of them shift. The one
     genuinely flush surface, .player-panel, opts out explicitly below. */
  padding: 20px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .panel--flush
   WHAT IT IS       A .panel whose content reaches the border.
   WHEN TO USE      A video surface, a full-bleed image, a child that draws
                    its own edge. Prefer this over re-zeroing padding in a
                    page's own CSS.
                    NOT needed with .player-panel, which already sets
                    padding: 0 for exactly this reason.
   MARKUP           <div class="panel panel--flush"> ... </div>
                    (UNVERIFIED example — no reviewed page demonstrates this
                    component yet; .player-panel covers the cases so far)
   STATES           None.
   DOES NOT DO      Does not add overflow: hidden — a child that must be
                    clipped to the radius sets that itself.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Opt-out for a panel whose content must reach the border — a video surface,
   a full-bleed image, a child that draws its own edge. Prefer this over
   re-zeroing padding ad hoc in a page's own CSS. (2026-09-22) */
.panel--flush { padding: 0; }

/* --------------------------------------------------------------------------
   CONTRACT — .empty-state
   WHAT IT IS       The "there is nothing here yet" block inside a panel.
   WHEN TO USE      A list, table or panel with no rows. Include one action if
                    there is an obvious next step.
                    NOT for loading — that is .badge.loading plus a one-shot
                    entrance; see the skeleton non-rule. NOT for an error —
                    that is .status-strip--attn.
   MARKUP           <div class="panel">
                      <div class="empty-state">
                        <h3>No streams</h3>
                        <p>Nothing is ingesting on this account yet.</p>
                        <div class="button-row">
                          <button type="button" class="btn btn-primary">Add a stream</button>
                        </div>
                      </div>
                    </div>
                    (UNVERIFIED example — checked in a scratch page during
                    development, but no reviewed page demonstrates this
                    component; tests/gap-probe.html shows the unstyled case
                    only, by design)
   STATES           None.
   DOES NOT DO      No icon or illustration slot, deliberately: decoration
                    that signals neither action nor state is what this theme
                    rejects. Does not centre itself vertically in the panel —
                    it sits where it falls.
   ACCENT POLICY    The block carries none. A single .btn-primary inside it
                    may, as the one action.
   -------------------------------------------------------------------------- */

/* Empty state (2026-09-24). A composition of .panel + heading + text +
   .button-row would work, but two pages would compose it two different ways;
   the part composition will not give you for free is the constrained
   measure. Deliberately NO icon slot: an illustration in an empty state
   signals neither action nor state, and decoration that signals nothing is
   what this theme rejects.

   Body text is --text-primary, not --text-muted, despite the instinct to
   quieten it: --text-muted is reserved for things that are INACTIVE
   (placeholders, disabled state, control handles), and an empty state is not
   inactive — it is waiting. Widening that token a third time would blur what
   it means. Size carries the hierarchy instead.

   Markup: .panel > .empty-state > <h3> + <p> + .button-row */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 40px 20px;
  text-align: center;
}
.empty-state h3 { margin: 0; }
.empty-state p {
  margin: 0;
  max-width: 40ch;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-primary);
}

/* ==========================================================================
   Form panel — label-beside-input layout
   ========================================================================== */

.form-panel {
  padding: 20px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .field
   WHAT IT IS       One labelled form row: label beside control.
   WHEN TO USE      Any input, select or textarea that needs a label.
                    NOT inside a .control-rail — the 200px label column does
                    not fit; that is .control-row. NOT for a checkbox or
                    radio, whose label follows the control: .checkbox-item /
                    .radio-item.
   MARKUP           <div class="field">
                      <label for="fg-stream">Stream name</label>
                      <div class="field-control">
                        <input type="text" id="fg-stream" placeholder="XXXXX-YYYYY">
                        <span class="helper">Stream name or ABR group ID.</span>
                      </div>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None itself. Its CONTROL carries them.
   DOES NOT DO      Does not validate; [aria-invalid] is set by the page.
                    Does not fit a container under ~400px — it stacks
                    automatically inside a workbench's panels, and
                    .field--stacked forces it elsewhere.
   ACCENT POLICY    None at rest. Its control takes accent-blue on hover
                    (select), the focus ring, and accent-orange on
                    [aria-invalid].
   -------------------------------------------------------------------------- */
.field {
  display: flex;
  align-items: flex-start;
  gap: 20px;
  margin-bottom: 16px;
}
.field:last-of-type { margin-bottom: 0; }

.field label {
  flex: 0 0 200px; /* fixed label column so inputs align vertically */
  padding-top: 10px; /* optically aligns label baseline with input text */
  font-size: 14px; /* 2026-09-04: labels 14px, field content (input/select) 16px — deliberately different sizes now, not both 16px */
  font-weight: 500;
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   CONTRACT — .field-control
   WHAT IT IS       The column holding the control and anything under it.
   WHEN TO USE      Always, inside .field. Not used alone.
   MARKUP           see the .field contract above
   STATES           None.
   DOES NOT DO      Does not style the control — the element selectors do.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */
.field-control {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

/* --------------------------------------------------------------------------
   CONTRACT — input[type="text"|"password"|"email"] / textarea / select
   WHAT IT IS       The field-shaped box: a recessed control on the darkest
                    surface with a bordered edge.
   WHEN TO USE      Inside a .field-control. A select outside a form (a
                    toolbar, a button row) gets the same treatment from the
                    bare `select` rule further down.
                    NOT for a value you cannot edit — that is
                    .info-row.wrap .value, which looks like a field but is a
                    readout.
   MARKUP           <div class="field-control">
                      <input type="text" id="fg-stream" placeholder="XXXXX-YYYYY">
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, hover (select only: accent-blue border),
                    focus-visible (blue ring), disabled, and
                    [aria-invalid="true"] (orange border on ALL sides). No
                    active or selected state.
   DOES NOT DO      Does not size itself to its content. A textarea resizes
                    VERTICALLY only — horizontal resize would break the
                    label/control alignment. A select's OPEN dropdown is drawn
                    by the OS and ignores most styling; only the closed
                    control is ours.
   ACCENT POLICY    Neutral at rest. Accent-blue on focus and select-hover;
                    accent-orange on [aria-invalid]. Never fills.
   -------------------------------------------------------------------------- */

/* Plain uniform border on every side, deliberately — no colored
   left-accent "stripe" on any field, ever (confirmed 2026-09-01, direct
   request after a consuming page — captions-workflow — had grown its own
   orange-left-accent form style; removed there too once it was pointed
   out it didn't match this theme). If a page needs to flag an error
   state, use border-color on ALL sides (e.g. var(--accent-orange)), never
   a single accented edge. */
/* email is in this list because sign-in forms need it and a page that
   vendors this file can't add it without hand-copying all seven
   declarations below (live-recorder had exactly that duplicate, 2026-09-07). */
.field input[type="text"],
.field input[type="password"],
.field input[type="email"],
.field textarea,
.field select {
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text-secondary); /* now white like everything else — the box reads as "a box" via --panel-inset's darker background, not text color; see note above :root */
  padding: 10px 12px;
  font-size: 16px;
  width: 100%;
}

/* Vertical-only resize (2026-09-22): a horizontally resizable textarea can
   be dragged wider than the .field-control column it sits in, breaking the
   form's label/control alignment. */
.field textarea {
  resize: vertical;
  min-height: 84px;
  line-height: 1.5;
}

/* A select inside .field needs the same treatment, as its own rule rather
   than joining the grouped selector above -- that group covers text inputs
   and a textarea too, and none of those wants a chevron. It also has to
   restate the background: the grouped rule sets the `background` SHORTHAND,
   which resets background-image, so a chevron declared only on the bare
   `select` rule would be wiped inside a form. (2026-09-23) */
.field select {
  -webkit-appearance: none;
  appearance: none;
  font-family: var(--font-body);
  background: var(--panel-inset) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%239aa1a8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 12px center;
  background-size: 12px 8px;
  padding: 10px 34px 10px 12px;
}

.field select:hover { border-color: var(--accent-blue); }
.field select:disabled { color: var(--text-primary); cursor: not-allowed; opacity: 0.6; } /* see select:disabled */

/* A <select> OUTSIDE .field got nothing until 2026-09-22 — the rule above is
   scoped `.field select`, so a select in a toolbar or a button row rendered
   as the browser's native control: white box, OS-accent chrome, on a dark
   page. Same bug class as the unstyled slider (see the parameter-controls
   section): an unstyled native control is painted by the OS, not by us.
   Element selector, matching how this file already handles body / a /
   button, input, textarea / input[type="range"] / table. `.field select`
   still wins inside a form by specificity, with identical values. */
select {
  /* appearance: none (2026-09-23). The select was still a NATIVE control --
     the only appearance resets in this file were on the range input, its
     thumb and the switch. Measured in Chrome the computed family already
     resolved correctly to Source Sans 3, but a natively-drawn select is where
     the platform gets to substitute its own UI face, which is what a macOS
     review was seeing. Removing the native rendering makes the declared type
     the only possible outcome on every platform.

     font-family is declared EXPLICITLY rather than left to the reset's
     `inherit`: WebKit form controls are unreliable with inherit, and every
     other rule in this file that cares about type states it. */
  -webkit-appearance: none;
  appearance: none;
  font-family: var(--font-body);
  background: var(--panel-inset) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%239aa1a8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 12px center;
  background-size: 12px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text-primary);
  /* 34px right = 12px gutter + 12px chevron + 10px clearance, so a long value
     never runs under the affordance. */
  padding: 10px 34px 10px 12px;
  font-size: 16px;
}
select:hover { border-color: var(--border-strong); }
/* The chevron is a background-image, so it cannot inherit the dimmed text
   colour the way a glyph would — without the opacity it stayed at full
   strength while its own label faded. Same class as the literals in
   BATCH-FINDINGS item 9: an image in url() has no access to the cascade, so
   the dimming has to be applied to the element. (2026-09-24) */
/* Disabled must stay LEGIBLE, not merely compliant (2026-09-24). Disabled
   text is exempt from WCAG contrast minimums, but a select whose value you
   cannot read is useless. opacity: 0.5 was compounding — it dimmed text,
   surface and border together, so --text-muted fell to 2.60:1 against the
   panel. The fix is inverted from the instinct: raise the LABEL to
   --text-primary and let a gentler opacity recede the chrome. Composite
   lands near 6.3:1, readable. */
select:disabled {
  color: var(--text-primary);
  cursor: not-allowed;
  opacity: 0.6;
}

/* The dropdown LIST (2026-09-23). Declared for the platforms that honour
   them, but know the limit before filing a bug: on macOS and Windows the
   OPEN popup is drawn by the operating system, not the page, and it ignores
   most author styling -- background-color and color are commonly dropped and
   the list is often rendered in the system UI font whatever is set here. No
   CSS can change that. The closed control above is fully ours; the open list
   is only partly. */
option,
optgroup {
  font-family: var(--font-body);
  background-color: var(--panel-inset);
  color: var(--text-primary);
}

/* input[type="file"] was likewise absent from the .field list — the same
   omission textarea had until earlier today — so it rendered native-white in
   both panels of the workbench archetype. The BOX takes the field-shaped
   treatment (--panel-inset, 8px) like every other input; the button part
   takes .btn-chip's neutral pill, since that is what it is.

   The two pseudo-elements must be SEPARATE rules: ::file-selector-button is
   the standard, ::-webkit-file-upload-button the legacy WebKit alias, and one
   unrecognised selector invalidates a whole list — same trap as the range
   pseudo-elements. (2026-09-22) */
/* --------------------------------------------------------------------------
   CONTRACT — input[type="file"]
   WHAT IT IS       A file picker: a neutral pill button plus the chosen
                    filename.
   WHEN TO USE      Inside a .field-control, normally with .field--stacked —
                    the filename truncates in a short label column.
                    NOT for a drag-and-drop zone; the theme has none.
   MARKUP           <div class="field field--stacked">
                      <label for="fg-file">Source file</label>
                      <div class="field-control">
                        <input type="file" id="fg-file" accept="video/*">
                      </div>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default and disabled only. The button part has no hover
                    state — the pseudo-element cannot carry one reliably
                    across engines.
   DOES NOT DO      Deliberately has NO field-shaped box of its own: it is a
                    button plus a filename, not something you type into. Does
                    not truncate the filename — a short container will.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* No field-shaped box of its own (corrected 2026-09-22, same day it was
   added). A file input is not a box you type into — it is a BUTTON plus the
   name of the chosen file. Wrapping that in --panel-inset with a border gave
   it the same shape as a text input, which reads as a disabled field and
   boxed the filename into a truncating well. The button part below carries
   the whole affordance; the filename is just text beside it. */
input[type="file"] {
  background: none;
  border: 0;
  padding: 0;
  color: var(--text-primary);
  font-size: 13px;
  width: 100%;
  cursor: pointer;
}
input[type="file"]::file-selector-button {
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-primary);
  padding: 5px 14px;
  margin-right: 10px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}
input[type="file"]::-webkit-file-upload-button {
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-primary);
  padding: 5px 14px;
  margin-right: 10px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--text-muted);
}

/* Stack label above input below 640px so the fixed label column doesn't
   crowd the field on narrow viewports. */
@media (max-width: 640px) {
  .field {
    flex-direction: column;
    gap: 6px;
  }
  .field label {
    flex-basis: auto;
    padding-top: 0;
  }
}

/* Fields stack automatically inside a workbench's panels (2026-09-24). The
   pinch is driven by CONTAINER width, not viewport: inside .workbench the
   rail takes up to 440px, so each .stats-row panel is ~218px at a 960
   viewport and ~353px at 1280 — leaving .field's 200px label column with
   -42px and 93px of control respectively. A viewport media query cannot fix
   that; it never fires in the range where the problem exists.

   A container query is the correct long-term answer and is deliberately NOT
   used here: container-type implies contain: layout, which changes the
   containing block for absolutely-positioned descendants, and four of the
   nine registered consumers are not cloned on any machine that has checked
   this — see BATCH-FINDINGS item 11. :has() is a plain structural selector
   with no containment semantics, and this file already requires it.

   Checked 2026-09-24: no downstream page combines .workbench + .stats-row +
   .field at all. Declarations are identical to .field--stacked, so this
   introduces no appearance the theme does not already have. */
body:has(.workbench) .stats-row .field {
  flex-direction: column;
  align-items: stretch; /* 2026-10-06, same bug and same fix as .field--stacked below — BATCH-FINDINGS 27 */
  gap: 6px;
}
body:has(.workbench) .stats-row .field label {
  flex-basis: auto;
  padding-top: 0;
}

/* --------------------------------------------------------------------------
   CONTRACT — .field--stacked
   WHAT IT IS       A .field with its label above the control.
   WHEN TO USE      A .field in a container under ~400px wide where the
                    automatic workbench rule does not apply — a narrow modal,
                    a half-width panel on a .container page. Also correct for
                    a file input, whose control is wide and whose filename
                    truncates in a short column.
                    NOT needed inside a workbench's .stats-row panels: those
                    stack automatically.
   MARKUP           <div class="field field--stacked">
                      <label for="fg-file">Source file</label>
                      <div class="field-control"><input type="file" id="fg-file"></div>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not change the control's INTRINSIC size — it stretches
                    the control to the field's full width (2026-10-06; before
                    that it left it at ~192px, which was the bug in
                    BATCH-FINDINGS 27). A control that must stay narrow needs
                    its own width.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Explicit stacked variant (2026-09-22). The media query above already
   stacks .field — but it tests the VIEWPORT, and the real constraint is the
   CONTAINER. .field's 200px label column is a desktop form layout; in a
   half-width .stats-row panel (about 413px, less 40px of panel padding) it
   leaves under 150px for the control, which truncates a file input's
   filename and overflows a select — at any viewport width, so the 640px
   breakpoint never fires. This applies the same declarations on demand.

   The general fix is a container query rather than a modifier; see
   BATCH-FINDINGS for why that isn't in this batch. */
/* align-items: stretch (2026-10-06) — see BATCH-FINDINGS item 27. .field's
   base rule sets align-items: flex-start, which governs the CROSS axis;
   turning the row into a column turns that axis HORIZONTAL, so flex-start was
   shrink-wrapping the control to its intrinsic ~192px while .field-control's
   flex: 1 1 0% acted on the vertical axis and did nothing. The modifier
   stacked the label but left the freed width unused, which is the problem it
   exists to solve. Measured 192px → 362px in a 420px card.
   MUST stay identical to the automatic rule above it. */
.field--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}
.field--stacked label {
  flex-basis: auto;
  padding-top: 0;
}

/* --------------------------------------------------------------------------
   CONTRACT — .helper
   WHAT IT IS       A line of explanatory text under a control or a row.
   WHEN TO USE      Explaining one field, always visible. Also the body of an
                    inline explanation toggled by a .btn-icon with
                    aria-expanded — see the tooltip non-rule.
                    NOT for an error: that is .field .error, which replaces it
                    while the error stands. NOT for a mode or status message:
                    that is .status-strip.
   MARKUP           <span class="helper">Stream name or ABR group ID.</span>
                    (verified example, from tests/archetype-workbench.html)

                    toggled form:
                    <p class="switch-item">
                      <input type="checkbox" role="switch" id="sw-gpu" checked>
                      <label for="sw-gpu">GPU acceleration</label>
                      <button type="button" class="btn-icon" aria-expanded="false"
                              aria-controls="h-gpu" aria-label="About GPU acceleration">i</button>
                    </p>
                    <p class="helper" id="h-gpu" hidden>Uses the GPU where available.</p>
                    (UNVERIFIED example — no reviewed page wires this yet)
   STATES           None. Hidden with the [hidden] attribute, owned by the page.
   DOES NOT DO      Does not truncate or clamp; a long helper wraps.
   ACCENT POLICY    Never carries accent — it is explanation, not state.
   -------------------------------------------------------------------------- */

/* Promoted from `.field .helper` 2026-09-24. The helper line is now needed
   outside a form too — under a .switch-item, as the inline explanation an
   `i` affordance toggles — and scoped to .field it rendered unstyled there.
   Same move select and input[type="range"] got this batch. */
.helper {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-primary);
}

/* Error state (2026-09-22). The rule this implements was ALREADY written in
   prose above the field-input block — a field in error takes border-color on
   ALL sides, never a single accented edge — but nothing in this file ever
   implemented it, so every page needing validation invented its own
   treatment. Orange, not red: per note 6 at the top of this file there is no
   red/green/amber anywhere in this theme, and error/warning/live all share
   the single attention hue. The message text carries the specific meaning;
   the color only says "this needs attention".

   Driven from aria-invalid="true" rather than a class, so the visual state
   can't drift from the state assistive tech is told about. */
.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--accent-orange);
}

/* Same idea as .btn-primary:focus-visible — the ring follows the element's
   own accent rather than the default blue. */
.field input[aria-invalid="true"]:focus-visible,
.field select[aria-invalid="true"]:focus-visible,
.field textarea[aria-invalid="true"]:focus-visible {
  outline-color: var(--accent-orange);
}

/* Sits in the same slot as .helper (the .field-control column, under the
   control) and replaces it while the error stands — don't show both. */
/* --------------------------------------------------------------------------
   CONTRACT — .field .error
   WHAT IT IS       The validation message for a field in error.
   WHEN TO USE      With [aria-invalid="true"] on the control. Set both — the
                    attribute drives the border, this carries the message.
                    NOT for a warning about something other than this field:
                    that is .status-strip--attn.
   MARKUP           <div class="field-control">
                      <input type="text" id="ingest" aria-invalid="true"
                             aria-describedby="ingest-err">
                      <span class="error" id="ingest-err">Ingest URL must include a stream name.</span>
                    </div>
                    (verified example, from tests/gap-probe.html — the
                    approved reference does not demonstrate an error state)
   STATES           None. Its presence IS the state.
   DOES NOT DO      Does not hide the .helper — the page removes or replaces
                    it. Showing both reads as contradictory.
   ACCENT POLICY    Orange, always. Orange is the attention bucket; there is
                    no red in this theme. NOT neutralised under
                    data-color-critical — an error must stay visible.
   -------------------------------------------------------------------------- */
.field .error {
  font-size: 12px;
  line-height: 1.5;
  color: var(--accent-orange);
}

/* --------------------------------------------------------------------------
   CONTRACT — .form-result / --attn / --ok
   WHAT IT IS       The outcome of a form SUBMISSION, shown inside the form.
   WHEN TO USE      The attempt as a whole succeeded or failed — wrong
                    credentials, an expired token, a rate limit, "reset link
                    sent". Always combined with exactly one variant, the way
                    .btn takes exactly one tier.
                    NOT for one field failing validation: that is
                    [aria-invalid="true"] plus .field .error. NOT for a
                    persistent mode or connection state: that is
                    .status-strip. NOT for something that happened elsewhere
                    on the page: that is .toast.
   MARKUP           <div class="form-result form-result--attn" role="alert">
                      Email or password is incorrect.
                    </div>
                    (UNVERIFIED example — no reviewed page in this skill
                    submits a form; three downstream consumers each invented
                    this component independently)
   STATES           Presence is the state. The page shows it and clears it on
                    the next attempt.
   DOES NOT DO      Spaces ITSELF — 14px above and below, reset at the first
                    and last position the way .notice-item and .button-row do,
                    so a container's padding is not doubled. Do not add margin
                    to it from outside.
                    Does not dismiss itself and carries NO dismiss control — a
                    result is replaced by the next attempt, not closed. For a
                    dismissible line that is .notice-item.
                    role="alert" is REQUIRED, not optional. Two of the three
                    consumer implementations have no live region at all, so a
                    screen reader gets silence on a failed sign-in.
   ACCENT POLICY    --attn orange, --ok blue, per note 6. The base class
                    carries no colour and must NEVER be used alone.
   -------------------------------------------------------------------------- */

/* Form-submission result (2026-10-06). The fourth message component, and the
   one every consumer had to invent, because the other three are each about
   something else: .field .error is one field, .status-strip is a standing
   state, .toast is elsewhere on the page. This is "the thing you just pressed
   did not work, and here is why", and it belongs inside the form it failed.

   NEUTRAL FILL, ACCENT BORDER — no alpha. All three consumers used a tinted
   rgba() background and all three got the tint wrong in some way (see
   BATCH-FINDINGS item 23). This file has no alpha accent token and should not
   grow one for a message box: --panel-soft with a 1px accent border is the
   same device .btn-chip[aria-pressed] uses, and it is the smallest saturated
   area that still reads.

   LEFT-ALIGNED, which all three consumers are not — they centre it. A
   one-line "incorrect password" centres fine; a two-line rate-limit
   explanation does not, and this is the component that carries the long
   rejections. captions-workflow's own code comments that a generic message
   "is actively misleading for a rate limit, a suspended account, or any other
   rejection". .status-strip and .helper are left-aligned and this reads
   beside them.

   Text stays --text-primary rather than taking the accent the way .field
   .error does. That rule has no box, so colour is its only channel; this one
   has a border to carry the hue, and a full sentence in orange is harder to
   read than the same sentence in white. */
.form-result {
  padding: 10px 12px;
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 4px; /* structural, per the radius scale */
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-primary);
  /* SYMMETRIC, with first/last resets (2026-10-07). Shipped 3.7.0 as
     margin-top: 14px alone, which was invisible for two days because every
     usage had it LAST in a panel with a caption under it. Putting it at the
     top of the sign-in gate's form made it obvious: measured 0.0px between
     the error box and the "Email" label it sat on top of.
     The moodboard does not move as a result — its captions carry their own
     top margin, so the new bottom margin collapses into theirs. Measured
     14.0 → 14.0 on two of three, and 11.5 → 14.0 on the third, which only
     makes it match the other two.
     Same shape as .notice-item, .button-row and .field: a component spaces
     itself, and the edges reset so a container's padding is not doubled. */
  margin: 14px 0;
}
/* :first-child fires on the moodboard, where it pulls the first result flush
   to the panel's padding (measured 14.0px → 0.0px). It does nothing in the
   gate, where the top margin was already collapsing through the form to
   .modal-panel__sub's 20px. */
.form-result:first-child { margin-top: 0; }
/* DORMANT TODAY, deliberately kept: no current usage has .form-result last —
   all four are followed by a caption or by the method blocks. It is here
   because the moment someone ends a panel with a result, its absence is a
   visible bug. Note the difference from the .color-swatch rule this file
   deleted for doing nothing: that one set a value the default already had and
   could NEVER fire; this one simply has not yet. Do not reason from it as if
   it were proven in a render. */
.form-result:last-child { margin-bottom: 0; }
.form-result--attn { border-color: var(--accent-orange); }
.form-result--ok   { border-color: var(--accent-blue); }

/* A helper toggled by an `i` affordance sits under its row. 6px from the
   spacing scale — tight, because it belongs to the row above it. Hide it
   with the `hidden` attribute; .helper declares no display, so the UA rule
   applies and no CSS is needed for the toggle. (2026-09-24) */
.switch-item + .helper { margin-top: 6px; }

/* --------------------------------------------------------------------------
   CONTRACT — .checkbox-row
   WHAT IT IS       A bordered band holding a group of checkbox or radio items.
   WHEN TO USE      A group of related toggles inside a form panel.
                    NOT for a single checkbox — use .checkbox-item alone. NOT
                    for switches — those are .switch-item rows.
   MARKUP           <div class="checkbox-row">
                      <div class="checkbox-item">
                        <input type="checkbox" id="c1"><label for="c1">Fast start</label>
                      </div>
                    </div>
                    (verified example, from tests/gap-probe.html)
   STATES           None.
   DOES NOT DO      Does not lay out its items — each is its own
                    .checkbox-item. Its top and bottom borders make it a
                    section divider, so do not nest one inside another.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */
.checkbox-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 20px 0;
  padding: 16px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

/* --------------------------------------------------------------------------
   CONTRACT — .checkbox-item + input[type="checkbox"]
   WHAT IT IS       A checkbox with its label.
   WHEN TO USE      A non-exclusive choice, or a form value submitted later.
                    NOT for an on/off setting that takes effect immediately —
                    that is [role="switch"]. NOT for an exclusive choice —
                    that is .radio-item.
   MARKUP           <div class="checkbox-item">
                      <input type="checkbox" id="c1"><label for="c1">Fast start</label>
                    </div>
                    (verified example, from tests/gap-probe.html)
   STATES           default, checked, disabled. No hover and no selected state
                    distinct from checked.
   DOES NOT DO      Does not wrap the label around the control — use `for`.
                    The 17px box is under the 24x24 hit-area minimum on its
                    own; the associated label extends the target, which is why
                    `for` is not optional.
   ACCENT POLICY    Neutral unchecked. Accent-blue fill with a white tick when
                    checked. Goes fully neutral under data-color-critical —
                    the tick's presence carries the state.
   -------------------------------------------------------------------------- */
.checkbox-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Checkbox (2026-09-24). Same fix the radio got, for the same reason: this
   was a NATIVE control tinted with accent-color, which styles the CHECKED
   state and leaves the unchecked one drawn by the OS — a light box on a dark
   panel. It went unnoticed because every checkbox in product-sample.html
   happens to be checked.

   :not([role="switch"]) rather than relying on source order. The switch rule
   further down is also an input[type="checkbox"], and would win today on
   specificity and order — but protecting one control's geometry with cascade
   order is a trap for whoever reorders this file, the same reason
   .switch-item does not reuse .checkbox-item. Excluding it explicitly means
   the two rules never overlap at all.

   17px matches the radio exactly so mixed groups line up; 4px radius is the
   structural step from the radius scale (a checkbox is a small box, not a
   pill). Neutral at rest, accent only when checked.

   Sizing and colour moved here from .checkbox-item input[type="checkbox"] so
   that EVERY checkbox is covered, not only those inside .checkbox-item —
   keeping a scoped duplicate is how the .field select font-family omission
   survived. accent-color is gone with the native rendering it tinted. */
input[type="checkbox"]:not([role="switch"]) {
  -webkit-appearance: none;
  appearance: none;
  width: 17px;
  height: 17px;
  flex-shrink: 0;
  margin: 0;
  border-radius: 4px;
  background: var(--panel-inset);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

/* The tick is an SVG centred with background-position, so it is centred by
   construction at any box size — no offsets to re-derive, per note 10.
   The stroke is a literal #ffffff because a data-URI SVG cannot read a custom
   property; it duplicates --text-on-accent and must be updated by hand if
   that token ever changes. Same limitation as the select chevron — both are
   logged in BATCH-FINDINGS item 9. */
input[type="checkbox"]:not([role="switch"]):checked {
  background: var(--accent-blue) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.5l2.5 2.5 4.5-5' fill='none' stroke='%23ffffff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
  background-size: 11px 11px;
  border-color: var(--accent-blue);
}
input[type="checkbox"]:not([role="switch"]):disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  input[type="checkbox"]:not([role="switch"]) { transition: none; }
}

.checkbox-item label {
  font-size: 13px;
  color: var(--text-primary);
}

/* ==========================================================================
   NON-RULE: there are no tooltips in this theme (2026-09-24, deliberate)
   ==========================================================================

   [role="tooltip"] has no rule ON PURPOSE. tests/gap-probe.html section 7
   renders one as permanently-visible inline text — that is the probe
   demonstrating the omission, not a bug to fix.

   WHAT YOU WANT                          WHAT TO USE
   --------------------------------------------------------------------------
   Explain one field, always visible      .helper, under the control
   Explain on demand, inline              .btn-icon with aria-expanded +
                                          a .helper toggled by [hidden]
   Reference material that would crowd    .detail-rail (wide screens; must
   the page                               degrade to one of the above)
   Explain a mode or a state              .status-strip
   A plain hover hint, no styling         the native title attribute

   The title attribute is UNTHEMEABLE BY DESIGN, not by oversight: the OS
   draws it. That is the trade — an unstyled OS tooltip beats a themed one
   that clips.

   WHY NOT: six components here declare overflow: hidden (.player-panel,
   .monitor, .journey, .snippet, and two .mb-* classes), so an
   absolutely-positioned tooltip is clipped inside the densest parts of a
   workbench. This file has no positioning machinery — no popover, no anchor
   positioning — so a correct one needs collision detection that does not
   exist here. Hover content is also unreachable on touch and carries real
   WCAG 1.4.13 obligations (dismissible, hoverable, persistent), and the
   AV-workbench copy rules already call for labels that explain themselves.
   A tooltip is usually a symptom that the label failed. */

/* Radio (2026-09-24, gap #6 from the original probe ranking). There was no
   input[type="radio"] rule at all, so a radio was painted in the VISITOR'S
   OS accent colour — the same hole that produced pink sliders before the
   parameter-controls section landed.

   This goes further than .checkbox-item above, which tints a NATIVE control
   with accent-color. That works for a checked box, but it leaves the
   UNCHECKED state drawn by the OS — a light circle on a dark panel. Every
   other control in this batch (select, range, switch) had to drop native
   rendering for exactly that reason, so the radio does too. "Neutral at
   rest" has to mean neutral on every OS, not just a dark one.
   NOTE: .checkbox-item still carries this latent issue — its unchecked box
   is OS-painted. Every checkbox in product-sample.html happens to be
   checked, which is why no screenshot has caught it. Worth its own pass.

   Element selector, not scoped to .radio-item: a radio outside the row
   component should not fall back to an OS-painted control either — matching
   how `select` and `input[type="range"]` are handled in this file.

   Neutral at rest, accent only when selected, per the approved reference.
   17px matches .checkbox-item exactly so a mixed group lines up. */
input[type="radio"] {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  width: 17px;
  height: 17px;
  flex-shrink: 0;
  margin: 0;
  border-radius: 50%;
  background: var(--panel-inset);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  transition: border-color 0.15s ease;
}
input[type="radio"]:checked { border-color: var(--accent-blue); }
/* The dot is inset from the PADDING box (17px less the 1px borders = 15px),
   so inset: 4px leaves a 7px dot centred by construction — no offsets. */
input[type="radio"]:checked::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--accent-blue);
}
input[type="radio"]:disabled { opacity: 0.6; cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
  input[type="radio"] { transition: none; }
}

/* Row component, mirroring .checkbox-item exactly — flex, centred per
   note 10, 8px gap from the spacing scale. */
/* --------------------------------------------------------------------------
   CONTRACT — .radio-item + input[type="radio"]
   WHAT IT IS       A radio with its label.
   WHEN TO USE      Exactly one of a small set, all options visible at once.
                    NOT for more than about five options — that is a select.
                    NOT for a non-exclusive choice — that is .checkbox-item.
   MARKUP           <fieldset>
                      <legend>Preview engine</legend>
                      <div class="radio-item">
                        <input type="radio" id="eng-js" name="engine" checked>
                        <label for="eng-js">JS canvas</label>
                      </div>
                    </fieldset>
                    (verified example, from tests/gap-probe.html)
   STATES           default, checked, disabled. No hover state.
   DOES NOT DO      Does not group or name itself — the shared `name` and the
                    <fieldset>/<legend> belong to the page. Same 24x24
                    hit-area note as .checkbox-item: the label extends it.
   ACCENT POLICY    Neutral unselected. Accent-blue ring and dot when
                    selected. Goes fully neutral under data-color-critical.
   -------------------------------------------------------------------------- */
.radio-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.radio-item label {
  font-size: 13px;
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   CONTRACT — fieldset / legend
   WHAT IT IS       Semantic grouping for a set of related controls, reset to
                    no visual chrome.
   WHEN TO USE      Wrap a radio group so the legend names it for assistive
                    tech. NOT as a visual container — that is .panel or
                    .control-section.
   MARKUP           see the .radio-item contract above
                    (verified example, from tests/gap-probe.html)
   STATES           None.
   DOES NOT DO      Draws nothing — no border, no background, no padding. If
                    a group needs to look like a group, put it in a panel.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* fieldset / legend (2026-09-24). Neither had a rule, so a grouped set of
   radios drew the browser's pale inset border with a notched legend cut into
   it — the most obviously off-theme thing left in the form section. Reset to
   nothing: grouping is semantic here, and the visual grouping is already done
   by .field, .checkbox-row or a .control-section.

   min-width: 0 is load-bearing, not tidiness: the UA stylesheet gives
   fieldset `min-inline-size: min-content`, which makes it refuse to shrink
   below its widest child and quietly overflow any flex or grid column it
   sits in — including .field-control. */
fieldset {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

/* The legend labels the group, so it takes the same treatment as a field
   label at the rail's 13px size. */
legend {
  padding: 0;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .button-row
   WHAT IT IS       A horizontal row of controls, with its own vertical
                    rhythm as a panel child.
   WHEN TO USE      Any group of buttons, and any row mixing buttons with a
                    select or a line of metadata.
                    NOT a separate thing from a toolbar of selectable tokens —
                    a .btn-chip row is still a .button-row container.
   MARKUP           <div class="button-row">
                      <button type="button" class="btn btn-primary">Start ingest</button>
                      <button type="button" class="btn btn-secondary">Play</button>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not stretch its children — align-items: center is
                    deliberate, so a text span beside a button sits on the
                    same line rather than being stretched with its text at the
                    top. Does not right-align or space-between.
   ACCENT POLICY    Never carries accent itself.
   -------------------------------------------------------------------------- */
.button-row {
  display: flex;
  flex-wrap: wrap;
  /* align-items: center (2026-09-24). Without it a flex row defaults to
     `stretch`, so a non-button child -- a metadata span, a select, a helper
     line sitting beside the buttons -- is stretched to the row's full height
     and its single line of text renders at the TOP of that stretched box.
     The BOXES were already centred (all 40px, spread 0.00px), which is why
     this reads as fine until you measure the text: the span's text centre
     sat 8.59px above the buttons' at 1338.19 against 1346.78.

     Every other flex row in this file already declares align-items
     deliberately -- .log-actions, .status-bar, .status-left, .checkbox-item,
     .switch-item, .header-left and .footer-inner all centre; .info-row uses
     baseline and .toast flex-start on purpose. .button-row was the only one
     that declared none. */
  align-items: center;
  gap: 10px;
  /* Even space above and below (2026-09-23). .field carries margin-bottom:
     16px and nothing carried margin-top, so a button row followed by a field
     got 16px above and 0 below — "Source file" sat hard against the buttons
     over it. 16px is the .control-section step from the spacing scale, and it
     collapses against .field's own 16px rather than stacking to 32. The
     first/last resets keep a row flush with the panel's own padding when it
     opens or closes a panel. */
  margin: 16px 0;
}
.button-row:first-child { margin-top: 0; }
.button-row:last-child { margin-bottom: 0; }

/* --------------------------------------------------------------------------
   CONTRACT — .btn + .btn-primary / .btn-secondary / .btn-neutral
   WHAT IT IS       The standard action button. Always combined with exactly
                    one tier.
   WHEN TO USE      .btn-primary   the ONE committing action in a view —
                                   apply, load, save, start. Orange. At most
                                   one per panel.
                    .btn-secondary the positive primary-path action that is
                                   not the commit — play, resume, connect.
                                   Blue.
                    .btn-neutral   plain utility — stop, clear, export, copy,
                                   revert. Bordered, no fill.
                    NOT for a toolbar-scale control: that is .btn-chip. Since
                    2026-10-05 the two are nearly the same HEIGHT — 28.8px
                    against 28.4px — so size no longer tells you which to
                    reach for. Type does: 14px Lexend Deca 700 performing an
                    action, against 12px 500 holding a selection. NOT for an
                    icon-only control: that is .btn-icon, used ALONE, never
                    with .btn.
   MARKUP           <button type="button" class="btn btn-neutral">Export settings</button>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, hover (tier-specific), focus-visible (blue ring;
                    orange on .btn-primary), active (1px translateY), disabled
                    (opacity 0.6). No selected state — a button that holds a
                    selection is .btn-chip.
   DOES NOT DO      Does not size itself to a container. Does not carry an
                    icon slot. Its label DOES wrap if it is longer than the
                    container — unlike .btn-chip and .theme-pill, which set
                    white-space: nowrap. That difference is deliberate: a chip
                    holds a short token, .btn can hold a sentence, and for
                    .btn wrapping is safer than overflowing. See
                    BATCH-FINDINGS item 15. Keep labels short anyway, which
                    the workbench copy rules ask for.
   ACCENT POLICY    .btn-primary and .btn-secondary are accent-filled: they
                    ARE the action, which is what accent is for. .btn-neutral
                    carries none. UNCHANGED under data-color-critical, in
                    geometry as well as in colour: that scope removes accent,
                    and a smaller orange button is still orange. The
                    2026-10-05 tightening is global and argued from density,
                    so the scope has nothing left to say about this control.
   -------------------------------------------------------------------------- */
.btn {
  font-family: var(--font-heading); /* Lexend Deca */
  border: none;
  border-radius: 999px; /* pill */
  /* Label centring is explicit (2026-09-23): a bare <button> centres its text
     by default, but with only 6px of vertical padding and Lexend Deca's tall
     ascenders the glyphs sat optically high. inline-flex + a stated
     line-height makes both axes deterministic rather than font-metric
     dependent. letter-spacing: 0.01em opens Lexend Deca 700 very slightly —
     at 14px bold in a tight pill it was setting too close to read cleanly.
     The face itself is correct and unchanged: Lexend Deca 700 is the product
     tier's mandated button font (SKILL.md, Two-Tier table). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  letter-spacing: 0.01em;
  /* 6px/15px, globally (2026-10-05). This value has moved four times and the
     arc matters more than the number:
       2026-09-22  cut 10px/22px → 6px/15px, justified by the fill being the
                   loudest saturated surface beside a preview.
       2026-09-25  reverted to 10px/22px globally, the cut moved into
                   [data-color-critical]. The scoping discipline's first
                   application: a colour-judgement-only justification cannot
                   hold a rule in the base.
       2026-10-05  the scoped copy deleted. Every other rule in that scope
                   removes accent; this one only shrank an accent that stayed
                   accent, so it did not belong there either.
       2026-10-05  cut again, globally, on a DIFFERENT argument — DENSITY.
                   10px/22px is a marketing-page button sitting in a tool UI:
                   a panel holding a button row, a field and a status line
                   spends more vertical space on its buttons than on its data.
                   Nothing in that sentence mentions a preview.
     THE REVIEW TEST, applied explicitly: would smaller buttons still be right
     on a log viewer? YES. A log viewer wants compact chrome and more visible
     rows, and there is no colour judgement anywhere in the argument — which
     is exactly why this belongs in the base rules and not in the scope. The
     2026-09-22 cut failed this same test. The test was not wrong then and is
     not being worked around now: a value can be wrong for one reason and
     right for another, and only the reason is ever under review.
     6px is the layout scale (the control range has no 6); 15px is the control
     range's top step. 28.8px rendered, clear of WCAG 2.5.8's 24px minimum. */
  padding: 6px 15px;
  font-size: 14px;
  font-weight: 700; /* Bold */
  cursor: pointer;
  transition: background-color 0.15s ease, transform 0.05s ease;
}
.btn:active { transform: translateY(1px); }
.btn:disabled {
  opacity: 0.6; /* 0.5 compounded too far on a dark surface — see select:disabled (2026-09-24) */
  cursor: not-allowed;
  transform: none;
}

/* Use for the single primary/committing action per view (load, save, copy) */
.btn-primary {
  background: var(--accent-orange);
  color: var(--text-on-accent);
}
.btn-primary:hover:not(:disabled) { background: var(--accent-orange-hover); }

/* Use for the positive / primary-path action that isn't the commit itself
   (resume, play, connect). NOTE (2026-09-22): plain utility actions — stop,
   clear, mute, export, copy — moved to .btn-neutral below; this rule's
   original "utility" wording predates that third tier and was why a rail of
   six utility buttons all came out blue. */
.btn-secondary {
  background: var(--accent-blue);
  color: var(--text-on-accent);
}
.btn-secondary:hover:not(:disabled) { background: var(--accent-blue-hover); }

/* Neutral utility button (2026-09-22). Until now the only non-primary button
   was .btn-secondary, which is accent-blue FILLED — so a row of utility
   actions (start / pause / stop, export, copy link) rendered as a wall of
   blue and accent stopped signalling anything. This is the third tier: a
   real action that is neither committing (.btn-primary, orange) nor the
   positive primary path (.btn-secondary, blue).

   Surface language taken from .color-swatch / .btn-icon rather than
   invented: --panel-soft fill, 1px var(--border), and .btn's own 999px pill.
   Padding is 5px/14px against .btn's 6px/15px so the 1px border doesn't
   make a neutral button 2px taller than the filled ones beside it.

   dark-neutral-studio reaches the same conclusion from the other side:
   "use dark grey secondary buttons for reset, stop, hide, utility" and
   "avoid making every button orange." */
.btn-neutral {
  background: var(--panel-soft);
  border: 1px solid var(--border);
  color: var(--text-primary);
  /* 1px less than .btn on each side, offsetting this tier's border so it
     matches the filled buttons' height. It is a COMPENSATION for .btn's
     value, never an independent choice, so it moves in step every time:
     restored alongside .btn 2026-09-25, tightened alongside it 2026-10-05.
     If you change one of these two, you are changing both. */
  padding: 5px 14px;
}
.btn-neutral:hover:not(:disabled) {
  background: var(--panel);
  border-color: var(--border-strong);
}

/* --------------------------------------------------------------------------
   CONTRACT — .btn-link
   WHAT IT IS       A button that looks like a link.
   WHEN TO USE      A real action, secondary to the primary button, that reads
                    as navigation — "Forgot password?", "Back to sign-in",
                    "Use an API key instead".
                    NOT for navigation: that is <a>. NOT for a toolbar-scale
                    control: that is .btn-chip. NOT for the second action in a
                    button row: that is .btn-neutral, which looks like a
                    button because it sits beside one.
   MARKUP           <button type="button" class="btn-link">Forgot password?</button>
                    (UNVERIFIED example — no reviewed page uses it; two
                    consumers hand-rolled it as .login-link and
                    .login-alt__toggle)
   STATES           default, hover (underline), focus-visible (blue ring),
                    disabled. No active and no selected state.
   DOES NOT DO      Does not submit. Keep type="button": a bare <button> inside
                    a <form> submits it, which is exactly the bug a "Forgot
                    password?" control would otherwise cause.
   ACCENT POLICY    Accent-blue label. It is an action, and blue is this
                    file's positive-path hue.
   -------------------------------------------------------------------------- */

/* Link-styled button (2026-10-06). Two consumers wrote this twice each —
   .login-link and .login-alt__toggle in captions-workflow, .login-toggle in
   live-recorder — because a form needs actions that are real buttons but must
   not look like one: a third pill beside "Sign in" and "Cancel" reads as a
   third equal choice, which "Forgot password?" is not.

   Source Sans 3 400, not the 600 both consumers used: the typography contract
   lists exactly one weight for the body face, and a link is text. Blue and the
   hover underline carry it instead of weight. */
.btn-link {
  display: inline;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--accent-blue);
  text-decoration: none;
  cursor: pointer;
}
.btn-link:hover:not(:disabled) {
  color: var(--accent-blue-hover);
  text-decoration: underline;
}
.btn-link:disabled { opacity: 0.6; cursor: not-allowed; }

/* --------------------------------------------------------------------------
   CONTRACT — .btn--block
   WHAT IT IS       A width modifier on .btn: fills its container.
   WHEN TO USE      The single submitting action of a narrow form — a sign-in
                    card, a one-field modal. Combined with .btn AND a tier.
                    Put it in a .button-row like any other button: that is
                    what carries the 16px above and below it, and .field's own
                    margin cannot — .field:last-of-type zeroes its
                    margin-bottom, so the LAST field in a card gives whatever
                    follows it no separation at all. See BATCH-FINDINGS 26.
                    NOT for several buttons in one row: a row of full-width
                    buttons stacks into a wall. NOT to make two buttons match
                    — that is the row's own layout's job.
   MARKUP           <button type="submit" class="btn btn-primary btn--block">Sign in</button>
                    (UNVERIFIED example — both consumers use an inline
                    style="width:100%", which this replaces)
   STATES           Inherits every .btn state unchanged.
   DOES NOT DO      Does not change padding, height or font. Width only.
   ACCENT POLICY    None of its own — the tier decides.
   -------------------------------------------------------------------------- */

/* Width modifier (2026-10-06). Both consumers reached for an inline
   style="width:100%" on their sign-in button, which is the signal that a
   modifier was missing rather than that a one-off was wanted. One declaration,
   so the inline style has somewhere to go. */
.btn--block { width: 100%; }

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

/* ==========================================================================
   Parameter controls — sliders, switches, swatches, icon buttons
   ==========================================================================

   Added 2026-09-22. None of these had any rule in this file, so a tuner
   page's most-repeated controls were also its least designed — and an
   unstyled native control is painted in the VISITOR'S OS accent colour, not
   this theme's. Observed in the wild 2026-09-22 as pink sliders on a macOS
   machine whose system accent was pink; the same mechanism the focus-ring
   reset near the top of this file was added for ("green outline on a blue
   button"). Styling these is what stops the OS leaking into the page.

   Accent rule for this whole section: colour signals ACTION or STATE, never
   decoration or identity. A control at rest is neither, so it is fully
   neutral; accent appears only on focus, while dragging, or to mark a
   selection — always as the smallest area that still reads.

   Why this goes beyond taste: on a chroma-key, despill or colour-balance
   page the operator judges hue, spill and alpha edges in the video itself,
   and saturated chrome beside the preview shifts that judgement —
   simultaneous contrast is a real perceptual effect, and it is why colour
   tools sit in neutral surrounds.

   Blue rather than orange for focus/drag: the global :focus-visible ring is
   already blue, and orange is this file's attention bucket (note 6), which
   a control you are deliberately operating is not.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — input[type="range"]
   WHAT IT IS       A slider: a 6px neutral track with an outlined ring thumb.
   WHEN TO USE      A continuous numeric parameter, normally in a .control-row
                    with a .badge--value showing the figure.
                    NOT for a discrete choice of a few values — that is
                    .radio-item or a select.
   MARKUP           <div class="control-row">
                      <label for="p-sim">Similarity</label>
                      <input type="range" id="p-sim" min="0" max="100" value="42">
                      <span class="badge badge--value">42</span>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, focus-visible (accent-blue thumb border), active
                    (blue border, slightly lifted fill), disabled. No hover.
   DOES NOT DO      Does not show a filled portion on either side of the thumb —
                    only Firefox can do that, and an engine-dependent slider is
                    worse on a colour tool than a plain one. Does not display
                    its own value; pair it with .badge--value. The control box
                    is 24px tall for the hit area while the visible track stays
                    6px.
   ACCENT POLICY    Fully neutral at rest. Accent-blue appears ONLY while
                    focused or being dragged — transient, one control at a
                    time. NOT neutralised under data-color-critical: that
                    feedback is what tells you the tool is responding.
   -------------------------------------------------------------------------- */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  /* Governing rule 9 (2026-09-24). The track is drawn by the pseudo-element,
     not this box, so the input's own height is free: 24px gives a real drag
     target without thickening the visible 6px track by a pixel. Measured
     before this: 402 x 6 — a 6px-tall drag target, the worst hit area in the
     file. A transparent wrapper would have needed markup on every page; a
     transparent border would have fought the thumb's margin arithmetic. */
  height: 24px;
  background: transparent; /* the track pseudo-element draws the bar */
  cursor: pointer;
}

/* WebKit and Moz pseudo-elements must be written as SEPARATE rules — one
   unrecognised selector invalidates an entire selector list, so grouping
   them silently drops the rule in both engines.

   Deliberately NO filled/progress portion on either side of the thumb:
   Firefox can do it (::-moz-range-progress), WebKit cannot without script,
   so implementing it would make the same tuner render differently in Chrome
   and Firefox. On a colour-judgement tool that inconsistency is worse than
   a plainer control — do not "fix" this by adding the Firefox half alone.
   (2026-09-22) */
input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 999px;
}
input[type="range"]::-moz-range-track {
  height: 6px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 999px;
}

/* Light stroke, dark fill (2026-09-23). History, because this rule has now
   been through three passes: it began --panel-soft on --panel (one tone
   apart — the handle was unfindable), was changed 2026-09-22 to a solid
   --text-muted fill to make it visible, and that solid light disc then read
   as too prominent beside a video preview. An OUTLINED thumb resolves both:
   a --panel-inset fill recedes like the track it sits in, while a light
   --text-muted ring keeps it locatable. No box-shadow — it added exactly
   the lift being removed here.

   --text-muted on a border, rather than a new --border-light: the token's
   documented scope was widened on 2026-09-22 to cover neutral control
   handles, and this is the same handle, just drawn as a ring instead of a
   disc. That is NOT the .theme-pill situation, where --text-on-accent would
   have described neither text nor an accent. A --border-light holding
   #9aa1a8 would be a second name for a value that already has one, which is
   how remaps go wrong.

   THUMB OFFSET — derived from measurement, not theory (2026-09-23):
     Measured at 3x device scale, the track renders 6.00px tall in total
     (#2e343d border, #0d171e fill, #2e343d border), so `height: 6px`
     INCLUDES its 1px borders. An earlier comment here derived the offset as
     -((16 - (6 + 2)) / 2), treating 6px as a content height with borders
     added — content-box arithmetic in a border-box file. It was wrong.
     Measured with that -4px value: track centre 748.00, thumb centre 750.00
     — the thumb sat 2.00px BELOW the track's centre line.
     The same measurement shows WebKit positions the thumb against the
     track's CONTENT box (inside the 1px top border), not its border box:
     thumbTop = trackTop + margin-top + 1.
     So, to centre: margin-top = (trackHeight / 2) - (thumbHeight / 2) - 1.
     For a 13px thumb on a 6px track: 3 - 6.5 - 1 = -4.5px.
     Re-derive from that formula if either size changes.
   Firefox centres its own thumb and must NOT get the offset. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: var(--panel-inset);
  border: 1px solid var(--text-muted);
  transition: background 0.12s ease, border-color 0.12s ease;
}
input[type="range"]::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--panel-inset);
  border: 1px solid var(--text-muted);
  transition: background 0.12s ease, border-color 0.12s ease;
}

input[type="range"]:focus-visible::-webkit-slider-thumb { border-color: var(--accent-blue); }
input[type="range"]:focus-visible::-moz-range-thumb { border-color: var(--accent-blue); }
/* Drag state is carried by the border, not by lightening the fill — a light
   fill on drag would reintroduce the solid disc this treatment removes. The
   fill lifts only to --panel-soft, which stays darker than the stroke.
   (2026-09-23) */
input[type="range"]:active::-webkit-slider-thumb {
  background: var(--panel-soft);
  border-color: var(--accent-blue);
}
input[type="range"]:active::-moz-range-thumb {
  background: var(--panel-soft);
  border-color: var(--accent-blue);
}

/* Disabled (2026-09-24). This was the only control in the file with no
   disabled treatment at all, so a locked parameter was pixel-identical to a
   live one — functional, not cosmetic, on a tuner page where a mode lock
   freezes parameters. Dims the whole control rather than the thumb alone, so
   the track reads as inactive too. */
input[type="range"]:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  input[type="range"]::-webkit-slider-thumb { transition: none; }
  input[type="range"]::-moz-range-thumb { transition: none; }
}

/* Switch. Hooked on [role="switch"] rather than a class, so the visual
   state can't drift from the state assistive tech is told about — same
   reasoning as [aria-invalid="true"] in the form section. The theme's plain
   checkbox (.checkbox-item) keeps its own blue accent-color: a switch is a
   different control, not a restyled checkbox.

   NEUTRAL IN BOTH STATES (2026-09-22, decided against an earlier draft that
   tinted the on-state blue). The argument for blue was that a toggle's
   on-state IS state, so accent is earned rather than decorative — true of
   one switch, false of a rail carrying five at once, where five blue tracks
   read as decoration by volume whatever each one means individually.

   State is carried by THREE things, only one of which was ever colour —
   thumb position, thumb luminance (--text-muted → --text-primary), and the
   track/border lift. That redundancy is exactly what makes dropping the
   tint cost nothing: no information lives in the colour. */
input[type="checkbox"][role="switch"] {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  width: 34px;
  height: 20px;
  flex-shrink: 0;
  margin: 0;
  border-radius: 999px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
/* Travel = track content box (34 - 2px border) - thumb (14) - inset (2 * 2)
   = 14px. Re-derive together with the sizes above. */
input[type="checkbox"][role="switch"]::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-muted);
  transition: transform 0.15s ease, background 0.15s ease;
}
input[type="checkbox"][role="switch"]:checked {
  background: var(--panel-soft);
  border-color: var(--border-strong);
}
input[type="checkbox"][role="switch"]:checked::after {
  transform: translateX(14px);
  background: var(--text-primary);
}
input[type="checkbox"][role="switch"]:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  input[type="checkbox"][role="switch"],
  input[type="checkbox"][role="switch"]::after { transition: none; }
}

/* Switch row (2026-09-23). Nothing in this file owned "switch + label +
   info affordance", so a page had to hold them in an ad-hoc container — the
   archetype used a bare <p>, which is display: block, so the three boxes
   aligned on the TEXT BASELINE instead of a common centre. Measured before
   this rule: switch centreY 160.53, label 165.53, .btn-icon 166.53 — a 6px
   spread, with the switch reading high.

   Structure follows .checkbox-item (display: flex + align-items: center +
   gap) rather than inventing a pattern; the gap and the row-to-row margin
   are both steps on the documented spacing scale. Deliberately NOT
   .checkbox-item itself: that rule sizes input[type="checkbox"] to 17x17,
   and while the switch rule out-orders it today, relying on source order to
   protect a control's dimensions is a trap for whoever reorders this file.

   The explicit line-height on the label matters as much as align-items:
   centring aligns BOXES, and a label left on `normal` gets whatever line box
   the font's metrics produce, so the text can still read low inside a
   correctly centred box.

   No compensating offsets anywhere: the switch, the label and the icon all
   keep their own geometry and the flex container does the aligning. */
/* --------------------------------------------------------------------------
   CONTRACT — .switch-item + input[type="checkbox"][role="switch"]
   WHAT IT IS       An on/off control that takes effect immediately, with its
                    label and an optional affordance.
   WHEN TO USE      A setting that applies the moment it is toggled.
                    NOT for a form value submitted later — that is a
                    checkbox. The distinction is when the change takes effect,
                    not how it looks.
   MARKUP           <p class="switch-item">
                      <input type="checkbox" role="switch" id="sw-gpu" checked>
                      <label for="sw-gpu">GPU acceleration</label>
                      <button type="button" class="btn-icon"
                              aria-label="About GPU acceleration">i</button>
                    </p>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, checked, disabled. No hover state.
   DOES NOT DO      Does not submit anything. role="switch" is REQUIRED —
                    without it the checkbox rule applies and you get a box
                    instead of a track. The two rules exclude each other
                    explicitly rather than relying on source order.
   ACCENT POLICY    NONE, in either state — the only control here that is
                    neutral when on. Thumb position and thumb luminance carry
                    the state. Five accented tracks in one rail would be
                    decoration by volume. Nothing to neutralise under
                    data-color-critical.
   -------------------------------------------------------------------------- */
.switch-item {
  display: flex;
  align-items: center;
  gap: 10px;
}
.switch-item + .switch-item { margin-top: 12px; }

.switch-item label {
  line-height: 1.2;
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   CONTRACT — .notice-item
   WHAT IT IS       A line of text with a dismiss control, as one row.
   WHEN TO USE      A local, dismissable message inside a panel — "key applied
                    from preset X".
                    NOT for a transient corner notification — that is .toast.
                    NOT for a persistent mode or state — that is .status-strip,
                    which has no dismiss.
   MARKUP           <p class="notice-item">
                      <span>Key applied from preset “Studio A”</span>
                      <button type="button" class="btn-icon" aria-label="Dismiss"> ... </button>
                    </p>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None. Its presence is the state; the page removes it.
   DOES NOT DO      Does not dismiss itself. Its first child takes flex: 1, so
                    the dismiss sits at the row's END rather than crowding the
                    end of the sentence — do not add a spacer.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Inline notice with a dismiss (2026-09-24). The last of the controls the
   workbench archetype had to hold in a bare <p>: a line of status text with
   a .btn-icon dismiss beside it. Inline layout gave it exactly one space
   character of separation — about 4px — so the affordance crowded the text
   it belonged to.

   Same structure as .checkbox-item and .switch-item (flex + align-items:
   center, per note 10) with a wider gap: 12px from the spacing scale rather
   than their 10px, because here the two parts are a message and its control
   rather than a control and its own label, and they should not read as one
   tight unit. The text takes flex: 1 so the dismiss stays at the row's end
   when the notice is short, instead of sitting immediately after the words. */
.notice-item {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 12px 0;
}
.notice-item:first-child { margin-top: 0; }
.notice-item:last-child { margin-bottom: 0; }

.notice-item > :first-child {
  flex: 1;
  min-width: 0;
  line-height: 1.4;
  color: var(--text-primary);
}

/* Icon-only button (2026-09-22) — presets, small tool actions, an info
   affordance beside a label. Named into the .btn-primary/.btn-secondary
   family, but use it ALONE, not combined with .btn: .btn's 10px/22px padding
   fights the fixed square size. (Since 2026-10-05 .btn is 6px/15px and 28.8px
   tall, so an icon button beside one is now the TALLER of the two at 30px —
   it was the shorter until that change. Mixing them in a row has always been
   a judgement call about rhythm; it is a different-looking call now.)
   Deliberately not filled — a row of six
   filled buttons is six competing accents. The selected one carries a 1px
   --accent-blue border, the smallest saturated area that still reads across
   a row. Always give it an aria-label; there is no visible text. */
/* Dark fill, lighter stroke (2026-09-23). These were a --panel-soft fill on
   a --panel rail, i.e. LIGHTER than their surroundings, so a row of six read
   as six bright dots competing with the preview. Recessed --panel-inset with
   a --border-strong outline makes them read as wells rather than chips — the
   stroke does the work, the fill stays quiet. */
/* --------------------------------------------------------------------------
   CONTRACT — .btn-icon
   WHAT IT IS       A 30px round button holding one icon.
   WHEN TO USE      Presets, an inline dismiss, an info affordance beside a
                    label. Always with an aria-label — there is no visible
                    text.
                    NOT combined with .btn: .btn's padding fights the fixed
                    square size. NOT for a labelled action — that is .btn.
   MARKUP           <button type="button" class="btn-icon" aria-label="Dismiss">
                      <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
                        <path d="M3 3L13 13M13 3L3 13" stroke="currentColor"
                              stroke-width="1.6" stroke-linecap="round"/>
                      </svg>
                    </button>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, hover, selected via aria-pressed, disabled.
   DOES NOT DO      Does not size its own icon beyond 10px, and does not
                    accept a text glyph in place of an SVG: a character sits
                    on its font's own metrics, not the circle's centre —
                    "×" measured 1.00px low before it was replaced.
   ACCENT POLICY    Neutral at rest. Selected takes a 1px accent-blue border
                    only. Goes fully neutral under data-color-critical.
   -------------------------------------------------------------------------- */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  background: var(--panel-inset);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-primary);
  font-family: var(--font-heading);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.btn-icon:hover:not(:disabled) {
  background: var(--panel-soft);
  border-color: var(--text-muted);
}
.btn-icon[aria-pressed="true"] {
  background: var(--panel-soft);
  border-color: var(--accent-blue);
}
/* Give .btn-icon an SVG, not a text glyph (2026-09-24). An icon button
   centres its BOX correctly via note 10, but a character still sits wherever
   its own font metrics put it: measured at 3x, "×" (U+00D7 MULTIPLICATION
   SIGN) painted 1.00px BELOW the circle's centre line, because it is a math
   operator positioned on the font's math axis rather than the line-box
   centre. Note 10 forbids correcting that with a nudge, and it would be the
   wrong fix anyway — it is per-glyph and per-face, so the next icon would
   need its own number.

   An SVG is centred geometrically instead, and inherits the button's colour
   through currentColor. This matches .drawer-panel__close, which already
   carries an inline SVG cross.

   10px, reduced from 12px (2026-09-24): at 12px the cross painted 8.67px of
   ink in a 30px circle and read heavier than the control warrants — a
   dismiss is a secondary action. 10px lands the ink near 7px, between the
   old text glyph's 5.67px (too faint to read as a control) and the 12px
   SVG's 8.67px. The drawn cross occupies 10 of the viewBox's 16 units, so
   ink is roughly 0.72 of whatever size is set here. */
.btn-icon svg {
  display: block;
  width: 10px;
  height: 10px;
}

.btn-icon:disabled { opacity: 0.6; cursor: not-allowed; }

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

/* Toolbar chip (2026-09-22) — a small neutral pill for toolbar-scale
   controls: a mode toggle (Preview / Live), a row of selectable file tokens,
   or a compact action (Reset all, Load preset…). Those were three separate
   gaps on the workbench archetype, all rendering as white native browser
   buttons; they are the same shape, so this is one component, not three.

   Geometry is .theme-pill's (5px/14px, 12px, 999px) — this file's existing
   small-pill precedent — with the neutral surface language of .btn-neutral /
   .btn-icon / .color-swatch instead of the header's white outline, because
   these sit on a panel or the page rather than on the brand bar.

   It was explicitly NOT .btn's geometry, which at the time was 10px/22px and
   swamped a toolbar. That argument expired on 2026-10-05, when .btn went to
   6px/15px globally: a chip is 28.4px tall, a .btn 28.8px. The two components
   stay separate on the distinction that actually survived — a chip is 12px
   weight 500 on a neutral fill and HOLDS A SELECTION (aria-pressed /
   aria-selected); a .btn is 14px weight 700 and PERFORMS AN ACTION. Size was
   never the real difference, only the visible one. Do not merge them because
   they now measure alike. (Note the chip's padding-y is 6px, not the 5px this
   paragraph inherits from .theme-pill's geometry — see the rule's own comment
   below for why.)

   --panel-soft is the theme's neutral CONTROL fill, shared with the three
   components above, so a chip reads as the same kind of thing as an icon
   button beside it. The other two steps of the neutral ramp, if a page ever
   needs a quieter chip, are --panel and --panel-inset.

   Named .btn-chip rather than .chip: a generic name in a shared theme is
   exactly how the .swatch collision happened (see BATCH-FINDINGS item 7),
   and one downstream page already defines a bare .chip of its own. */
/* --------------------------------------------------------------------------
   CONTRACT — .btn-chip
   WHAT IT IS       A small neutral pill for toolbar-scale controls.
   WHEN TO USE      A mode toggle (Preview / Live), a row of selectable
                    tokens, a compact action, or a tab in a [role="tablist"].
                    NOT for a page's main action — that is .btn. NOT for a
                    non-interactive status pill — that is .badge.
   MARKUP  toggle   <button type="button" class="btn-chip" aria-pressed="true">Preview</button>
                    (verified example, from tests/archetype-workbench.html)
           tab      <div role="tablist" class="button-row" aria-label="Stream detail">
                      <button role="tab" class="btn-chip" aria-selected="true"
                              aria-controls="p1" id="t1">Overview</button>
                    </div>
                    <div role="tabpanel" id="p1" aria-labelledby="t1"> ... </div>
                    (UNVERIFIED example — no reviewed page uses a tablist yet)
   STATES           default, hover, selected via aria-pressed OR aria-selected,
                    disabled. No active state distinct from hover.
   DOES NOT DO      Does not hide or show tab panels — that is the [hidden]
                    attribute on the panel, owned by the page.
   ACCENT POLICY    Neutral at rest. Selected takes a 1px accent-blue border
                    only — the smallest area that reads. Goes fully neutral
                    under data-color-critical.
   -------------------------------------------------------------------------- */
.btn-chip {
  /* Same explicit centring as .btn and .btn-icon (2026-09-23) — this was the
     last control still relying on the default line box, which puts the label
     wherever the font's metrics land rather than in the middle of the pill. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-primary);
  /* 6px, not the original 5px: the explicit line-height above replaced the
     font's own `normal` leading (about 1.45 at this size) and took 3px off
     the rendered height. Padding gives it back, so a chip stays the height it
     was before the centring fix. (2026-09-23) */
  padding: 6px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.btn-chip:hover:not(:disabled) {
  background: var(--panel);
  border-color: var(--border-strong);
}
/* Two hooks, one treatment (2026-09-24): aria-pressed for a toggle, and
   aria-selected so the same chip serves as a [role="tab"] without a third
   component. A tab strip is .button-row + .btn-chip + ARIA + [hidden].
   NOTE .detail-rail__tab is NOT a tab despite the name — it is a step
   picker that borrowed the word; don't reach for it expecting tabs. */
.btn-chip[aria-pressed="true"],
.btn-chip[aria-selected="true"] {
  background: var(--panel);
  border-color: var(--accent-blue);
}
.btn-chip:disabled { opacity: 0.6; cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
  .btn-chip { transition: none; }
}

/* --------------------------------------------------------------------------
   CONTRACT — .color-swatch / .color-swatch__sample
   WHAT IT IS       A neutral pill naming a colour, with a 10px sample of the
                    literal colour inside it.
   WHEN TO USE      Choosing a key colour on a chroma page, or any control
                    where the colour itself is the value.
                    NOT for a status colour — the theme has two hues and a
                    swatch is not one of them. NOT for a general toggle — that
                    is .btn-chip.
   MARKUP           <button type="button" class="color-swatch" aria-pressed="true"
                            style="--swatch-color:#17B24A">
                      <span class="color-swatch__sample"></span>Green
                    </button>
                    (verified example, from tests/archetype-workbench.html)
   STATES           default, hover, selected via aria-pressed.
   DOES NOT DO      Does not carry its own colour — --swatch-color is set
                    INLINE, per element. That is the ONE sanctioned inline
                    style in this system: a key colour is data the control
                    carries, not styling, and it cannot come from a token
                    without putting literal green and blue hexes in this file,
                    which note 6 forbids. A sample with no --swatch-color falls
                    back to --panel-inset.
   ACCENT POLICY    The pill is neutral; selection is a --border-strong ring
                    plus aria-pressed. AREA, not hue, is what is controlled
                    here: a 10px sample is ~95% less saturated surface than a
                    full-size chip. UNCHANGED under data-color-critical, and
                    that is correct rather than an omission: the control is
                    already neutral at rest, so the scope has nothing to
                    remove. (It WAS in the scope's selector list until
                    2026-10-01, setting the border colour it already had — see
                    the note there.)
   -------------------------------------------------------------------------- */

/* Colour swatch (2026-09-22). A chroma key colour is one of the very few
   literal colours in this system: the hue IS the data the control carries,
   so it cannot come from a token. What CAN be controlled is its AREA — a
   10px sample inside a neutral pill puts roughly 95% less saturated surface
   beside the preview than a full-size colour chip.

   Named .color-swatch, NOT .swatch: chromakey already defines
   `.preset-btn .swatch` in both styles.css and setup-brand.css, on two pages
   that also link this file. A page's scoped rule only wins for the
   properties it declares, so a bare .swatch here would leak this rule's
   padding, display, gap and 999px radius onto a 16px circular preset swatch
   — a break nobody would trace back to the theme. Checked 2026-09-22:
   .color-swatch is free in every locally-cloned consumer.

   THE SAMPLE COLOUR IS SET INLINE, per element:
     <button class="color-swatch" aria-pressed="true" style="--swatch-color:#17B24A">
       <span class="color-swatch__sample"></span>Green
     </button>
   This is the ONE sanctioned inline style in this system. It is permitted
   because it is data, not styling — the same reason a page hardcodes a
   stream name. Do NOT add key-colour hexes to this file: green and blue as
   literal values here would read as status colours, which note 6 forbids.

   Selection is carried by aria-pressed, not a class, matching
   [aria-invalid] and [role="switch"]. (.lang-switch__btn still uses .active
   — a known inconsistency, deliberately not migrated in this pass.) */
.color-swatch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px 5px 8px;
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-primary);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.color-swatch:hover { border-color: var(--border-strong); }
.color-swatch[aria-pressed="true"] {
  background: var(--panel);
  border-color: var(--border-strong);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}

.color-swatch__sample {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--swatch-color, var(--panel-inset));
}

@media (prefers-reduced-motion: reduce) {
  .color-swatch { transition: none; }
}

/* ==========================================================================
   Monitor / player surface
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .player-panel / .video-surface / .video-el / .status-bar
   WHAT IT IS       A video panel with a status bar beneath it.
   WHEN TO USE      Video inside a .container page — a player demo, a
                    diagnostics page — where the video needs a status line.
                    NOT for a workbench's hero preview — that is .monitor,
                    which has no status bar and fills the main column.
   MARKUP           <section class="panel player-panel">
                      <div class="video-surface"><video class="video-el"></video></div>
                      <div class="status-bar">
                        <div class="status-left">
                          <span class="badge paused"><span class="dot"></span>Paused</span>
                          <span class="status-text">Paused (medianotavailable)</span>
                        </div>
                        <div class="status-right">Latency — · Bitrate —</div>
                      </div>
                    </section>
                    (verified example, from product-sample.html — the approved
                    reference uses .monitor instead)
   STATES           None itself; the .badge inside carries the player state.
   DOES NOT DO      .player-panel sets padding: 0 deliberately — that zero is
                    LOAD-BEARING since .panel gained padding, and it is what
                    keeps the video flush to the panel edge. Its 16:10 ratio
                    differs from .monitor's 16:9: the status bar sits in the
                    difference.
   ACCENT POLICY    Never carries accent. The .badge inside does, by variant.
   -------------------------------------------------------------------------- */
.player-panel {
  overflow: hidden; /* clips the video element to the panel's border-radius */
  /* Load-bearing since 2026-09-22: .panel now carries 20px padding, so this
     zero is what keeps the video flush to the panel edge. Before that it was
     merely redundant. */
  padding: 0;
}

.video-surface {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10.5;
  background: #000000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-el {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000000;
}

.status-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  padding: 14px 20px;
}

.status-left {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* ==========================================================================
   Badges — status pills
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .badge + .paused / .playing / .loading
   WHAT IT IS       A non-interactive status pill.
   WHEN TO USE      The current state of a stream, a player, a job — paused,
                    playing, loading.
                    NOT for a numeric readout — that is .badge--value. NOT for
                    anything clickable — that is .btn-chip. NOT for a sentence
                    about state — that is .status-strip.
   MARKUP           <span class="badge playing"><span class="dot"></span>Live</span>
                    (verified example, from tests/archetype-workbench.html)
   STATES           Three variants, not states: .paused (orange), .playing and
                    .loading (both blue). The base class alone has NO
                    background — it is unfinished without a variant.
   DOES NOT DO      Does not animate, ever — including .loading's dot. Note 5
                    forbids it: state is already carried by colour and label,
                    and motion on a page someone sits with reads as
                    distracting rather than informative.
   ACCENT POLICY    Accent-filled by variant — orange for attention (paused),
                    blue for positive (playing, loading). This is state, which
                    is what accent is for. There is no third hue: if you need a
                    fourth meaning, the LABEL carries it.
   -------------------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 13px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}

.badge.paused { background: var(--accent-orange); color: var(--text-on-accent); }
.badge.playing { background: var(--accent-blue); color: var(--text-on-accent); }
.badge.loading { background: var(--accent-blue); color: var(--text-on-accent); }

.badge .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}
/* Static, not animated — deliberate (2026-09-01, per repeated direct
   feedback on two separate product-mode pages, instant-test/instanttest.html
   and captions-workflow/index.html): infinite looping animations on status
   dots/spinners (pulse, breathe, ping, spin) read as distracting rather than
   informative on a page a visitor sits with. State changes (paused → loading
   → playing) already carry the state via color/label; motion adds nothing
   the badge doesn't already say. Applies to every status/loading indicator
   in this theme, not just this one dot — don't reintroduce `infinite`
   animations elsewhere in this file without checking with the user first. */
.badge.loading .dot {
  opacity: 0.9;
}

/* --------------------------------------------------------------------------
   CONTRACT — .badge--value
   WHAT IT IS       A neutral mono readout of a number.
   WHEN TO USE      The current value of a slider or parameter, normally in a
                    .control-row.
                    NOT for state — that is .badge with a variant. A value is
                    data, not a status.
   MARKUP           <span class="badge badge--value">42</span>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not format its number — the page does. Does not
                    right-align itself; .control-row's grid does.
   ACCENT POLICY    NEVER carries accent. A value is neither an action nor a
                    state. tabular-nums is functional, not decorative: it stops
                    the badge changing width, and the row reflowing, while a
                    slider is dragged.
   -------------------------------------------------------------------------- */

/* Numeric readout badge (2026-09-22). A value is DATA — not an action and
   not a state — so it takes no accent. The base .badge sets no background at
   all, which meant a page wanting a neutral readout had to reach for
   .badge.playing (blue) or invent something; this is the missing variant.
   Uses the same "field-shaped box" language as .info-row.wrap .value
   (--panel-inset + --border), so a readout looks like every other readout in
   this theme. Mono + tabular-nums is functional, not stylistic: it stops the
   number changing width, and the row reflowing, while a slider is dragged. */
.badge--value {
  background: var(--panel-inset);
  border: 1px solid var(--border);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 400;
  padding: 2px 8px;
}

.status-left .status-text,
.status-right {
  font-size: 12px;
  color: var(--text-primary);
}

/* ==========================================================================
   Stats / info panels — label/value row list
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .stats-row + .stats-panel
   WHAT IT IS       A two-column grid of panels (.stats-row), and the padded
                    panel companion that sits in it (.stats-panel).
   WHEN TO USE      Two panels side by side. Despite the name nothing in
                    .stats-row is stats-specific — it is the theme's only
                    two-column grid, and the approved reference uses it for
                    Foreground/Background.
                    NOT for three or more columns; there is no such rule.
   MARKUP           <div class="stats-row">
                      <section class="panel stats-panel"><h2>Foreground</h2> ... </section>
                      <section class="panel stats-panel"><h2>Background</h2> ... </section>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Collapses to one column below 640px VIEWPORT — a viewport
                    test, so a narrow panel at a wide viewport stays
                    two-column. That is why .field auto-stacks inside a
                    workbench; see BATCH-FINDINGS item 11.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Panel-level vertical rhythm (2026-09-23). A panel's direct children each
   have to carry their own margin — .panel sets padding, not a flex gap — and
   only .field (16px bottom) and .checkbox-row (20px) ever did. .button-row
   got 16px 0 on 2026-09-22; .stats-row, .status-strip and bare tables had
   none at all, so any sequence mixing them spaced unevenly. All three now
   match .field's 16px from the documented spacing scale, with first/last
   resets so a child opening or closing a panel stays flush with the panel's
   own padding. Adjacent margins collapse to 16px rather than stacking to 32.

   .checkbox-row keeps its 20px: it carries border-top/border-bottom and acts
   as a section divider rather than an ordinary child, and it is shipped.
   .control-row's 14px is the rail's internal row rhythm inside
   .control-section, not a panel-level gap — both deliberately left alone. */
.stats-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin: 16px 0;
}
.stats-row:first-child { margin-top: 0; }
.stats-row:last-child { margin-bottom: 0; }
@media (max-width: 640px) {
  .stats-row { grid-template-columns: 1fr; }
}

.stats-panel {
  padding: 20px;
}

.stats-panel h2 {
  font-family: var(--font-heading);
  margin: 0 0 12px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   CONTRACT — .info-row / .info-row.wrap
   WHAT IT IS       A label/value line, optionally with the value in a
                    field-shaped box on its own line.
   WHEN TO USE      Read-only facts inside a panel — stream state, latency, a
                    URL, an ID. Use .wrap when the value is long enough to need
                    its own line.
                    NOT for editable values — that is .field. NOT for more than
                    two columns — that is a table.
   MARKUP           <div class="info-row">
                      <span class="label">State</span>
                      <span class="value"><span class="badge playing"><span class="dot"></span>Live</span></span>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not truncate a long value — .wrap breaks it instead.
                    Its last child drops the bottom border, so a list ends on
                    content rather than a rule.
   ACCENT POLICY    Never carries accent itself. A .badge inside a value may.
   -------------------------------------------------------------------------- */
.info-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.info-row:last-child { border-bottom: none; }

/* Use for long values (URLs, IDs) that need to wrap onto their own line
   instead of squeezing next to the label */
.info-row.wrap {
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
  padding: 10px 0;
}

.info-row .label {
  color: var(--text-primary);
  flex-shrink: 0;
}

.info-row .value {
  color: var(--text-primary);
  font-weight: 500;
  text-align: right;
}

/* Renders as a field-shaped box (background/border, --panel-inset) —
   matching selects/inputs above, which is what reads as "a box" now, not
   the text color (--text-secondary is white like everything else). */
.info-row.wrap .value {
  text-align: left;
  word-break: break-all;
  font-weight: 400;
  font-family: var(--font-mono);
  font-size: 13.5px;
  color: var(--text-secondary);
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .raw-toggle + .raw-payload
   WHAT IT IS       The theme's only disclosure control, and its body.
   WHEN TO USE      Hiding machine detail behind a click — a raw payload, a
                    diagnostic dump, an overlay section.
                    NOT for an inline explanation beside a control — that is a
                    .btn-icon with aria-expanded plus a .helper (see the
                    tooltip non-rule). .raw-toggle is full-width with its
                    chevron at the far right: it is a section header, not an
                    affordance.
   MARKUP           <button type="button" class="raw-toggle" aria-expanded="false"
                            aria-controls="p">
                      <span>Overlay</span><span class="chevron">›</span>
                    </button>
                    <div class="raw-payload" id="p"> ... </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           aria-expanded false/true, which rotates the chevron. Hover
                    tints the label blue.
   DOES NOT DO      Does not open or close anything — the page toggles
                    .raw-payload's .open class and the aria-expanded value. The
                    CSS only reflects them.
   ACCENT POLICY    Accent-blue on hover only. The chevron rotation, not
                    colour, carries the open state.
   -------------------------------------------------------------------------- */

/* Expandable "raw payload" disclosure row */
.raw-toggle {
  width: 100%;
  background: none;
  border: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 7px 0 0;
  font-size: 13px;
  color: var(--text-primary);
  cursor: pointer;
  font-family: inherit;
}
.raw-toggle:hover { color: var(--accent-blue-hover); }

.raw-toggle .chevron {
  transition: transform 0.15s ease;
  display: inline-block;
}
.raw-toggle[aria-expanded="true"] .chevron {
  transform: rotate(90deg);
}
@media (prefers-reduced-motion: reduce) {
  .raw-toggle .chevron { transition: none; }
}

.raw-payload {
  display: none;
  margin-top: 10px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-primary);
  overflow-x: auto;
  white-space: pre;
}
.raw-payload.open { display: block; }

/* ==========================================================================
   Data table — multi-column row list
   ==========================================================================

   Added 2026-09-22. This file had no table rules at all: a page needing more
   columns than .info-row's two either hand-rolled its own CSS or bent
   .info-row into a shape it doesn't fit. Borders, dividers and type sizes
   below are deliberately the ones .info-row already uses — 1px var(--border)
   row rules, no vertical lines, 14px text, last row unruled — so a table and
   a stats panel read as the same component family rather than two systems.
   The caption matches .stats-panel h2 (15px/700 heading font) for the same
   reason: a table's title and a panel's title are the same thing.

   A table wider than its container does NOT scroll on its own — put it in a
   wrapper with overflow-x: auto if the column count needs it.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — table / caption / th / td / td.num
   WHAT IT IS       A multi-column row list.
   WHEN TO USE      More columns than .info-row's two — a channel list, a
                    breakdown, a log summary.
                    NOT for two columns of facts — .info-row reads better and
                    stacks. NOT for layout, ever.
   MARKUP           <table>
                      <caption>Ingest channels</caption>
                      <thead><tr><th>Stream</th><th class="num">Bitrate</th></tr></thead>
                      <tbody><tr><td>XXXXX-YYYYY</td><td class="num">4200 kbps</td></tr></tbody>
                    </table>
                    (verified example, from tests/gap-probe.html — the
                    approved reference has no table)
   STATES           None.
   DOES NOT DO      DOES NOT SCROLL. A table wider than its container overflows
                    the page — put it in a wrapper with overflow-x: auto if the
                    column count needs it. No zebra striping and no vertical
                    rules: row rules only, matching .info-row.
   ACCENT POLICY    Never carries accent. Put .num on BOTH the th and the td of
                    a numeric column, or the header sits left of its figures.
   -------------------------------------------------------------------------- */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  margin: 16px 0; /* panel-level rhythm, 2026-09-23 — see the note on .stats-row */
}
table:first-child { margin-top: 0; }
table:last-child { margin-bottom: 0; }

caption {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
  text-align: left;
  padding-bottom: 12px;
}

th {
  font-weight: 600;
  color: var(--text-primary);
  text-align: left;
  padding: 7px 12px 7px 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

td {
  color: var(--text-primary);
  padding: 7px 12px 7px 0;
  border-bottom: 1px solid var(--border);
}

/* Same as .info-row:last-child — the list ends on content, not on a rule. */
tbody tr:last-child td { border-bottom: none; }

/* No trailing gutter on the last column, so the row's right edge lines up
   with the panel's content edge. */
th:last-child,
td:last-child { padding-right: 0; }

/* Right-aligned numerics, following .info-row .value's precedent (added
   2026-09-22). Figures compare down a column far better right-aligned, and
   tabular-nums stops digit width shifting between rows as values change. */
th.num,
td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Horizontal rule (2026-09-24). <hr> had no rule, so it rendered as the
   browser's harsh near-white inset line — conspicuously unlike the eight
   places this file already draws a divider (.control-section, .info-row,
   .checkbox-row, .station, .detail-rail__head, .snippet__head, table rows,
   .field boundaries), every one of them 1px solid var(--border). This just
   makes the element agree with them. 16px from the spacing scale. */
hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 16px 0;
}

/* Keyboard key (2026-09-24). Borrows .badge--value's language exactly — mono
   on --panel-inset with a --border edge — because a keycap and a numeric
   readout are the same idea: a small boxed literal. 4px is the structural
   step from the radius scale. */
kbd {
  font-family: var(--font-mono);
  font-size: 0.92em;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  color: var(--text-primary);
}

/* Standalone monospace (2026-09-24). --font-mono was previously reachable
   only through classes that drag a box or a layout with them (.log-line,
   .raw-payload, .info-row.wrap .value, .badge--value, .snippet pre code), so
   an inline literal in a sentence — a stream name, a parameter, a header —
   had nowhere to go. No box: this is inline type, not a container. The more
   specific rules above still win where they apply. */
code,
samp {
  font-family: var(--font-mono);
  font-size: 0.92em;
  color: var(--text-primary);
}

/* NON-RULE: no blockquote (2026-09-24). A product page is an interface, not
   prose — there is no interface use for a pull quote. If you are quoting
   MACHINE output use .raw-payload; if you are calling out a mode or a
   consequence use .status-strip; if you are explaining a control use
   .helper. A customer quote belongs on a marketing page, which is a
   different tier with a different stylesheet. */

/* ==========================================================================
   NON-RULE: there is no skeleton loader in this theme (2026-09-24)
   ==========================================================================

   A shimmer, a pulsing block and a spinner are all infinite animations,
   which note 5 forbids outright. That rule came from direct, repeated
   feedback on two real product pages — it is not negotiable here, and a
   skeleton is the conventional answer it removes.

   WHAT YOU WANT                          WHAT TO USE
   --------------------------------------------------------------------------
   Say that something is loading          .badge.loading — already static by
                                          deliberate design
   Say it at more length                  .status-strip
   Show content arriving                  a ONE-SHOT entrance; note 5 permits
                                          these, see .toast and .station__body
   Show determinate progress              <progress value="..."> below
   Show indeterminate progress            nothing — give it a value

   ESCAPE HATCH, so this door is not closed by accident: if a real page turns
   up where content takes long enough that a held placeholder genuinely beats
   an empty panel, the answer is a STATIC placeholder block in --panel-soft at
   the content's shape — no motion — with the label carrying the meaning. That
   is ~8 lines and has not been written because no page has needed it. The
   risk to weigh at that point: a static skeleton can read as BROKEN rather
   than pending, which is the problem shimmer exists to solve. */

/* --------------------------------------------------------------------------
   CONTRACT — progress
   WHAT IT IS       A determinate progress bar.
   WHEN TO USE      A task with a known proportion complete — an upload, an
                    encode, a batch.
                    NOT for "something is happening" with no known end — that
                    is .badge.loading. NOT for a measurement within a range —
                    that is meter.
   MARKUP           <progress value="0.4"></progress>
                    (UNVERIFIED example — checked in a scratch page during
                    development; no reviewed page uses progress)
   STATES           None.
   DOES NOT DO      MUST have a value. An indeterminate progress animates
                    forever, which note 5 forbids. These rules suppress that
                    animation in WebKit (measured) but Firefox is UNVERIFIED —
                    do not rely on the theme for it.
   ACCENT POLICY    Accent-blue fill. A determinate bar is STATE — how far
                    along — which is what accent is for.
   -------------------------------------------------------------------------- */

/* Progress (2026-09-24). Same bug class as the pink sliders: with no rule at
   all, progress renders in the VISITOR'S OS accent colour. 6px track to match
   the slider, so a rail showing both reads as one system.

   The accent fill is defensible under the accent policy: a determinate
   progress bar is STATE — how far along something is — not decoration.

   DETERMINATE ONLY. This is a requirement, not a preference: an indeterminate
   <progress> (one with no value attribute) animates forever by definition,
   which note 5 forbids outright.
   Measured 2026-09-24 in Chrome 153, by screenshotting the same page at 600ms
   and 2400ms of virtual time and diffing: a RAW indeterminate progress
   animates, and the same element with appearance: none plus an explicit
   background is STATIC. So these rules do suppress it in WebKit. Firefox is
   NOT verified — no Firefox was available — so the animation: none on
   ::-moz-progress-bar is precautionary. Do not assume this file handles an
   indeterminate progress on every engine: give the element a value. */
progress {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 6px;
  border: 0;
  border-radius: 999px;
  overflow: hidden;
  background: var(--panel-inset);
}
/* Separate rules per engine — one unrecognised selector invalidates a whole
   list, the same trap as the range pseudo-elements. */
progress::-webkit-progress-bar {
  background: var(--panel-inset);
  border-radius: 999px;
}
progress::-webkit-progress-value {
  background: var(--accent-blue);
  border-radius: 999px;
}
progress::-moz-progress-bar {
  background: var(--accent-blue);
  animation: none;
}

/* --------------------------------------------------------------------------
   CONTRACT — meter
   WHAT IT IS       A measurement within a known range, with optimum and
                    sub-optimum bands.
   WHEN TO USE      A level, a health figure, a quota — anything where the
                    same number can be good or bad depending where it falls.
                    NOT for task completion — that is progress.
   MARKUP           <meter value="0.8" min="0" max="1" low="0.3" high="0.7"
                           optimum="0.9"></meter>
                    (UNVERIFIED example — checked in a scratch page during
                    development; no reviewed page uses meter)
   STATES           None. Its bands are driven by value against
                    low/high/optimum, which the page sets.
   DOES NOT DO      Does not distinguish the two sub-optimum bands: both read
                    as attention. The theme has two hues and the native
                    green/yellow/red is forbidden by note 6, so the VALUE and
                    its label must say which is which.
   ACCENT POLICY    Accent-blue at optimum, accent-orange for both sub-optimum
                    bands. This is state, not decoration.
   -------------------------------------------------------------------------- */

/* Meter (2026-09-24). Identical bug class to progress and the pink sliders —
   no rule, so the OS paints it.

   The reason meter needs more than progress does: its NATIVE rendering is
   green / yellow / red, straight through the three value bands. That is
   precisely the palette note 6 forbids on any product page. Collapsing those
   three bands onto this file's two-hue system is the whole point of styling
   it: optimum reads positive (--accent-blue), and BOTH sub-optimum bands read
   as attention (--accent-orange) — there is no third hue, so the two worse
   bands deliberately look the same. The value and its label carry which is
   which; colour only says whether it needs attention.

   Track matches progress and the slider at 6px. */
meter {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 6px;
  border: 0;
  border-radius: 999px;
  overflow: hidden;
  background: var(--panel-inset);
}
meter::-webkit-meter-bar {
  background: var(--panel-inset);
  border-radius: 999px;
}
meter::-webkit-meter-optimum-value {
  background: var(--accent-blue);
  border-radius: 999px;
}
meter::-webkit-meter-suboptimum-value {
  background: var(--accent-orange);
  border-radius: 999px;
}
meter::-webkit-meter-even-less-good-value {
  background: var(--accent-orange);
  border-radius: 999px;
}
/* Firefox routes all three bands through one pseudo-element, switched by a
   pseudo-CLASS on the meter itself. Separate rules, same invalidation trap. */
meter::-moz-meter-bar {
  background: var(--accent-blue);
}
meter:-moz-meter-sub-optimum::-moz-meter-bar {
  background: var(--accent-orange);
}
meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
  background: var(--accent-orange);
}

/* ==========================================================================
   Event log
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .log-panel / .log-box / .log-line / .ts / .evt
   WHAT IT IS       A scrolling monospace event log in a panel.
   WHEN TO USE      A running list of timestamped events — player events,
                    connection changes, diagnostics.
                    NOT for a one-off payload — that is .raw-payload. NOT for
                    tabular data with columns — that is a table.
   MARKUP           <section class="panel log-panel">
                      <h2>Event log</h2>
                      <div class="log-box">
                        <div class="log-line"><span class="ts">00:00:41</span><span class="evt">onPlay</span></div>
                      </div>
                    </section>
                    (verified example, from product-sample.html — the approved
                    reference has no log panel)
   STATES           None.
   DOES NOT DO      Does not auto-scroll, cap its line count, or virtualise —
                    the page owns all three. Its fixed 210px height makes it
                    the one scrolling container in the theme.
   ACCENT POLICY    Never carries accent. Timestamps and events are both white;
                    the mono face and the inset box carry the distinction.
   -------------------------------------------------------------------------- */
.log-panel {
  padding: 20px;
}

.log-panel h2 {
  font-family: var(--font-heading);
  margin: 0 0 12px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
}

.log-box {
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  height: 210px;
  overflow-y: auto;
  padding: 12px 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.7;
}

.log-line {
  white-space: pre-wrap;
  word-break: break-word;
}
.log-line .ts { color: var(--text-primary); margin-right: 10px; }
.log-line .evt { color: var(--text-primary); }

/* --------------------------------------------------------------------------
   CONTRACT — .log-actions / .copy-confirm
   WHAT IT IS       A row of log controls, and the transient "Copied" line
                    beside them.
   WHEN TO USE      Under a .log-box — a copy button, a clear button, and the
                    confirmation.
                    NOT as a general toast — that is .toast, which is fixed to
                    the corner. .copy-confirm is inline and local to its action.
   MARKUP           <div class="log-actions">
                      <button type="button" class="btn btn-neutral">Copy log</button>
                      <span class="copy-confirm">Copied</span>
                    </div>
                    (verified example, from product-sample.html)
   STATES           .copy-confirm has two: hidden (opacity 0) and .show.
   DOES NOT DO      Does not time itself out — the page adds and removes .show.
                    Does not animate beyond a 0.15s fade, which is a transition
                    rather than a looping animation.
   ACCENT POLICY    .copy-confirm is accent-blue-hover — a positive
                    confirmation, which is state. The row itself carries none.
   -------------------------------------------------------------------------- */
.log-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 16px;
}

.copy-confirm {
  font-size: 12px;
  color: var(--accent-blue-hover);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.copy-confirm.show { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .copy-confirm { transition: none; }
}

/* ==========================================================================
   Permission / consent gate — full-screen overlay with a single primary
   action, shown before the page requests a sensitive browser permission
   (camera, mic, location, notifications, etc.) so the browser's own
   prompt doesn't come out of nowhere. Extracted from instanttest.html's
   camera/mic gate (2026-08) — optional section, only needed by pages that
   gate a permission request behind an explicit user action.

   Usage: swap .engage-title / .engage-hint copy for the page's own —
   title states the payoff, hint states what's about to be asked for and
   why (plus a privacy reassurance, e.g. "nothing is recorded or stored").

   Deliberately no pulsing/blinking animation on .engage-btn: a blinking
   CTA reads as attention-grabbing at first glance but becomes visually
   noisy on a static page and can be uncomfortable for some users. A
   static shadow still stands out against the dimmed backdrop without
   relying on motion — see .btn-primary/#startButton-style CTAs elsewhere
   on the page for the same rule if they need extra emphasis.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .engage-overlay and its parts
   WHAT IT IS       A full-screen consent gate with a single large action,
                    shown before the page asks for a browser permission.
   WHEN TO USE      ONLY when the page requests a sensitive permission —
                    camera, mic, location, notifications — so the browser's own
                    prompt does not arrive unannounced. Skip the section
                    entirely otherwise.
                    NOT as a splash screen or a marketing interstitial. NOT for
                    a dialog — that is .modal-overlay.
   MARKUP           <div class="engage-overlay">
                      <div class="engage-content">
                        <h2 class="engage-title">Test your camera</h2>
                        <p class="engage-hint">We'll ask for camera and mic access.
                          Nothing is recorded or stored.</p>
                        <button type="button" class="engage-btn">Allow access</button>
                      </div>
                    </div>
                    (UNVERIFIED example — no page in this skill demonstrates
                    the gate; instanttest.html downstream does)
   STATES           default and .fadeout, which the page adds once permission
                    is granted.
   DOES NOT DO      Does not request the permission — the button's handler
                    does. Deliberately no pulsing or blinking on the button: a
                    blinking CTA becomes noise on a static page, and note 5
                    forbids it. The copy must say what is about to be asked for
                    AND give the privacy reassurance.
   ACCENT POLICY    .engage-btn is accent-orange — the page's one committing
                    action, which is what accent is for. The overlay tint
                    carries none.
   -------------------------------------------------------------------------- */
.engage-overlay {
  position: fixed;
  inset: 0;
  background: rgba(13, 23, 30, 0.88);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 15vh;
  z-index: 10002;
  transition: opacity 0.6s ease;
}
.engage-overlay.fadeout { opacity: 0; pointer-events: none; }

.engage-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.engage-title {
  font-family: var(--font-heading);
  font-size: 1.3em;
  font-weight: 700;
  color: var(--text-primary);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: -4px;
}

.engage-hint {
  color: var(--text-primary);
  font-size: 14px;
  text-align: center;
  max-width: 380px;
  line-height: 1.5;
  opacity: 0.85;
}

.engage-btn {
  /* Label centring — see note 10 at the top of this file. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  padding: 18px 42px;
  background: var(--accent-orange);
  color: var(--text-on-accent);
  border: none;
  border-radius: 999px;
  font-family: var(--font-heading);
  font-size: 1.15em;
  font-weight: 700;
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
  transition: background-color 0.15s ease, transform 0.15s ease;
  opacity: 1;
}
.engage-btn.fadeout { opacity: 0; }
.engage-btn:hover { background: var(--accent-orange-hover); transform: translateY(-2px); }

@media (max-width: 640px) {
  .engage-btn { padding: 16px 28px; font-size: 1em; max-width: 90%; text-align: center; }
}

/* ==========================================================================
   Modal / dialog panel — centered overlay with a bordered panel, for a
   sign-in form, confirmation dialog, or settings panel. Optional — only
   include if the page needs one; skip it entirely otherwise. Added
   2026-09-01, generalized from captions-workflow's sign-in popup restyle:
   same .panel radius/border as everywhere else in this theme (this is NOT
   a separate "modal" visual language, just the standard panel, centered
   over a dimmed backdrop) — reuse .field/.btn/.badge inside it rather than
   inventing modal-specific form styling.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .modal-overlay / .modal-panel / __title / __sub
   WHAT IT IS       A centred dialog over a dimmed backdrop.
   WHEN TO USE      A sign-in form, a confirmation, a settings panel.
                    NOT for content too large to centre — that is the drawer.
                    NOT for a notification — that is .toast.
   MARKUP           <div class="modal-overlay" hidden>
                      <div class="modal-panel">
                        <h2 class="modal-panel__title">Sign in</h2>
                        <p class="modal-panel__sub">Use your dashboard account.</p>
                        <div class="field"> ... </div>
                      </div>
                    </div>
                    (verified example, from tests/archetype-stream.html,
                    whose sign-in gate ships hidden. That page is also a
                    standing test of the [hidden] rule below: if the gating
                    breaks, the gate renders as a backdrop over the player.)
   STATES           Hidden via [hidden] or aria-hidden. There is no "open"
                    class in this file — visible is the default.
   DOES NOT DO      Does not trap focus, close on Escape, or close on backdrop
                    click — all three are the page's job. Reuse .field, .btn
                    and .badge inside it: this is not a separate visual
                    language, just the standard panel centred.
                    CRITICAL: .modal-overlay sets display: flex, which
                    OVERRIDES the UA [hidden] rule. Hiding it depends on the
                    rule below that re-asserts display: none — without that,
                    <div class="modal-overlay" hidden> renders a visible
                    full-screen click-blocker. See governing rule 2a.
   ACCENT POLICY    Never carries accent. A .btn-primary inside may, as the
                    dialog's one committing action.
   -------------------------------------------------------------------------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(13, 23, 30, 0.72);
}

/* Governing rules 2 and 2a. The overlay had no inactive state of its own, so
   each consumer invented one — .active in multiplayer, .hidden in
   live-recorder, [hidden] in analytics-tests.

   Deliberately NOT pointer-events: none on the base rule the way
   .drawer-overlay does it: multiplayer's .active hook does not restore
   pointer-events, so a base-level `none` would leave its modal unclickable
   WHEN OPEN — the precise failure this file's governing rule exists to
   prevent. Gating on the hidden state instead can only fire when the page
   says the overlay is inactive, so it cannot break an open one.

   display: none is the load-bearing half: .modal-overlay sets display: flex,
   which overrides the UA [hidden] rule, so <div class="modal-overlay" hidden>
   currently renders a visible full-screen click-blocker. analytics-tests hit
   exactly this and worked around it in its own CSS. (2026-09-24) */
.modal-overlay[hidden],
.modal-overlay[aria-hidden="true"] {
  display: none;
  pointer-events: none;
}

.modal-panel {
  width: min(460px, 100%);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 4px; /* same modest radius as every panel in this theme */
  /* Strongest elevation on the page — a dialog floating over the page it
     covers needs to lift off it more than a base .panel does. */
  box-shadow: var(--shadow-lg);
  padding: 28px;
}

.modal-panel__title {
  font-family: var(--font-heading);
  font-size: 19px;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0 0 4px;
}

.modal-panel__sub {
  font-size: 13.5px;
  color: var(--text-secondary);
  margin: 0 0 20px;
}

/* ==========================================================================
   NON-RULE: there is no sign-up form in this theme (2026-10-06, deliberate)
   ==========================================================================

   Sign-in is here. Sign-up is not, and the gap is a decision.

   Both product consumers that authenticate link OUT to
   dashboard.nanostream.cloud/signup rather than collecting an account
   themselves. The real sign-up — homepage-ai/signup.html, marketing tier —
   needs company, email, password, confirm-password, a REQUIRED terms
   checkbox, a feature multi-select and a use-case select: seven fields
   against sign-in's two, plus consent and cross-field validation. That is a
   different component wearing a similar coat.

   WHAT YOU WANT                          WHAT TO USE
   --------------------------------------------------------------------------
   Let someone sign in                    .modal-overlay / .modal-panel, with
                                          .field--stacked, .btn--block and
                                          .form-result
   Let someone get an account             an <a> to the dashboard's signup —
                                          what both consumers already do
   Confirm a destructive action           .modal-overlay with .btn-primary
   Collect a long form on a product page  .panel + .field — and reconsider: a
                                          product page is not a conversion
                                          surface (see page-generation.md)

   ESCAPE HATCH: if a product page ever genuinely has to create an account in
   place, the missing pieces are a required-consent checkbox row and
   cross-field validation (password against confirm). Both are additive — the
   checkbox exists, [aria-invalid] exists, and .form-result already covers the
   submission outcome. Nothing here blocks it. It has not been written because
   no product page has needed it, and building the third shape of a component
   on speculation is how the first two got invented three times over.
   ========================================================================== */

/* ==========================================================================
   Drawer — a panel that slides in from the side, for content too large for
   a centered modal (a media player, a long detail view) but still meant as
   a temporary overlay, not a permanent part of the layout. Optional — only
   include if the page needs one; skip it entirely otherwise. Added
   2026-09-04, extracted from captions-workflow's caption player drawer
   (`.e2e-player-drawer`) — only the reusable overlay/slide/backdrop
   mechanism is adapted here, not that page's player-specific content.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .drawer-overlay / .drawer-panel and their parts
   WHAT IT IS       A panel sliding in from the right over a dimmed backdrop.
   WHEN TO USE      Content too large for a centred dialog — a media player, a
                    long detail view — that is still temporary.
                    NOT for permanent layout — that is .detail-rail. NOT for a
                    short form or confirmation — that is .modal-*.
   MARKUP           <div class="drawer-overlay" aria-hidden="true">
                      <div class="drawer-overlay__backdrop"></div>
                      <div class="drawer-panel" role="dialog" aria-modal="true">
                        <div class="drawer-panel__head">
                          <span class="drawer-panel__title">Live Captions Preview</span>
                          <button type="button" class="drawer-panel__close" aria-label="Close"> ... </button>
                        </div>
                        <div class="drawer-panel__body"> ... </div>
                      </div>
                    </div>
                    (verified example, from product-theme-moodboard.html)
   STATES           Closed by default; .open slides it in and enables pointer
                    events. aria-hidden drives visibility, delayed so the
                    slide-out finishes before it vanishes.
   DOES NOT DO      Does not trap focus or close on Escape — the page's job.
                    The overlay is pointer-events: none until .open: this is
                    the pattern .modal-overlay should have had, and governing
                    rule 2 exists because of it.
   ACCENT POLICY    Never carries accent. Its head reuses the header gradient —
                    identity, not accent, and not a separate colour.
   -------------------------------------------------------------------------- */
.drawer-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
}
.drawer-overlay__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(13, 23, 30, 0.72); /* same overlay tint as .modal-overlay */
  opacity: 0;
  transition: opacity 0.24s ease-out;
}
.drawer-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(960px, calc(100% - 48px));
  background: var(--panel);
  border-radius: 4px 0 0 4px; /* same modest radius as every panel, just one-sided */
  box-shadow: var(--shadow-lg); /* strongest elevation, same as .modal-panel */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: translateX(102%);
  transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.drawer-overlay.open { pointer-events: auto; }
.drawer-overlay.open .drawer-overlay__backdrop { opacity: 1; }
.drawer-overlay.open .drawer-panel { transform: none; }
/* Keep the closed drawer out of the tab order and off-screen for assistive
   tech, but delay it until the slide-out transition finishes so the panel
   doesn't just vanish mid-animation. */
.drawer-overlay[aria-hidden="true"] { visibility: hidden; transition: visibility 0s 0.3s; }
.drawer-overlay.open[aria-hidden="false"] { visibility: visible; }

.drawer-panel__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  background: linear-gradient(180deg, #00486c 0%, #00486c 25%, #0f8dc8 100%); /* same header gradient, not a separate color */
  flex-shrink: 0;
}
.drawer-panel__title {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 700;
  color: var(--text-on-accent);
  white-space: nowrap;
}
.drawer-panel__close {
  /* Label centring — see note 10 at the top of this file. */
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  color: var(--text-on-accent);
  padding: 7px;
  cursor: pointer;
  display: inline-flex;
  flex-shrink: 0;
  transition: background 0.15s ease;
}
.drawer-panel__close:hover { background: rgba(255, 255, 255, 0.18); }

.drawer-panel__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 18px;
}

/* ==========================================================================
   Toast — a transient corner notification. Optional — only include if the
   page needs one; skip it entirely otherwise. Added 2026-09-04, extracted
   from captions-workflow's `.app-toast`. Two states only, per the no-green/
   red/yellow rule: the default is fully neutral (informational needs no
   colour), .toast--attn is the attention bucket (error, warning) and marks
   itself with a 6px orange dot. There is no third variant — don't add one.
   Entrance is a one-shot slide-in, never looping. Both states previously
   carried a 4px accent left bar; removed 2026-09-22, see the note on
   .status-strip--attn for why a single accented edge was wrong here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .toast-stack / .toast and their parts
   WHAT IT IS       Transient notifications stacked in the top-right corner.
   WHEN TO USE      Something happened that the visitor should know about but
                    need not act on — saved, copied, connection restored.
                    NOT for a message local to a panel — that is .notice-item.
                    NOT for persistent state — that is .status-strip. NOT for a
                    field error — that is .field .error.
   MARKUP           <div class="toast-stack">
                      <div class="toast toast--attn" role="status">
                        <div class="toast__text">
                          <div class="toast__title">Ingest stopped</div>
                          <div class="toast__body">The encoder disconnected.</div>
                        </div>
                        <button type="button" class="toast__dismiss" aria-label="Dismiss"> ... </button>
                      </div>
                    </div>
                    (verified example, from product-theme-moodboard.html)
   STATES           Two variants: default (informational) and --attn (error,
                    warning). There is deliberately no third.
   DOES NOT DO      Does not time itself out or limit the stack — the page owns
                    both. Its entrance is a ONE-SHOT slide-in, never looping.
                    .toast-stack is pointer-events: none with the toasts
                    themselves auto, so the empty corner stays clickable.
   ACCENT POLICY    Default carries NONE — informational needs no colour.
                    --attn marks itself with a 6px orange dot, not a left edge,
                    which this file forbids.
   -------------------------------------------------------------------------- */
.toast-stack {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 110;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(360px, calc(100vw - 32px));
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow);
  padding: 11px 12px; /* was 11px 12px 11px 16px to clear the removed left bar */
  overflow: hidden;
  animation: toast-in 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Attention dot — see the note on .status-strip--attn. margin-top aligns it
   with the first line of .toast__title (13.5px text) rather than the top of
   the flex row. */
.toast--attn::before {
  content: '';
  width: 6px;
  height: 6px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--accent-orange);
  flex-shrink: 0;
}
@keyframes toast-in {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}

.toast__text { flex: 1; min-width: 0; }
.toast__title { font-size: 13.5px; font-weight: 700; color: var(--text-primary); }
.toast__body { font-size: 13px; color: var(--text-secondary); margin-top: 1px; overflow-wrap: break-word; }
.toast__dismiss {
  /* Label centring — see note 10 at the top of this file. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  background: none;
  border: none;
  padding: 3px;
  margin: 0;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 4px;
  flex-shrink: 0;
}
.toast__dismiss:hover { color: var(--text-primary); background: var(--panel-inset); }

/* ==========================================================================
   Footer — full-width orange bar, column-aligned content
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .footer / .footer-inner / .page-version
   WHAT IT IS       The orange bar closing every page, carrying the copyright
                    and the version stamp.
   WHEN TO USE      Every product page, unchanged.
                    NOT a place for links beyond the nanocosmos one — Cookies,
                    Privacy and Imprint are marketing-tier.
   MARKUP           <footer class="footer">
                      <div class="footer-inner">
                        <span>© 2026 <a href="https://www.nanocosmos.net">nanocosmos</a> GmbH</span>
                        <div class="page-version" aria-label="Page version">
                          <span>v<!-- version -->1.0.0<!-- /version --></span>
                          <span aria-hidden="true">·</span>
                          <span>Updated <!-- updated -->2026-09-25<!-- /updated --></span>
                        </div>
                      </div>
                    </footer>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not stick. The version stamp's HTML comment markers
                    are REQUIRED — CI bumps the values without parsing HTML,
                    so removing them breaks the bump.
   ACCENT POLICY    The orange bar is the one sanctioned full-width accent
                    surface in the theme, and it is identity rather than
                    action. Everything on it is white, including .page-version
                    — solid, not the translucent marketing treatment.
   -------------------------------------------------------------------------- */
.footer {
  background: var(--accent-orange);
  color: var(--text-on-accent);
  padding: 16px 0;
  font-size: 13px;
  margin-top: 40px; /* 2026-09-04, matches product-sample.html — was missing entirely */
}

/* Same width formula as .header-inner/.container — see the note on
   .header-inner. */
.footer-inner {
  width: min(calc(100% - (var(--page-margin) - 16px) * 2), calc(var(--page-max-width) + 32px));
  margin: 0 auto;
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
}

/* 2026-09-04: links no longer always-underlined — bold, white, underline
   only on hover, matching product-sample.html/captions-workflow. */
.footer a {
  color: var(--text-on-accent);
  text-decoration: none;
  font-weight: 600;
}
.footer a:hover {
  text-decoration: underline;
}

/* Page version stamp (2026-09-04 — added, was completely missing from this
   file despite being a mandatory brand requirement per SKILL.md's "Page
   Version Stamp" section: every generated page carries this, no exceptions).
   Solid #ffffff, not translucent — that's the marketing-tier convention,
   not this one. Markup:
     <div class="page-version" aria-label="Page version">
       <span>v<!-- version -->1.0.0<!-- /version --></span>
       <span aria-hidden="true">·</span>
       <span>Updated <!-- updated -->2026-09-04<!-- /updated --></span>
     </div>
   Keep the HTML comment markers intact — CI/scripts bump them without
   parsing HTML. */
.page-version {
  display: flex;
  gap: 8px;
  font-size: 12px;
  color: var(--text-on-accent);
  letter-spacing: 0.02em;
}

/* ==========================================================================
   AV workbench layout — for tool pages built around a live preview/monitor
   (chromakey setup, encoder config, stream dashboard, player diagnostics)
   ==========================================================================

   Added 2026-09-04. Adapted from the dark-neutral-studio skill's AV-workbench
   pattern — but only the STRUCTURE and the COPY RULES below are adapted.
   dark-neutral-studio's own color tokens (#ff821f orange / #07587f blue) and
   its "strong geometric sans" font suggestion are NOT used anywhere in this
   section — every color and font below is this file's own token
   (--accent-orange, --accent-blue, --panel, --border, --font-heading,
   --font-body). See SKILL.md's dark-neutral-studio priority note: for any
   nanocosmos page, this file's tokens always win, even when
   dark-neutral-studio is the skill that supplied the layout idea.

   Reading order for a page with a live preview (put elements in this order,
   top to bottom or in this visual priority):
   1. The preview/monitor/canvas itself — the thing the visitor is actually
      looking at.
   2. Its current status (connecting, live, error, etc).
   3. The controls that act on it.
   4. Setup, auth, and other one-time or secondary content.
   A setup form or instructions block must never be visually bigger or
   louder than the preview area.

   Status color rule: this file only has two status colors, per the "No
   green/red/yellow" rule earlier in this file (see :root comment) —
   --accent-orange for anything needing attention (error, warning, live)
   and --accent-blue for a positive/neutral state (connected, ready, done).
   Do not add a third hue for status. Use the icon or text label to say
   which specific thing is happening; color only signals "does this need
   attention or not."

   Copy style for this kind of page — write labels and messages like a
   control surface, not a pitch:
     Good: "Preview engine: JS canvas", "Live Apply", "Needs Apply",
           "Stream ready", "Capture blocked by browser".
     Avoid: "Experience the next generation of chromakey",
            "Unlock powerful creative possibilities",
            long explanatory tutorials inside the tool itself.
   This matches the "Copy: Functional, label-driven, brief, instructional"
   row in the Two-Tier table in SKILL.md — it is not a new rule, just a
   concrete example set for workbench-style pages.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .workbench
   WHAT IT IS       The page column for a tool built around a live
                    preview/monitor.
   WHEN TO USE      Chroma tuner, encoder config, stream dashboard, player
                    diagnostics — anything where a preview is the hero.
                    NOT for a form or a reading page: that is .container.
   MARKUP           <div class="workbench">
                      <div class="workbench-layout">
                        <div class="workbench-main"> ... </div>
                        <aside class="control-rail"> ... </aside>
                      </div>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None. This is layout.
   DOES NOT DO      Does not space the blocks stacked inside one column —
                    .workbench-layout's gap separates the two COLUMNS only.
                    The main column needs .workbench-main for its own rhythm.
                    Does not scroll or cap its own height.
   ACCENT POLICY    Never carries accent. On a nanoStudio page this is
                    normally the element carrying data-color-critical — see
                    the scoping discipline in that section.
   -------------------------------------------------------------------------- */
.workbench {
  width: min(calc(100% - var(--page-margin) * 2), var(--page-max-width));
  margin: 18px auto 30px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .workbench-main / .workbench-layout
   WHAT IT IS       The two-column grid of a workbench, and the left column's
                    own vertical rhythm.
   WHEN TO USE      Always together, inside .workbench. .workbench-main wraps
                    everything in the left column so its blocks are spaced.
                    NOT interchangeable: .workbench-layout's gap separates the
                    two COLUMNS, .workbench-main separates blocks WITHIN one.
   MARKUP           <div class="workbench-layout">
                      <div class="workbench-main"> ... </div>
                      <aside class="control-rail"> ... </aside>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Collapses to one column below 960px viewport, preview
                    first. The rail does NOT get .workbench-main's rhythm —
                    .control-section already spaces itself, and both would
                    double every divider's breathing room.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */

/* Vertical rhythm for the main (left) column of a workbench layout, added
   2026-09-22. .workbench-layout's gap: 18px separates the two GRID ITEMS;
   it does nothing between the blocks stacked inside one of them, and .panel
   carries no margin — so a toolbar, a status strip, a monitor and a panel
   row rendered touching each other with no space at all. This is exactly
   what .container already does for non-workbench pages
   (flex-direction: column + gap), reusing its 20px so the two page patterns
   share one rhythm rather than each inventing a number. The rail opposite
   does NOT get this: .control-section's border-top + 16px already spaces it,
   and adding a flex gap on top would double every divider's breathing room. */
.workbench-main {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0; /* lets the column shrink instead of overflowing the grid */
}

.workbench-layout {
  display: grid;
  grid-template-columns: minmax(720px, 1fr) minmax(390px, 440px);
  gap: 18px;
  align-items: start;
}

/* 1224px, not 960px (2026-09-28). The grid's own minimum is
   720 + 390 + 18 = 1128px of container width, and .workbench is
   viewport - 96px — so the column count has to change at 1128 + 96 = 1224px.
   At 960px the grid was still asking for 1128px inside 864px, and the PAGE
   scrolled sideways for the whole range between 961px and 1223px. That
   breaks this file's own rule that the body must never scroll horizontally.
   Measured on tests/archetype-workbench.html before the fix: body scrollWidth
   1176 at both 1000px and 1100px viewport, against innerWidth 1000 / 1100.
   Re-derive this number if either track or the gap changes. */
@media (max-width: 1224px) {
  .workbench-layout {
    grid-template-columns: 1fr;
  }
}

/* Widen the header/footer content column to match the page's actual wide
   content whenever one is present anywhere on the page — see item 9 in the
   top-of-file comment for why max-width here is content-width + 96px
   (content-width + this rule's own 48px*2 padding), not the bare content
   width: a wide layout typically has no side padding of its own, so
   matching its figure directly here would leave the header/footer content
   sitting 48px further right than that layout's actual edge (confirmed on
   real pages, fixed 2026-09-08).
   Generalized 2026-09-07 (was .workbench-only): this alignment problem
   isn't specific to the AV-workbench pattern — ANY product-page pattern
   that widens its main content past .container's 1040px hits the same
   48px gap (the guided-journey pattern's own dev-view rail row is a real
   example, independently solved with the identical arithmetic in
   captions-workflow's hand-rolled CSS). Rather than hardcode every such
   pattern's own content width here, a new pattern opts in by adding
   `data-wide-layout` to <body> and setting `--wide-content-width` to that
   pattern's own max content width (e.g. `1740px` for a journey+rail row) —
   no further edit to this rule needed. `.workbench` keeps its own
   pre-registered selector at its known 1520px so existing consumers are
   unaffected. Requires :has() (Chrome 105+, Safari 15.4+, Firefox 121+),
   an acceptable baseline for internal product/demo tooling. Pages built
   on .container only (no wide pattern present) are unaffected either way. */
/* 2026-09-22 — these override WIDTH, not max-width. The 2026-09-08 fix set
   max-width: 1616px here and was dead code: .header-inner's base rule
   declares width: min(100% - 64px, 1552px), and since 1552 < 1616 the
   max-width could never bind at any viewport. Header content therefore sat
   32px right of the wide layout's edge — the exact difference between the
   two margin terms, (--page-margin - 16px) * 2 = 64px here against
   --page-margin * 2 = 96px on .workbench. Measured 2026-09-22 at both 1920
   (header x=232 vs workbench x=200) and 1400 (x=80 vs x=48), then
   re-measured after this change: both share an edge at both widths.
   The + 96px is still content-width + this rule's own 48px-per-side padding
   — see item 9 in the top-of-file comment. Do not reintroduce max-width
   alongside width; two competing constraints on the same box is what made
   the previous fix silently ineffective. */
body:has(.workbench) .header-inner,
body:has(.workbench) .footer-inner {
  width: min(100%, calc(var(--page-max-width) + 96px));
  padding: 0 48px;
}
body[data-wide-layout] .header-inner,
body[data-wide-layout] .footer-inner {
  width: min(100%, calc(var(--wide-content-width, 1520px) + 96px));
  padding: 0 48px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .monitor
   WHAT IT IS       The live preview surface — a near-black frame at 16:9.
   WHEN TO USE      The video, canvas or output a workbench page is built
                    around. One per page.
                    NOT for a video inside a panel elsewhere — that is
                    .player-panel + .video-surface, which carries a status bar.
   MARKUP           <div class="monitor">
                      <video class="video-el" muted playsinline></video>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not letterbox or scale its content — the video
                    element does, via object-fit. Its background is a literal
                    near-black (#050607), not a token: it is the frame around
                    video, not a themed UI surface.
   ACCENT POLICY    Never carries accent. Under data-color-critical its border
                    drops to --panel-inset so no light ring competes with the
                    edges the operator is judging.
   -------------------------------------------------------------------------- */

/* The live preview surface. Stays near-black regardless of theme — this
   is the frame around video content, not a themed UI surface, so it does
   not use --panel/--bg. */
.monitor {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #050607;
  border: 1px solid var(--border);
  border-radius: 4px; /* was 6px, a dark-neutral-studio value — see the radius scale */
  overflow: hidden;
  box-shadow: var(--shadow);
}

/* --------------------------------------------------------------------------
   CONTRACT — .control-rail
   WHAT IT IS       The right-hand parameter rack of a workbench layout.
   WHEN TO USE      Inside .workbench-layout, as the second column, holding
                    the controls that act on the preview.
                    NOT as a general sidebar on a .container page — it is
                    sized and structured for a workbench. NOT for reference
                    material that would crowd the page — that is .detail-rail.
   MARKUP           <aside class="control-rail">
                      <div class="control-section"> ... </div>
                    </aside>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not scroll or cap its own height — it grows with its
                    content and the page scrolls. Does not space its children
                    directly; each .control-section owns its own separation.
   ACCENT POLICY    Never carries accent. Its top border was accent-blue until
                    2026-09-23 and is now --border-strong: a structural edge
                    is identity, not action or state.
   -------------------------------------------------------------------------- */

/* Right-hand parameter panel. Use section dividers (.control-section)
   instead of nested cards for each group of controls. */
.control-rail {
  background: var(--panel);
  border: 1px solid var(--border);
  /* Neutral, not accent (2026-09-22). This was a 2px --accent-blue bar
     across the top of the rail: structural decoration, signalling neither an
     action nor a state, sitting inches from a preview an operator may be
     colour-judging. --border-strong keeps the "this is a distinct structural
     surface" reading that dark-neutral-studio's blue top border was for,
     without putting saturation next to the video. */
  border-top: 2px solid var(--border-strong);
  border-radius: 4px; /* was 9px, a dark-neutral-studio value — see the radius scale */
  padding: 18px;
}

/* --------------------------------------------------------------------------
   CONTRACT — .control-section
   WHAT IT IS       One group of controls inside a rail, separated by a rule.
   WHEN TO USE      Every group in a .control-rail. Dividers rather than
                    nested cards — a rail is a rack, not a pile of panels.
                    NOT a .panel: nesting panels inside a rail doubles the
                    borders and shadows.
   MARKUP           <div class="control-section">
                      <h3>Key parameters</h3>
                      ...
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None.
   DOES NOT DO      Does not draw a top border on the first section — the
                    :first-child rule removes it, so the rail opens on content
                    rather than a line.
   ACCENT POLICY    Never carries accent.
   -------------------------------------------------------------------------- */
.control-section {
  border-top: 1px solid var(--border);
  padding-top: 16px;
  margin-top: 16px;
}
.control-section:first-child {
  border-top: 0;
  margin-top: 0;
  padding-top: 0;
}

/* --------------------------------------------------------------------------
   CONTRACT — .control-row
   WHAT IT IS       A label / control / value row sized for a narrow rail.
   WHEN TO USE      Any parameter in a .control-rail — typically a slider with
                    a .badge--value.
                    NOT in a form panel — that is .field, whose 200px label
                    column needs the width. The two are NOT interchangeable:
                    .field beside a preview is unusable, .control-row in a wide
                    panel wastes the row.
   MARKUP           <div class="control-row">
                      <label for="p-hue">Hue</label>
                      <input type="range" id="p-hue" min="0" max="360" value="120">
                      <span class="badge badge--value">120°</span>
                    </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           None itself.
   DOES NOT DO      Does not accept a checkbox or switch as its control —
                    those belong on the label line, so use .switch-item or
                    .checkbox-item. The grid areas let the value badge sit LAST
                    in the markup, after the control it describes, while
                    rendering top-right: do not reorder the markup to match the
                    visual.
   ACCENT POLICY    Never carries accent itself.
   -------------------------------------------------------------------------- */

/* Narrow-rail control row (2026-09-22). .field is a label-BESIDE-control
   layout with a fixed 200px label column; inside a 390–440px .control-rail,
   less its 18px padding either side, that leaves under 200px for the control
   itself — so a slider row built with .field is unusable. This stacks
   instead: label and value share the top line, the control spans the full
   width beneath.

   Grid AREAS rather than source order, deliberately: the value badge can sit
   last in the markup — after the control it describes, which is the right
   reading order — while rendering top-right. */
.control-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "label value"
    "control control";
  align-items: center;
  column-gap: 10px;
  row-gap: 6px;
  margin-top: 14px;
}
.control-row:first-child { margin-top: 0; }

.control-row > label {
  grid-area: label;
  font-size: 13px;
  color: var(--text-primary);
}
.control-row > .badge { grid-area: value; justify-self: end; }

/* Explicit control list, same convention as the .field selector list — a
   bare `> input` would also catch a checkbox or switch, which belongs on the
   label line, not spanning the row. */
.control-row > input[type="range"],
.control-row > input[type="text"],
.control-row > select {
  grid-area: control;
  width: 100%;
}

/* --------------------------------------------------------------------------
   CONTRACT — .status-strip / .status-strip--attn
   WHAT IT IS       A compact line stating a mode, a connection or a stream
                    state.
   WHEN TO USE      Preview vs live, engine in use, "needs apply", a capture
                    blocked by the browser.
                    NOT for a field's validation message — that is
                    .field .error. NOT for a transient notification — that is
                    .toast. NOT for explaining a control — that is .helper.
   MARKUP           <p class="status-strip status-strip--attn">Preview mode — switch to Live to send this composite to the ingest.</p>
                    (verified example, from tests/archetype-workbench.html)
   STATES           Two only: default (informational) and --attn (error,
                    warning, live). There is deliberately no third.
   DOES NOT DO      Does not dismiss itself or animate. Does not wrap its text
                    in a container — it IS the container.
   ACCENT POLICY    Default carries NONE: informational is the resting state
                    and needs no colour. --attn marks itself with a 6px orange
                    dot — not a left edge, which this file forbids. NOT
                    neutralised under data-color-critical: ~28px of orange is
                    the smallest marker that still reads, and removing it would
                    leave the attention state with no distinction at all.
   -------------------------------------------------------------------------- */

/* Compact status line for preview engine / connection / stream state.
   Default is fully neutral — informational IS the resting state and does not
   need colour (2026-09-22; it previously carried a 3px --accent-blue left
   edge). Add .status-strip--attn for error/warning/live states — do not
   invent a third colour for this. */
.status-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-primary);
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-radius: 4px; /* was 6px, a dark-neutral-studio value — see the radius scale */
  padding: 7px 12px;
  font-size: 12px;
  margin: 16px 0; /* panel-level rhythm, 2026-09-23 — see the note on .stats-row */
}
.status-strip:first-child { margin-top: 0; }
.status-strip:last-child { margin-bottom: 0; }

/* Attention state as a dot, not an edge (2026-09-22). The 3px accent left
   border this replaced was a single-edge stripe — the exact pattern this
   file forbids at the .field rule above ("never a single accented edge"),
   added 2026-09-01 after a consuming page was corrected for growing one.
   Both .status-strip and .toast had been quietly breaking that rule ever
   since.

   A dot rather than the uniform all-sides border that rule literally
   prescribes: on a full-width strip a 1px border on every side is roughly
   1780px of orange against the old bar's 120px, so literal compliance would
   put MORE saturation beside a preview, not less. The dot is ~28px and
   still unmistakable. Same reasoning as dark-neutral-studio's "use a small
   orange dot for dirty/needs-apply rather than large warning text", and the
   same 6px marker this file already uses for .badge .dot.

   Generated content, so no consuming page's markup changes. */
.status-strip--attn::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-orange);
  flex-shrink: 0;
}

/* ==========================================================================
   Guided journey / station list — for a page built around a linear,
   step-by-step setup flow (onboarding, activation, a multi-stage wizard)
   rather than a live preview/monitor (that's the AV workbench above).
   ==========================================================================

   Added 2026-09-04, extracted from captions-workflow's five-station
   onboarding journey (`.cw-journey`/`.cw-station`) — the reusable
   structure/interaction only (marker states, progress spine, spotlight,
   expand-on-current), not that page's own station content or copy.

   One station open at a time. Completed stations collapse to a one-line
   summary and stay visible (never removed from the DOM or hidden away) —
   the visitor should always be able to see how far they've come. Exactly
   one station carries the "current" spotlight + orange halo at a time; two
   competing "you are here" markers means the flow has failed.

   Status color rule (same two-hue system as everywhere else in this file):
   a done station's marker/spine is --accent-blue (positive/complete), the
   current station's marker is --accent-orange (the one thing needing
   action right now), a locked station is neutral border color. No green
   checkmark, no third hue.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .journey / .station and their parts
   WHAT IT IS       A vertical, one-step-at-a-time setup flow with a progress
                    spine.
   WHEN TO USE      Onboarding, activation, a multi-stage wizard — where the
                    steps are ordered and the visitor should see how far they
                    have come.
                    NOT for a page built around a live preview — that is the
                    workbench. NOT for tabs: a journey is sequential, tabs are
                    not.
   MARKUP           <div class="journey">
                      <section class="station" data-state="done">
                        <div class="station__head">
                          <span class="station__marker">1</span>
                          <div>
                            <div class="station__title">Create a stream</div>
                            <div class="station__summary">XXXXX-YYYYY</div>
                          </div>
                        </div>
                      </section>
                      <section class="station" data-state="current"> ... </section>
                    </div>
                    (verified example, from product-theme-moodboard.html)
   STATES           data-state on each station: done, current, or absent
                    (locked). Exactly ONE station is current — two competing
                    "you are here" markers means the flow has failed.
   DOES NOT DO      Does not advance itself. Completed stations COLLAPSE to a
                    one-line summary and stay visible — never removed from the
                    DOM. Keep hidden station bodies in the DOM if a script
                    reads their fields by id: hiding preserves that state,
                    removing does not.
   ACCENT POLICY    Marker only: accent-blue for done, accent-orange for
                    current — the one thing needing action now. The spine
                    follows the markers. The card's top hairline was
                    accent-blue until 2026-09-23 and is now --border-strong: it
                    was identity, not state.
   -------------------------------------------------------------------------- */
.journey {
  position: relative;
  background: var(--panel);
  border-radius: 4px;
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* Hairline across the very top of the card — the one such line in the whole
   component, appears exactly once. Neutralised 2026-09-22 for the same
   reason as .control-rail's top border: it was a --accent-blue "brand
   accent" by its own description, which is identity/decoration rather than
   action or state. Journey pages are not usually colour-critical, so this is
   the consistency half of that change rather than the perceptual half. */
.journey::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  /* Governing rule 4: a decoration overlaying the first .station__head must
     not intercept its clicks. (2026-09-24) */
  pointer-events: none;
  background: var(--border-strong);
  opacity: 0.9;
}

.station {
  position: relative;
  border-top: 1px solid var(--border);
}
.station:first-of-type { border-top: none; }

/* Connecting progress spine — a 2px line down the marker gutter, joining
   each station's marker to the next. Markers paint over it via head
   z-index (below). Position math assumes 22px card padding + a 30px
   marker (half-marker = 15px): left = 22 + 15 = 37px — re-derive both
   numbers together if either changes. */
.station::before {
  content: '';
  /* Governing rule 4. The spine sits at left: 37px while .station__body
     indents to 70px, so today it overlays nothing interactive — but that
     safety depended on an indent nobody knew was load-bearing. Now it is
     unconditional. (2026-09-24) */
  pointer-events: none;
  position: absolute;
  left: 37px;
  top: 0;
  bottom: 0;
  width: 2px;
  transform: translateX(-50%);
  background: var(--border);
  z-index: 0;
}
.station:first-of-type::before { top: 31px; }
.station:last-of-type::before  { bottom: auto; height: 31px; }
.station[data-state="done"]::before { background: var(--accent-blue); }
.station[data-state="current"]::before {
  background: linear-gradient(180deg, var(--accent-blue) 0px, var(--accent-blue) 31px, var(--border) 72px);
}

/* "You are here" spotlight on the active station — a plain translucent
   white lift, no hue, so it works regardless of accent color. */
.station[data-state="current"] { background: rgba(255, 255, 255, 0.04); }

.station__head {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  align-items: center;
  column-gap: 14px;
  padding: 16px 22px;
  position: relative; /* lifts the head, including the marker, above the spine */
  z-index: 1;
}

.station__marker {
  width: 30px; height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-heading);
  font-size: 14px;
  font-weight: 700;
  flex-shrink: 0;
  border: 2px solid var(--border);
  color: var(--text-secondary);
  background: var(--panel);
  /* Ring in the panel's own color punches the marker out of the spine
     running behind it. */
  box-shadow: 0 0 0 4px var(--panel);
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.station[data-state="current"] .station__marker {
  background: var(--accent-orange);
  border-color: var(--accent-orange);
  color: var(--text-on-accent);
  /* Derived from the token, not hand-typed from its value — this halo was
     an rgba() of the pre-2026-09-07 orange and silently kept the old color
     through the remap. */
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.04),
              0 0 0 8px color-mix(in srgb, var(--accent-orange) 18%, transparent);
}
.station[data-state="done"] .station__marker {
  background: var(--accent-blue);
  border-color: var(--accent-blue);
  color: var(--text-on-accent);
  box-shadow: 0 0 0 4px var(--panel),
              0 0 0 7px color-mix(in srgb, var(--accent-blue) 12%, transparent);
  animation: station-pop 0.24s ease-out;
}
@keyframes station-pop {
  0% { transform: scale(0.7); }
  60% { transform: scale(1.08); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .station[data-state="done"] .station__marker { animation: none; }
}

.station__title {
  font-family: var(--font-heading);
  font-size: 16.5px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.25;
}
/* One-line recap shown once a station is done — never disappears, the
   trail behind the visitor stays visible. */
.station__summary {
  font-size: 13.5px;
  color: var(--text-secondary);
  margin-top: 1px;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
}

/* Only the current (or explicitly reopened) station shows its body. Keep
   station bodies in the DOM even while hidden if any form fields inside
   them hold live state a script reads by id — hiding, not removing,
   preserves that state across steps. */
.station__body {
  display: none;
  padding: 0 22px 22px 70px; /* left-indents to sit under the title, not the marker */
}
.station[data-state="current"] > .station__body,
.station[data-open="1"] > .station__body {
  display: block;
  animation: station-body-in 0.22s ease-out;
}
@keyframes station-body-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .station__body { animation: none; }
}

.station__lead {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-primary);
}
.station__help {
  margin: 0 0 16px;
  font-size: 14px;
  color: var(--text-secondary);
  max-width: 62ch;
}

/* ==========================================================================
   Snippet viewer — a copyable, multi-language code sample block with a
   language switcher. For any page that shows the real API call behind a
   setup step. Optional — only include if the page needs one.
   ==========================================================================

   Added 2026-09-04, extracted from captions-workflow's per-station "API
   request" disclosures (`.cw-code-block`/`.cw-langswitch`) — the reusable
   block/switcher/copy-button mechanism, not that page's own snippet
   content or language list.

   Syntax coloring is hand-rolled from this file's own tokens, not a new
   palette: keywords = blue, strings = --text-secondary, comments =
   --text-muted (italic), function/type names = --text-primary, numbers =
   orange, tags = orange-hover. That's deliberate — six visually distinct
   token roles built from exactly the two brand accents plus the existing
   text-color scale, with no new hue introduced for syntax highlighting.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .snippet / .lang-switch and their parts
   WHAT IT IS       A copyable, multi-language code sample with a language
                    switcher and hand-rolled syntax colouring.
   WHEN TO USE      Showing the real API call behind a setup step.
                    NOT for machine OUTPUT — that is .raw-payload. NOT for an
                    inline literal in a sentence — that is code/samp.
   MARKUP           <div class="lang-switch">
                      <button type="button" class="lang-switch__btn active">curl</button>
                      <button type="button" class="lang-switch__btn">JavaScript</button>
                    </div>
                    <div class="snippet" data-lang-count="2">
                      <div class="snippet__head">
                        <button type="button" class="snippet__copy" aria-label="Copy"> ... </button>
                      </div>
                      <pre data-lang="curl" class="is-shown"><code><span class="tok-kw">curl</span> ...</code></pre>
                    </div>
                    (verified example, from product-theme-moodboard.html)
   STATES           .lang-switch__btn uses .active — the file's ONE legacy
                    state hook, kept rather than migrated because changing it
                    means markup edits in every consumer. Do not copy the
                    pattern: new components use aria-pressed or aria-selected.
                    .snippet__copy has --copied.
   DOES NOT DO      Does not switch languages or copy anything — the page
                    toggles .is-shown and .active. Caps at 280px tall so
                    several open snippets stay a quiet side path; inside a
                    .detail-rail that cap is removed, since the rail is already
                    the one scroll container.
   ACCENT POLICY    Syntax colours are built from the EXISTING two accents plus
                    the text scale — six token roles, no new hue: keywords
                    blue, numbers orange, strings --text-secondary, comments
                    --text-muted italic, functions --text-primary, tags
                    orange-hover. The copy button takes blue on hover and when
                    copied — a positive confirmation.
   -------------------------------------------------------------------------- */
.snippet {
  position: relative;
  margin: 14px 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel-inset);
}
.snippet__head {
  display: flex;
  justify-content: flex-end;
  padding: 6px 8px;
  background: var(--panel-inset);
  border-bottom: 1px solid var(--border);
}
.snippet pre {
  margin: 0;
  padding: 16px 18px;
  background: transparent;
  overflow: auto;
  /* Caps how tall one snippet gets — several open at once must stay a
     quiet side path, never a wall of gray chrome competing with the
     page's primary content. Most snippets never hit this. */
  max-height: 280px;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.snippet pre::-webkit-scrollbar { width: 8px; height: 8px; }
.snippet pre::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
.snippet pre code {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-primary);
  white-space: pre;
}
.snippet__copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-secondary);
  padding: 6px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.snippet__copy:hover { color: var(--accent-blue-hover); border-color: var(--accent-blue); }
.snippet__copy--copied { color: var(--accent-blue); border-color: var(--accent-blue); }

/* One global language preference can drive every open snippet on the page
   at once — show only the panel matching the active language. */
.snippet[data-lang-count] pre[data-lang] { display: none; }
.snippet[data-lang-count] pre[data-lang].is-shown { display: block; }

.lang-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 14px;
  padding: 8px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.lang-switch__btn {
  /* Label centring — see note 10 at the top of this file. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  padding: 6px 14px;
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.lang-switch__btn:hover { color: var(--text-primary); background: var(--panel); }
.lang-switch__btn.active {
  background: var(--panel);
  border-color: var(--border);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

.snippet pre code .tok-kw  { color: var(--accent-blue); }
.snippet pre code .tok-str { color: var(--text-secondary); }
.snippet pre code .tok-com { color: var(--text-muted); font-style: italic; }
.snippet pre code .tok-fn  { color: var(--text-primary); }
.snippet pre code .tok-num { color: var(--accent-orange); }
.snippet pre code .tok-tag { color: var(--accent-orange-hover); }

/* ==========================================================================
   Detail rail — a sticky companion panel that appears beside the main
   content only on wide viewports, carrying reference material (e.g. the
   snippet viewer above) that would otherwise crowd a narrow-measure page.
   Optional, opt-in, wide-screen-only — the page must still work as a
   single column with this panel simply absent below the breakpoint; never
   make it the only way to reach its content.
   ==========================================================================

   Added 2026-09-04, extracted from captions-workflow's developer API rail
   — a real, load-bearing pattern there (documented at length in that
   repo's CLAUDE.md: it replaced five separate inline disclosures with one
   synced panel on wide screens). Generalized here: one clean 1280px
   breakpoint and a fixed 420px rail width, instead of that page's two-tier
   1600/1800px thresholds tuned to its own specific 1040px content column —
   re-derive both numbers for whatever column width the new page actually
   uses, the same way that page did for its own.

   Structural rules only carry over, not the multi-step-picker/role-chip
   specifics — pick a step/tab picker (.detail-rail__tabs below) sized to
   however many items the new page's rail actually needs.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — .detail-rail and its parts
   WHAT IT IS       A sticky companion panel beside the main content, on wide
                    viewports only.
   WHEN TO USE      Reference material that would crowd a narrow page — API
                    snippets synced to the step in view.
                    NOT for the controls that act on a preview — that is
                    .control-rail. NOT for anything essential: the page MUST
                    work with this panel absent.
   MARKUP           <aside class="detail-rail">
                      <div class="detail-rail__head">
                        <p class="detail-rail__kicker">API request</p>
                        <div class="detail-rail__tabs">
                          <button type="button" class="detail-rail__tab active">1</button>
                        </div>
                      </div>
                      <div class="detail-rail__body"> ... </div>
                    </aside>
                    (verified example, from product-theme-moodboard.html)
   STATES           .detail-rail__tab: default, hover, .active, :disabled. Uses
                    .active, the same legacy hook as .lang-switch__btn.
   DOES NOT DO      Displays NOTHING below 1280px — it is display: none, so
                    every piece of content in it must be reachable another way.
                    Its 420px width and 1280px breakpoint were derived for a
                    1040px content column; re-derive both for a different one.
                    NOTE: .detail-rail__tab is a STEP PICKER, not a tab strip —
                    it borrowed the word. For tabs use .btn-chip with
                    role="tab".
   ACCENT POLICY    The active step takes an accent-blue fill — state. The rail
                    itself carries none.
   -------------------------------------------------------------------------- */
.detail-rail { display: none; }

@media (min-width: 1280px) {
  .detail-rail {
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 84px; /* clear a sticky header of that height — adjust to match */
    max-height: calc(100vh - 84px - 24px);
    width: 420px;
    flex-shrink: 0;
    background: var(--panel-inset);
    border: 1px solid var(--border);
    border-radius: 4px;
    overflow: hidden;
  }
}

.detail-rail__head {
  flex: none; /* fixed band above the scrolling body — never scrolls away */
  padding: 14px 18px 12px;
  border-bottom: 1px solid var(--border);
}
.detail-rail__kicker {
  margin: 0;
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-primary);
}

.detail-rail__tabs { display: flex; gap: 4px; margin-top: 8px; }
.detail-rail__tab {
  /* Label centring — see note 10 at the top of this file. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.2;
  width: 24px;
  height: 24px;
  padding: 0;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.detail-rail__tab:hover:not(:disabled) { color: var(--accent-blue); border-color: var(--accent-blue); }
.detail-rail__tab.active {
  background: var(--accent-blue);
  border-color: var(--accent-blue);
  color: var(--text-on-accent);
}
.detail-rail__tab:disabled {
  color: var(--text-muted);
  background: var(--panel-inset);
  cursor: not-allowed;
  opacity: 0.7;
}

.detail-rail__body {
  padding: 0 18px 18px;
  /* The one scroll container in the rail: fills whatever height the head
     leaves, and is what actually scrolls when content exceeds the rail's
     viewport cap above. min-height: 0 is load-bearing — a flex item's
     default min-height:auto refuses to shrink below its content, which
     would silently defeat the cap. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.detail-rail__body::-webkit-scrollbar { width: 8px; }
.detail-rail__body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
/* Self-gating "more below" fade: transparent → panel-inset over a
   panel-inset background, so it's invisible until content actually passes
   under it. */
.detail-rail__body::after {
  content: '';
  display: block;
  position: sticky;
  bottom: 0;
  height: 26px;
  margin: -26px -18px 0;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--panel-inset));
}
/* No caption content ever needs a max-height inside the rail — the body
   above is the one scroll container; capping a nested element too would
   create a second, confusing scroller. */
.detail-rail .snippet pre { max-height: none; }

/* ==========================================================================
   TWO CORES: nanoStream and nanoStudio (2026-09-25)
   ==========================================================================

   This file styles product pages for both cores of the nanocosmos platform.
   The framing below is not invented here — it follows SKILL.md's naming
   rules and the nanostudio skill. Always in this order; reversing them is a
   naming violation:

     nanoStream .... the STREAMING core. Delivery, playback, ingest,
                     analytics, diagnostics. The page IS the product: the
                     visitor is watching, configuring or measuring a stream.
     nanoStudio .... the PRODUCTION core. Real-time chroma key, compositing,
                     colour correction, despill. The page is a TOOL for making
                     video, and what the operator is judging is inside the
                     video, not in the UI.

   Registered consumer pages, at 2026-09-25:
     nanoStudio .... chroma-setup-tuner, multi-chroma-key-player,
                     player-diagnostics
     nanoStream .... instant-test, live-recorder, multiplayer, vod-player,
                     analytics-tests, captions-workflow, token-revocation,
                     live-plus-replay, stream-switching-demo

   WHAT DIFFERS
   --------------------------------------------------------------------------
   Almost nothing, and that is deliberate. One stylesheet serves both; the
   difference is one opt-in scope, [data-color-critical], which neutralises
   the accent remaining in the zone around a preview. Six selectors, in five
   rules, out of this file. Everything else — every component, every token,
   the typography contract, the state vocabulary — is shared.

   That decision was made 2026-09-25 against the alternative of forking a
   nanoStudio layer above a shared base. The evidence was that nearly every
   change in the batch that prompted the question was fixing something broken
   on BOTH cores: the header misalignment, the OS-painted form controls, the
   select font, the modal [hidden] trap, the slider hit area. The clearest
   case was a latent checkbox bug that rendered an unchecked box in the
   visitor's OS colours — it affected player-diagnostics and the moodboard,
   and NEITHER is a chroma page. A fork would have put the boundary where the
   requirement does not follow, and you would still have needed the scope.

   WHY THE SCOPE EXISTS — do not remove this as over-engineering
   --------------------------------------------------------------------------
   On a chroma-key, despill or colour-balance page the operator is judging
   hue, spill and alpha edges in the video itself. Saturated chrome beside
   that preview shifts the judgement: simultaneous contrast is a real
   perceptual effect, which is why colour-grading tools sit in neutral
   surrounds. This is not taste. It is the same reason a photographer does not
   colour-match against a coloured wall.

   THE TRIGGER IS COLOUR-JUDGEMENT, NOT THE CORE
   --------------------------------------------------------------------------
   The core tells you which pages NORMALLY need the scope. It does not decide
   it. A nanoStudio analytics view judges no colour and does not need it. A
   chroma preview embedded in a nanoStream page does. Ask what the operator is
   looking at, not which core owns the page.

   The two cores are not separate worlds in any case: the nanostudio skill is
   explicit that nanoStudio outputs ARE regular nanoStream streams. The same
   stream is produced by one core and delivered by the other, and a page can
   sit on either side of that line.

   WHERE THE ATTRIBUTE GOES
   --------------------------------------------------------------------------
   On the container surrounding the preview AND its adjacent controls —
   typically .workbench-layout. Not on <body>: container-level is deliberate,
   so a single page can show a scoped and an unscoped rail side by side, which
   is the only way to judge whether the neutral treatment reads as neutral
   relative to anything.

   WHAT THE SCOPE CHANGES, AND WHAT IT DELIBERATELY DOES NOT
   --------------------------------------------------------------------------
   Changes: the monitor's frame drops to the darkest tier so no light ring
   competes with the edges being judged; selected chips, icon buttons,
   checkboxes and radios lose their accent border or fill. Nothing is resized —
   the scope removes accent, it does not change geometry. As of 2026-10-05
   nothing in this file resizes a control for a colour reason at all: .btn's
   tightening to 6px/15px is global and argued from density, so both modes
   render the same button at the same size.

   Does NOT change: .btn-primary (stripping accent from the control that
   commits to a live stream trades restraint for safety); the attention dot on
   .status-strip--attn and .toast--attn (~28px of orange, the smallest marker
   that still reads — removing it leaves the attention state with no
   distinction at all); focus and drag accents (transient, one control at a
   time, and they are how the tool tells you it is responding).

   See the [data-color-critical] section below for the rules themselves and
   for the scoping discipline that keeps this split honest.
   ========================================================================== */

/* ==========================================================================
   Colour-critical scope — [data-color-critical]
   ==========================================================================

   Added 2026-09-22. Opt-in, and deliberately CONTAINER-level rather than on
   <body> like data-wide-layout. The asymmetry is intentional: a page tuning
   this treatment has to render a normal rail and a colour-critical rail SIDE
   BY SIDE to judge whether the neutral version reads as neutral relative to
   something, and a body-level attribute makes that impossible on one page.
   Put the attribute on whatever element actually surrounds the preview and
   its adjacent controls — typically .workbench-layout, or a single rail.

   What it is for: on a chroma-key, despill or colour-balance page the
   operator judges hue, spill and alpha edges in the video itself. Saturated
   chrome beside the preview shifts that judgement (simultaneous contrast),
   which is why colour-grading tools sit in neutral surrounds.

   This scope is deliberately SMALL, because the defaults above already did
   most of the work: sliders, switches, value badges, icon buttons and colour
   swatches are all neutral at rest for every page, not just these ones.

   THE SCOPING DISCIPLINE (2026-09-25, the condition attached to keeping ONE
   stylesheet rather than forking a nanoStudio layer above a shared base):

     Any change justified SOLELY by colour-judgement reasoning belongs in
     this scope, not in the base rules.

     THE REVIEW TEST: would this change still be right on a log viewer?
     If no, it is scoped.

   This matters because the alternative was a second stylesheet, and the case
   against forking rested on the nanoStudio-specific surface staying small —
   7 rules out of ~3,800 lines at the time of the decision. That only stays
   true if colour-judgement changes are actually scoped rather than quietly
   shipped to every consumer. Two worked examples, both real, both from the
   batch that produced this rule:

     PASSES, stays global — .status-strip losing its permanent blue left edge.
     The colour-judgement argument was present but not load-bearing: it was a
     single-edge accent stripe, which this file forbids at the .field rule and
     had already corrected a consuming page for; and an always-blue status
     line spends accent on "nothing is happening", which contradicts the
     accent policy on any page. A log viewer's status strip is still right —
     arguably more right — without it. Not scoped.

     FAILS, scoped — .btn's padding cut from 10px/22px to 6px/15px. Its own
     comment gave it away: "the fill is the loudest saturated surface... a
     rail or a button row puts several of them BESIDE A PREVIEW". Nothing in
     that reasoning is about a log viewer, where there is no colour judgement
     to protect and a 40% smaller primary action is just a smaller button. A
     general density argument could have been constructed after the fact —
     which is exactly what this test is meant to catch. Moved here 2026-09-25,
     changing a page that had already been approved: the discipline has to
     cost something the first time or it is not one.

     POSTSCRIPT (2026-10-05): the scoped copy was then removed altogether. The
     test correctly said "not in the base rules", but the honest follow-up
     question was whether it belonged in the scope at all — and it did not.
     Every other rule here removes accent; that one only shrank an accent that
     stayed accent. THE DISCIPLINE TELLS YOU WHERE A CHANGE DOES NOT BELONG;
     IT DOES NOT BY ITSELF PROVE THE CHANGE BELONGS SOMEWHERE.

     CODA (2026-10-05, the same day): .btn then went to 6px/15px in the BASE
     rules, on a density argument — 10px/22px is a marketing-page button in a
     tool UI, which holds on a log viewer as firmly as beside a preview. Same
     value, different justification, different home, and this time it passes
     the review test rather than failing it.
     Read the three moves as one sequence, because that is where the lesson
     is. The discipline rejected a BAD JUSTIFICATION, not a bad value — it is
     a test of reasoning, and reasoning is the only thing it can test. So this
     is not the test being worked around: "it really is fine everywhere" is
     not an appeal, it is a different argument, and it has to be written down
     and survive the same question on its own terms. This one did. The first
     one did not. A discipline that could never let a rejected value back in
     under better reasoning would be a rule about values, which is not what
     this is.

   THREE THINGS ARE DELIBERATELY NOT HERE — don't add them back:

   1. A switch on-state override. Switches are neutral in BOTH states as of
      2026-09-22 (see the [role="switch"] rule), so there is nothing left to
      neutralize. An earlier draft of this scope carried one. The RADIO is
      different and IS handled below: it keeps an accent dot by default (it
      mirrors .checkbox-item's blue), so beside a preview there is something
      to take away — and the dot's presence carries the state without it.
   2. .status-strip--attn / .toast--attn. Their attention marker is now a 6px
      dot, not a 3px edge — roughly 28px of orange. Neutralizing it would
      leave the attention state with no visual distinction at all, which
      trades a real signal for a negligible perceptual gain.
   3. .btn-primary. Stripping the accent from the one control that commits a
      change to a live stream would be restraint bought at the cost of
      safety.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — [data-color-critical]
   WHAT IT IS       An opt-in scope that removes the remaining accent from the
                    zone around a preview.
   WHEN TO USE      On nanoStudio pages where the operator judges hue, spill,
                    alpha edges or colour balance IN THE VIDEO — chroma key,
                    despill, colour correction. Put it on the container
                    surrounding the preview and its adjacent controls,
                    typically .workbench-layout.
                    NOT on <body> — container-level is deliberate, so one page
                    can show a scoped and an unscoped rail side by side.
                    NOT on a page with no colour judgement: a nanoStudio
                    analytics view does not need it, and a chroma preview
                    embedded in a nanoStream page does. The trigger is
                    COLOUR-JUDGEMENT, not product line.
   MARKUP           <div class="workbench-layout" data-color-critical> ... </div>
                    (verified example, from tests/archetype-workbench.html)
   STATES           Present or absent.
   DOES NOT DO      Does not neutralise .btn-primary (stripping accent from the
                    control that commits to a live stream trades restraint for
                    safety), the attention dot on .status-strip--attn /
                    .toast--attn (~28px, the smallest marker that still reads),
                    or focus and drag accents (transient, one control at a
                    time, and they are how the tool says it is responding).
   ACCENT POLICY    This scope IS the accent policy at its strictest. See the
                    scoping discipline above: any change justified SOLELY by
                    colour-judgement reasoning belongs here, not in the base
                    rules — test it by asking whether it would still be right
                    on a log viewer.
   -------------------------------------------------------------------------- */

/* No light ring around the image being judged: the frame drops to the
   darkest surface so nothing competes with the video's own edges. */
[data-color-critical] .monitor {
  border-color: var(--panel-inset);
}

/* A checked box keeps its tick and a selected radio keeps its dot — presence
   vs absence is the state — but both drop the accent, the same trade the
   switch made by default. background-COLOR on the checkbox, not the
   shorthand, so the tick image survives. (2026-09-24) */
[data-color-critical] input[type="checkbox"]:not([role="switch"]):checked {
  background-color: var(--panel-soft);
  border-color: var(--border-strong);
}
[data-color-critical] input[type="radio"]:checked {
  border-color: var(--border-strong);
}
[data-color-critical] input[type="radio"]:checked::after {
  background: var(--text-primary);
}

/* Selection falls back to fill + aria-pressed alone.

   .color-swatch is deliberately NOT in this list (removed 2026-10-01). It was
   here from the start and did nothing: the DEFAULT selected swatch already
   uses --border-strong, so the scoped rule set the value it already had. The
   moodboard's own comparison showed the two swatches rendering identically,
   which is how it was caught. A rule that does nothing is worse than no rule,
   because the next person assumes it works and reasons from it.

   The swatch needs no scoping because it is already neutral AT REST — there
   was never accent to remove. Its contract explains why: the control is a
   neutral pill, and the only saturated thing on it is a 10px sample of the
   literal key colour, roughly 95% less surface than a full-size chip. AREA,
   not hue, is what is controlled there, and that holds on every page. */
[data-color-critical] .btn-chip[aria-pressed="true"],
[data-color-critical] .btn-icon[aria-pressed="true"] {
  border-color: var(--border-strong);
}

/* Focus and drag accents are NOT removed. They are transient, only ever on
   one control at a time, and they are the feedback that says the tool is
   responding to you. */

/* ==========================================================================
   Moodboard / documentation page styles
   ==========================================================================

   Presentation classes used only by diagnostics-theme-moodboard.html to
   showcase the tokens/components above (color swatches, gradient strip,
   type specimens, corner-radius comparison). Not part of the reusable
   component set — skip this section when lifting the theme into a new page.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTRACT — the .mb-* set (collective)
   WHAT IT IS       Presentation classes used ONLY by
                    product-theme-moodboard.html to display the theme — swatch
                    grids, gradient strips, type specimens, radius comparisons.
   WHEN TO USE      Never, on a product page. This section exists so the
                    moodboard can document the theme without a second
                    stylesheet. Building a page? Skip this section entirely
                    when lifting the theme.
   MARKUP           n/a — see product-theme-moodboard.html.
   STATES           None.
   DOES NOT DO      Is not a component set. .mb-swatch in particular is NOT the
                    colour control — that is .color-swatch. Copying .mb-* into
                    a product page is always a mistake.
   ACCENT POLICY    .mb-section-title uses accent-blue-hover as a heading
                    colour, which would violate the accent policy on a product
                    page. It is tolerated here because the moodboard is
                    documentation, not an interface. Do not take it as
                    precedent.
   -------------------------------------------------------------------------- */

/* Matches .header-inner/.footer-inner's width formula (2026-09-07) — this
   page's own header/footer already widened to the universal 1520px/48px
   standard, so a bare 1040px here would misalign the moodboard against its
   own chrome. See the note on .header-inner. */
.mb-container {
  width: min(calc(100% - (var(--page-margin) - 16px) * 2), calc(var(--page-max-width) + 32px));
  margin: 0 auto;
  padding: 40px 16px 60px;
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.mb-intro h1 {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 28px;
  margin: 0 0 8px;
  color: var(--text-primary);
}
.mb-intro p {
  max-width: 640px;
  color: var(--text-secondary);
  line-height: 1.6;
  margin: 0;
}

/* Caption under a section title explaining a demoed component's real
   behavior when the demo itself had to fake something (e.g. a fixed
   overlay shown static, in flow). Added 2026-09-04 for the journey/
   snippet/drawer/toast/detail-rail demos. */
.mb-pattern-note {
  max-width: 640px;
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1.55;
  margin: -4px 0 0;
}
.mb-pattern-note code {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

.mb-section {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.mb-section-title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-blue-hover);
  border-bottom: 1px solid var(--border);
  padding-bottom: 10px;
}

/* Color token swatches */
.mb-swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
}
.mb-swatch {
  border: 1px solid var(--border);
  border-radius: 4px;
  overflow: hidden;
  background: var(--panel);
}
.mb-swatch-color {
  height: 64px;
}
.mb-swatch-label {
  padding: 10px 12px;
  font-size: 12px;
}
.mb-swatch-name {
  display: block;
  color: var(--text-primary);
  font-weight: 600;
  margin-bottom: 2px;
}
.mb-swatch-hex {
  display: block;
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 11px;
}

/* Header gradient strip */
.mb-gradient-strip {
  height: 72px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: linear-gradient(180deg, #00486c 0%, #00486c 25%, #0f8dc8 100%);
}
.mb-gradient-caption {
  color: var(--text-secondary);
  font-size: 12px;
  font-family: var(--font-mono);
}

/* Type specimens */
.mb-type-row {
  display: flex;
  align-items: baseline;
  gap: 20px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.mb-type-row:last-child { border-bottom: none; }
.mb-type-meta {
  flex: 0 0 260px;
  color: var(--text-secondary);
  font-size: 12px;
  font-family: var(--font-mono);
  line-height: 1.6;
}
.mb-type-sample {
  color: var(--text-primary);
}
.mb-type-heading-bold {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 22px;
}
.mb-type-heading-regular {
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
.mb-type-body {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 15px;
}
.mb-type-mono {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 13px;
}

/* Component demo rows */
.mb-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}

.mb-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
@media (max-width: 640px) {
  .mb-two-col { grid-template-columns: 1fr; }
  .mb-type-row { flex-direction: column; gap: 6px; }
  .mb-type-meta { flex-basis: auto; }
}

/* Corner-radius comparison cards */
.mb-radius-sample {
  width: 100%;
  height: 90px;
  background: var(--panel);
  border: 1px solid var(--border);
}
.mb-radius-row {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}
.mb-radius-card {
  flex: 1;
  min-width: 140px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.mb-radius-caption {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
  text-align: center;
}
