/* base.css — reset, typography, rhythm.
 *
 * @docs ../../README.md
 *
 * Concatenated (with components.css, filename order) into public/site.css by
 * pipeline/emit.mjs. theme/tokens.css is NOT concatenated in — theme/head.html
 * links it, and it loads FIRST, so every token below is already in scope here.
 *
 * 🔴 NO LITERAL COLOURS BELOW THE FALLBACK BLOCK. design.md's rule: colour
 * comes from theme tokens, never hand-picked, so switching a theme repaints
 * everything with zero JS and zero edits here. A hex in a rule further down is
 * a bug, not a shortcut.
 */

/* ── The token contract, and the window before it exists ────────────────────
 *
 * These are the tokens src/css/ consumes. They are generated by the theme
 * build (theme/tokens.css) from the vendored ColorTheme documents; this block
 * is a floor for the window where a scaffolded site has run `npm run build`
 * but not yet `npm run tokens`, so the page reads as designed rather than as
 * unstyled black-on-white.
 *
 * 🔴 `:where()` is load-bearing, not decoration. It has ZERO specificity, so
 * theme/tokens.css's own `:root { … }` beats every line below without needing
 * !important, without depending on file order, and without this block ever
 * fighting a real theme. A plain `:root` here would silently override the
 * whole palette.
 *
 * The values are design.md's stated defaults: off-black on off-white, never
 * #000 or #fff, one muted accent held for emphasis.
 */
:where(:root) {
  --bg: #FBFAF8;
  --surface: #F4F2ED;
  --surface-lift: #EAE7E0;
  --border: #E0DCD3;
  --border-bright: #9A948A;
  --text-primary: #23211E;
  --text-bright: #121110;
  --text-secondary: #55514B;
  --text-muted: #6B665E;
  --accent: #7A5C3E;
  --accent-bright: #5E4630;
  /* The accent drawn as TYPE. theme/tokens.css floors this at 4.5:1 against the
     ground it is worst on; this fallback is measured 4.96:1 on the darkest
     surface above, so it is already the same colour as --accent and the split
     costs nothing here. It exists so a rule can NAME the text tier before a
     theme is built, instead of reaching for --accent and inheriting whatever
     contrast the next palette happens to have. */
  --accent-text: #7A5C3E;
  --link: #5E4630;
  /* ⚠️ NEAR-BLACK, AND IT USED TO BE NEAR-WHITE. Type on the accent GROUND —
     the red band, the primary button's fill. The old value was the page's
     off-white, which was correct for a muted brown accent and is an
     accessibility failure against the hot red this site actually uses:
     measured on #FF4F45, #FBFAF8 is 2.83:1 and even pure #fff only reaches
     3.25:1, under AA for anything but large text. #050505 measures 6.27:1 and
     clears both tiers. theme/tokens.css emits the real value; this floor is
     corrected so the pre-theme window is not a failing one. */
  --ink-on-accent: #050505;

  /* ── THE BAND GROUNDS (DESIGN.md "Field") ────────────────────────────────
   *
   * 🔴 THESE FOUR ARE NOT DEFINED HERE — THEY ARE FLOORED HERE. theme/ owns
   * the real values and theme/tokens.css states them at `:root`, which beats
   * every line in this block (that is what the `:where()` wrapper above is
   * for). The floor exists for the same window the rest of this block covers:
   * a tree where `npm run build` has run and `npm run tokens` has not. Without
   * it the home page renders eight bands with no grounds at all — eight
   * invisible sections, which looks like a layout bug rather than a missing
   * step.
   *
   * The values are DESIGN.md's own table, and --band-void is the one place on
   * this site where true #000000 is correct rather than sloppy: RecordExporter's
   * output was sampled at both corners and measures #000000 exactly, so a
   * near-black band renders the square reel as a visible lighter rectangle —
   * the "card" the whole system forbids. Chrome and reading bands still obey
   * the house off-black/off-white rule. Both are true at once.
   *
   * ⭐ FOUR GROUNDS SINCE 2026-09-25, NOT DESIGN.md's THREE — owner decision,
   * and DESIGN.md has not been updated (it is the owner's file). --band-carbon
   * is a SECOND DARK ground worn by the closing band and the footer, and it
   * does NOT contradict the paragraph above: that measurement is about a band
   * holding a BLACK-CORNERED RASTER — the square reel, a capture of a
   * pitch-black app — where anything other than #000000 draws the rectangle.
   * The closing band holds the wordmark, which is transparent art (its PNG
   * fallback measures alpha 1 at all four corners), so there is no square to
   * reveal. What made a second dark ground necessary is the opposite problem:
   * `sign` and `cta-final` were both true black, butted edge to edge, and read
   * as one long black run with no boundary at all — and neither could go light.
   * Because the bands are FULL-BLEED, a tonal step across the entire width
   * reads as a band change rather than as a card.
   *
   * 🔴 A DARK GROUND IS ALSO A POLARITY SCOPE. theme/tokens.css re-binds the
   * whole ink vocabulary inside `[data-band="carbon"]` / `.band-carbon` /
   * `.band--carbon`, exactly as it does for the void spellings, and
   * theme/build-tokens.mjs's DARK_BAND_SCOPES is the one place that list
   * lives. A dark band painted under a name that is not in that table gets the
   * LIGHT ladder on a dark field and no tool reports it.
   */
  --band-void: #000000;
  --band-carbon: #222220;
  --band-paper: #EFEFED;
  --band-steel: #C9C9C6;
  --ink: #111111;
  --tracking-display: -0.035em;
  --tracking-mono: 0.14em;

  --font-body: system-ui, -apple-system, "Helvetica Neue", sans-serif;
  --font-display: system-ui, -apple-system, "Helvetica Neue", sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, monospace;

  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2.5rem;
  --space-5: 4rem;
  --space-6: 6rem;

  --motion-duration: 160ms;
  --motion-ease: cubic-bezier(0, 0, 0.2, 1);
}

/* Type-led rhythm the theme has no opinion about. `--measure` is the reading
 * line-length ceiling — roughly 70 characters, the editorial register design.md
 * asks for (a well-set spread, not a full-bleed dashboard). */
:root {
  --measure: 46rem;
  --page: 72rem;

  /* ── THE CENTRED CONTENT COLUMN ───────────────────────────────────────────
   * 🔴 ONE CAP, TWO KINDS OF PAGE, AND THAT IS THE WHOLE POINT (owner,
   * 2026-09-30). This is the width `main` gives every reading page — /features,
   * /releases, /support and the three legal pages — AND the width the home
   * page's CONTAINED scenes are drawn inside (src/css/components.css, `.scene`).
   * They are the same number on purpose: the drawings and the prose stand in one
   * column, so moving between the home page and /features does not move the
   * page's centre line under the reader.
   *
   * ⚠️ IT IS NOT `--page`. That is the CHROME row cap — the width the header and
   * footer rows align their contents to — and it stays at 72rem so the nav and
   * the wordmark keep sitting near the viewport's edges, framing the column
   * rather than stacking on top of it. Two separate numbers because they answer
   * two separate questions; changing one to follow the other is how the frame
   * collapses onto the content.
   *
   * ⚠️ AND IT IS NOT UNIVERSAL ON THE HOME PAGE. Three scenes opt OUT via
   * `bleed: true` in content/artwork.json (share, wall, crowd) because they are
   * composed edge to edge. See layouts/scene-band.yml for the test.
   *
   * `box-sizing: border-box` is global (below), so this is the OUTER width and
   * `main`'s own --space-3 side padding is inside it, not added to it. */
  --content-max: 769px;

  --tracking-tight: -0.011em;

  /* ── band geometry ────────────────────────────────────────────────────────
   * Spacing, not colour, so it is authored here rather than taken from theme/.
   *
   * `--band-pad` is the ONE inset the whole chrome layer shares: the nav's
   * left edge, the mono plates in every band corner, the footer row. They have
   * to agree to the pixel, because a plate that does not line up with the nav
   * above it reads as a mistake rather than as a system — and in a layout with
   * no boxes and no rules, alignment is the only thing doing the work a border
   * would have done.
   *
   * `--band-air` is the vertical breathing room a band gives its one object.
   * Fluid on purpose: the bands are full-bleed, so the only thing setting the
   * page's rhythm is how much emptiness each one holds. */
  --band-pad: 20px;
  --band-air: clamp(60px, 10vw, 130px);

  /* ── THE HERO PEEK — 🔴 THE HERO IS NEVER 100vh, BY CONSTRUCTION ───────────
   *
   * Owner instruction, 2026-09-25: "the hero must NEVER be 100vh — the next
   * band must always be partially in view, so the page reads as continuing."
   * A band that exactly fills the viewport is indistinguishable from a page
   * that has nothing under it, and this one is a black field with a disc in
   * the middle and no headline, so there is not even a cut-off line of type to
   * imply more. The peek is the whole scroll affordance.
   *
   * The amount is chosen, not inherited: enough that the strip under the fold
   * is unmistakably A DIFFERENT BAND rather than a rendering artifact. The
   * hero is void and the band under it is `made`, which is --band-paper, so
   * what the visitor sees is a near-white band edge across the bottom of a
   * black screen. 80px at the floor is about four lines of the mono chrome
   * tier — a stripe nobody reads as a seam.
   *
   *   390 x 844   10vh =  84.4px   the phone floor, just over the 80px clamp
   *   834 x 1112  10vh = 111.2px
   *   1440 x 900  10vh =  90.0px
   *   short landscape (1024 x 640)  10vh = 64px → clamped up to 80px
   *
   * The clamp's CEILING matters as much as its floor: on a 1600px-tall
   * display an unclamped 10vh would hand 160px to a band the visitor has not
   * arrived at yet and take it off the object they have. 132px stops that.
   *
   * ⚠️ USED AS `100svh - var(--hero-peek)`, NOT `100vh - …`. On iOS Safari
   * `100vh` is the LARGEST viewport (toolbars retracted), so a `100vh - 80px`
   * hero is taller than the visible area while the toolbar is showing and the
   * peek disappears exactly when the visitor first looks at it. `svh` is the
   * smallest viewport, which is the one that is always actually visible; the
   * `vh` twin is kept as the fallback for engines without `svh`. */
  --hero-peek: clamp(80px, 10vh, 132px);

  /* The mono chrome tier — plates, nav, spec column, captions. One size, one
   * tracking, everywhere, per DESIGN.md's type spec ("Labels, captions, nav —
   * IBM Plex Mono, 11px, letter-spacing: 0.14em"). Named rather than repeated
   * so the twelve-odd selectors that draw a plate cannot drift apart.
   *
   * ⚠️ FLUID SINCE 2026-09-25, AND IT IS A DELIBERATE DEVIATION FROM DESIGN.md
   * — FLAGGED, NOT SNUCK IN. That file's type spec says a flat 11px. The owner
   * read the live site on a phone and the verdict was that the site's text is
   * too small on small devices; the instruction was a fluid scale with a RAISED
   * FLOOR, not a media query. 11px was the site's smallest tier and it is the
   * tier the whole chrome layer is built out of, so it is where the complaint
   * actually lives. DESIGN.md is the owner's file and is not edited here; this
   * comment is the flag site/CLAUDE.md asks for ("flag any code that does not
   * match it").
   *
   *   390px   0.6rem + 1.755px = 11.36px  → floored to 12px
   *   756px   0.6rem + 3.40px  = 13.00px  → the cap is reached here
   *   ≥756px                              → 13px
   *
   * So a phone reads 12px where it read 11px (+9%) and a desktop reads 13px
   * (+18%), with ONE expression and no breakpoint. The floor is the number
   * that matters: it is what a 390px phone gets.
   *
   * 🔴 THREE ARITHMETICS DOWNSTREAM READ THIS AND ALL THREE ARE EXPRESSED IN
   * TERMS OF IT, never in terms of 11: --frame-header-h (both branches, below)
   * and the wordmark's width ceiling. Changing the clamp changes the bar's
   * height and the point at which the nav wraps — re-derive, do not patch.
   *
   * ⚠️ EVERY `ch` CAP ON THIS SITE IS SCALE-INVARIANT AND THAT IS WHY THEY
   * SURVIVED THIS CHANGE UNTOUCHED. --say-cap (6.8ch) and .band-sub's 52ch are
   * measured in the width of the face's own "0", so raising the font-size
   * scales the cap by exactly the same factor as it scales the line it is
   * capping. Re-swept empirically after this change anyway, at 390 / 834 /
   * 1440: all four statements break where they are authored to. */
  --chrome-size: clamp(12px, 0.6rem + 0.45vw, 13px);

  /* ── THE NOTE ARC — the system's one technical mark ───────────────────────
   *
   * 🔴 THIS IS A SANCTIONED EXCEPTION TO DESIGN.md's "Shape", AND THE REASON
   * IT IS ALLOWED IS THE REASON IT MUST NOT SPREAD. That section retired "the
   * arc motif" — but the arc it retired was the WALL SHELF'S RAIL, borrowed as
   * a divider laid on top of a band edge, i.e. a decorative line the system
   * already had a better answer for. This is a different object: a ROUND-CAPPED
   * ARC SEGMENT is the app's own NOTE — `TracksScene.swift` draws every note on
   * the deck as exactly this shape, and assets/brand/riffmix.icon.svg's twelve
   * rainbow segments are twenty-four instances of it (measured in that file:
   * `stroke-linecap="round"` x 24). Owner-approved 2026-09-25 as the site's
   * recurring technical mark, replacing plain rules in three places and NOWHERE
   * ELSE: the band seam, the nav's current-page indicator, and the list marker
   * on the legal pages. It is a MARK, not ornament — the owner explicitly
   * declined drips, speckle and groove textures in the same conversation, and
   * DESIGN.md's "a motif used twice becomes a template motif" still governs
   * everything that is not this one shape.
   *
   * ── WHY A MASK AND NOT AN <img> ──────────────────────────────────────────
   * An image asset carries its own colour and cannot invert. This site's whole
   * text vocabulary RE-BINDS inside `[data-band="void"]` / `.band-void` /
   * `.band--void` (theme/tokens.css), so a mark that does not read
   * `currentColor` renders black-on-black on half the home page. `mask-image`
   * + `background: currentColor` is the only CSS shape primitive that gives a
   * round cap AND inherits the band's polarity for free. The colour below the
   * mask is a token at every use site; the mask itself carries no colour at
   * all (the `%23fff` stroke is an ALPHA carrier — mask-mode resolves to
   * `alpha` for an image source, so the value is irrelevant, and white is the
   * safe choice in case an engine ever resolves it as luminance instead).
   *
   * ── THE GEOMETRY IS EXACT, AND IT IS THE ICON'S OWN ──────────────────────
   * Chord 20, radius 50.5 → sagitta = 50.5 - sqrt(50.5² - 10²) = 50.5 - 49.5 =
   * 1.0 exactly (2450.25 is 49.5², so no rounding anywhere). Sweep is
   * 2·asin(10/50.5) = 22.8°, against the icon's own outer segments at 21.0° —
   * the same shallowness, so the mark reads as a note and not as a smile.
   * Stroke 2 in a 22-wide box is 1:11, against the icon's 1:5.9 — thinner,
   * because the icon's segment is an object and this is chrome.
   *
   * With `stroke-width: 2` and round caps the ink's bounding box is
   * x [0.02, 21.98], y [0, 3] — so `viewBox="0 0 22 3"` is tight to the mark
   * on all four sides and `mask-size: 100% 100%` on a box of the SAME 22/3
   * ratio scales it with no letterboxing and no distortion. 🔴 Change the
   * stroke or the radius and the viewBox has to be re-derived, or the caps
   * clip.
   *
   * ── ONE SHAPE, THREE SIZES, AND WIDTH IS THE ONLY DIAL ───────────────────
   * Every use sets WIDTH; `aspect-ratio: 22 / 3` derives the height and the
   * mask derives the stroke. Rendered stroke = width / 11, which is the number
   * to check when adding a size: under ~1px it fringes into grey, over ~3px it
   * stops being chrome.
   *
   *   seam  28px → 3.82px tall, 2.55px stroke   a stamp on a full-bleed field
   *   nav   18px → 2.45px tall, 1.64px stroke   under an 11px mono label
   *   list  13px → 1.77px tall, 1.18px stroke   beside 16px body prose
   */
  --arc-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 3'%3E%3Cpath d='M1 2A50.5 50.5 0 0 1 21 2' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  --arc-ratio: 22 / 3;
  --arc-w-seam: 28px;
  --arc-w-nav: 18px;
  --arc-w-list: 13px;

  /* ── THE NAV WORDMARK'S HEIGHT ────────────────────────────────────────────
   *
   * ⭐ 11px → 30px, 2026-09-25, OWNER INSTRUCTION FROM READING THE LIVE SITE:
   * "the nav wordmark is too small to read — make it legible."
   *
   * ── WHY 11px WAS ILLEGIBLE, MEASURED ───────────────────────────────────────
   * assets/brand/riffmix.wordmark.svg is `viewBox="0 0 640 360"` (7:3.94) with
   * its ink filling 605.5 x 318.9 of that box — so at `height: 11px` the mark
   * rendered 19.55 x 11 CSS px and the WORD "Riffmix" was 18.5px wide: 2.6px
   * per letter. The drawing's finest features — the drips, the flicks, the
   * speckle — are 4-6 units in 640-space, i.e. 0.12-0.18 CSS px at that scale,
   * an order of magnitude under the ~0.7-1.3px floor where a feature stops
   * resolving at all. It was not small type, it was a coloured smudge.
   *
   * ── WHY 30px, AND NOT MORE ─────────────────────────────────────────────────
   * At 30px the mark is 53.33 x 30 and the word is 50.5px wide: 7.2px per
   * letter, which is the same per-character advance as the mono nav links
   * beside it (--chrome-size x 0.74em/char), and the letterform strokes land
   * at ~1.7px — clear of the pixel floor. Rendered and read at 11/16/18/20/22/
   * 24/26/30/34/40px on all four grounds before choosing: the word starts
   * reading at 26 and is unambiguous at 30. FULL spray detail would need
   * ~56px (a 5-unit feature only reaches 0.78 CSS px there), and that is a
   * banner, not chrome — DESIGN.md's restraint rule is what stops this at the
   * first size that is legible rather than the size that is faithful.
   *
   * ── 🔴 THE OLD 11.88px BASELINE CEILING IS RETIRED, NOT CROSSED ───────────
   * It said: stay under 11.88px or --frame-header-h under-measures the bar and
   * every `#` anchor lands behind it. 11.88px was `half-leading + ascender` for
   * an 11px mono link in a BASELINE-ALIGNED row, and the ceiling existed
   * because a taller image would out-reach it and start setting the row's
   * height. Both halves of that are gone: the row is `align-items: center` now
   * (components.css argues why — a spray lockup drawn on a diagonal has no
   * baseline to align to, and the ascent metric made the sum unmeasurable) and
   * --frame-header-h is a plain `max()` of two boxes. THERE IS NO HEIGHT AT
   * WHICH THIS TOKEN BREAKS THE ANCHORS ANY MORE — verified by measurement at
   * 320 / 390 / 834 / 1440: the token and the rendered bar agree to 0.02px at
   * every one.
   *
   * 🔴 THE SECOND CEILING, ON WIDTH, IS STILL REAL AND IS RE-DERIVED:
   * W < 68px, i.e. THIS TOKEN < 38.2px at the asset's 640:360 box. Only HEIGHT
   * is set in CSS — the width comes from the asset's own aspect ratio, which is
   * data this file must not guess at — but the width still lands in an
   * arithmetic this file owns. The bar wraps when `2 x --band-pad + wordmark +
   * gap + nav` exceeds the viewport, and the nav grew with --chrome-size, so
   * the old `299.24 + W` is dead. Measured at the C = 12 phone floor: the
   * three-item nav is 252px, so the wrap point is `40 + W + 24 + 252` =
   * `316 + W`. At W = 53.33px that is 369.3px — and swept against the live page
   * in 2-5px steps, the nav wraps between 370 and 368. The narrow-screen block
   * below assumes the wrap lands under 24rem (384px), which holds while
   * W < 68px.
   *
   * ⚠️ RE-MEASURE IF THE ASSET IS REDRAWN OR A FOURTH NAV ITEM LANDS. Past the
   * ceiling the bar wraps somewhere ABOVE 384px while --frame-header-h still
   * reports the one-line height, and in that window the bar occludes the top of
   * every reading page and every `#` anchor. That is the harmful direction of
   * being wrong, which is why it is written down rather than left to be
   * noticed. (Between the real 369px and the 384px boundary the token
   * over-estimates by 36px instead — air at the top of a page, which is the
   * direction this file has always chosen to be wrong in.)
   *
   * ⚠️ THE BAR IS A FIXED, UNPAINTED FLYOVER SINCE 2026-09-25, so this height
   * no longer costs the page any layout: bands do not reserve it. What it
   * still drives is --frame-header-h, and --frame-header-h still drives
   * `scroll-padding-top`, the reel's pause control, and the reading pages'
   * top inset. A taller mark is cheap here; it was not before. */
  --wordmark-nav-h: 30px;
}

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

html { -webkit-text-size-adjust: 100%; }

/* THE GROUND IS A READING BAND, NOT A PAGE COLOUR.
 *
 * On the home page this is never seen: eight full-bleed bands paint over it
 * edge to edge, top to bottom. It is what /features, /releases, /support and
 * the legal floor stand on — and DESIGN.md's table names exactly one ground
 * for that job ("--band-paper: the reading band. Statements, releases,
 * legal."). Taking it from the band palette rather than from --bg is what
 * stops the site having two different light grounds: one for bands and one
 * for everything else, differing by a few points of warmth and looking like a
 * rendering fault where they meet.
 *
 * Measured, 2026-09-25: --ink #111 on --band-paper #EFEFED is 16.5:1. */
/* 🔴 A BAND PAGE IS DARK ALL THE WAY OUT TO THE BROWSER CHROME.
 *
 * theme/tokens.css sets `color-scheme: light` on :root — correct for the
 * site's base, which is off-white paper, and wrong for a page made entirely of
 * black bands. iOS Safari tints its top and bottom bars from `color-scheme`,
 * so the home page rendered as a black field between two white bars.
 *
 * ⚠️ `theme-color: #000000` was already set and did NOT fix it — `color-scheme`
 * takes precedence for the chrome, and both have to agree. Setting it here
 * rather than editing tokens.css is deliberate: that file is GENERATED by
 * theme/build-tokens.mjs and a hand edit there is lost on the next `npm run
 * tokens`. src/css is the authored layer, so this is where an override lives.
 *
 * The backgrounds matter as much as the scheme: html is what shows in the
 * OVERSCROLL rubber-band past either end of the page, and body is what shows
 * anywhere a band does not paint. Both go dark so there is no white flash when
 * the page is dragged past its own end.
 *
 * Scoped to the attribute, so reading pages keep light chrome — they really
 * are off-white and dark bars there would be the same mistake inverted. */
html[data-ground="dark"] {
  color-scheme: dark;
  background: var(--band-void);
}
html[data-ground="dark"] body { background: var(--band-void); }

body {
  margin: 0;
  background: var(--band-paper);
  color: var(--ink);
  font-family: var(--font-body);
  /* Fluid, floored at 17px — see the type-scale note above. It was a flat
     1rem, which on a phone is 16px and is the browser default nobody chose.
     The ceiling is 1.1rem so a desktop reading column gains a point rather
     than a size. */
  font-size: clamp(1.0625rem, 1rem + 0.25vw, 1.1rem);
  line-height: 1.6;
  letter-spacing: var(--tracking-tight);
}

/* Tighter than the browser default on purpose: loose-tracked type reads as
 * decorative, tight reads as confident (design.md). */
h1, h2, h3 {
  font-family: var(--font-display);
  color: var(--text-bright);
  letter-spacing: var(--tracking-tight);
  line-height: 1.15;
  text-wrap: balance;
  margin: 0 0 var(--space-2);
}

/* ── THE FLUID TYPE SCALE, AND WHAT THE OWNER ACTUALLY ASKED FOR ────────────
 *
 * ⚠️ "PORT TOUCHTONE'S CLAMPS" WAS ALREADY DONE — THE DELTA IS THE FLOOR.
 * The instruction that produced this block named touchtone's
 * `h1 { clamp(2rem, 5vw, 3.25rem) }` as the reference, and that expression was
 * already here character for character (so is its bands.css statement clamp,
 * over on .band-say). Porting it again would have changed nothing and shipped
 * as "done". The actual complaint — read off a phone — is that the text is too
 * SMALL ON SMALL DEVICES, and on every one of these clamps the small-device
 * value is the FLOOR, not the vw term:
 *
 *     h1  clamp(2rem, 5vw,   3.25rem)   the 2rem floor binds below 640px
 *     h2  clamp(1.5rem, 3.5vw, 2.25rem) the 1.5rem floor binds below 686px
 *
 * So the fix is to raise the floors and leave the vw slope and the ceilings
 * alone: a desktop reader sees exactly the page they saw before, and a 390px
 * phone gets h1 at 40px instead of 32px (+25%) and h2 at 28.8px instead of
 * 24px (+20%). No media query, which is the half of the instruction that was
 * explicit. The ceilings are untouched, so the scale still converges on the
 * approved desktop rendering rather than inflating the whole site.
 *
 * The crossover points move with the floors and that is the intended effect —
 * the raised floor simply holds for longer:
 *
 *     h1  2.5rem  binds below 800px, then 5vw takes over up to the 3.25rem cap
 *     h2  1.8rem  binds below 823px, then 3.5vw up to the 2.25rem cap
 *
 * h3 and body were FLAT and are now fluid for the same reason: a flat size is
 * a floor that never lifts. Body copy gains a 17px floor (from 16) and settles
 * at 17.6px — this site's reading pages are legal text and release notes, the
 * two places where a phone reader is least forgiving. */
h1 { font-size: clamp(2.5rem, 5vw, 3.25rem); }
h2 { font-size: clamp(1.8rem, 3.5vw, 2.25rem); }
h3 { font-size: clamp(1.2rem, 0.9rem + 0.8vw, 1.375rem); }

p { margin: 0 0 var(--space-2); max-width: var(--measure); }

a {
  color: var(--link);
  text-decoration-color: var(--border-bright);
  text-underline-offset: 0.15em;
}
a:hover { text-decoration-color: currentColor; }

img { display: block; max-width: 100%; }

/* WCAG 2.1 AA (EN 301 549 / BFSG) is a launch blocker, not a preference. A
 * visible focus ring on EVERY interactive element, verified with tooling rather
 * than eyeballed.
 *
 * ⚠️ --accent-text, NOT --accent. 1.4.11 asks a focus indicator for 3:1 against
 * ITS OWN adjacent ground, and the ring lands on more than one: measured
 * 2026-09-25 on the teenager pole, #F0501E is 3.01:1 on --bg but 2.73:1 on
 * --surface — so focusing a button inside .cta-final (a --surface panel) drew a
 * ring that FAILED, while the identical ring on the page ground scraped past.
 * The text tier is floored at 4.5:1 against the ground it is worst on, which
 * clears 1.4.11 on every surface in the ladder with room to spare, and keeps
 * the ring in the brand hue — --border-bright would have cleared it too but
 * repaints the ring a neutral the page uses for rules. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Screen-reader-only text — content a sighted reader doesn't need repeated
 * (a <fieldset>'s <legend> here duplicates the section's own visible <h2>)
 * but an accessibility-tree walk still needs. Clipped rather than
 * `display: none`, which would drop it from that tree entirely. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  z-index: 100;
  background: var(--surface);
  color: var(--text-bright);
  padding: var(--space-1) var(--space-2);
}
.skip-link:focus {
  left: var(--space-2);
  top: var(--space-2);
}

/* The skip link's destination. `<main id="main" tabindex="-1">` is what makes
 * the jump actually MOVE FOCUS rather than only move the viewport — without
 * it, `document.activeElement` was still BODY after activating the link and
 * the next Tab restarted from the top of the page. Chrome's sequential-focus
 * navigation starting point papers over it; older WebKit does not, and this is
 * an iOS app's marketing site.
 *
 * No focus ring on the landmark itself: -1 means it is not in the tab order,
 * so 2.4.7 (which governs keyboard-OPERABLE elements) has nothing to ask for
 * here, and a 1152px-wide outline around the whole page reads as a rendering
 * fault rather than as a cue. The <h1> arriving at the top of the viewport is
 * the visible confirmation. */
main[tabindex="-1"]:focus { outline: none; }

/* ── page frame ─────────────────────────────────────────────────────────── */

/* The footer floor (wireframe.yml `frame.footer: sticky`, the default — the
 * shell stamps it as data-frame-footer). This is the LAYOUT sense of "sticky
 * footer", not position:sticky: a min-100vh flex column floats a SHORT page's
 * footer to the bottom of the viewport instead of leaving it stranded
 * mid-screen, while a long page's footer still scrolls in after the content.
 * main takes the slack. Auto side margins on main keep it centred inside the
 * flex column (they win over the default stretch, so max-width still holds).
 * `frame.footer: static` opts out — the footer just follows the content. */
body[data-frame-footer="sticky"] {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
body[data-frame-footer="sticky"] > main { flex: 1 0 auto; }

/* ⚠️ THE CROSS-AXIS TRAP BITES EVERY CHILD, NOT JUST `main`.
 *
 * The rule above makes `body` a COLUMN flex container, so WIDTH is the cross
 * axis for all three of its children. `main` states `width: 100%` and carries
 * the long comment below explaining why; `.site-header` and `.site-footer`
 * never got the same treatment and so resolved their width against their own
 * CONTENT. Measured 2026-09-25, identical on all seven pages: header 360.7px
 * and footer 557.8px at EVERY viewport (390 / 834 / 1440) while `main` was
 * 390 / 834 / 1152.
 *
 * It cost two separate things, which is why the fix is here and not a nudge in
 * components.css:
 *
 *   1. ALIGNMENT. `margin: 0 auto` centred those shrink-wrapped boxes, so at
 *      1440 the wordmark sat at x≈564 while the <h1> it is supposed to line up
 *      with sat at x=168. `.nav { flex: 1 }` is the giveaway that the intended
 *      shape was wordmark-left / nav-right all along.
 *   2. THE STICKY BAR'S OPACITY. components.css paints `.site-header` with
 *      `background: var(--bg)` so bands scrolling under it stay legible — but
 *      a 360.7px background cannot cover a bar it does not span. Scrolling the
 *      pitch-black showcase reel under it showed a cream rectangle floating on
 *      black with video visible either side.
 *
 * Stating the width resolves the cross axis against the containing block, and
 * both bars span the viewport. Kept unconditional rather than scoped to
 * `[data-frame-footer="sticky"]`: with `frame.footer: static` the body is not
 * a flex container and a block-level `width: 100%` is what these two already
 * resolve to, so the rule is a no-op there by construction rather than by a
 * second selector.
 *
 * ✅ AND THE FULL-BLEED CONSEQUENCE THIS COMMENT PREDICTED IS NOW REAL.
 * The paragraph above used to end: "A future FULL-BLEED band would break that
 * and would need this bar split into a full-width painted shell around a
 * --page content row." That future arrived with the band redesign — the home
 * page is eight bands that run edge to edge — so THE SPLIT IS DONE, in
 * src/html/_header.html and src/html/_footer.html, and the cap moved off these
 * two elements onto `.site-header-row` / `.site-footer-row` (components.css).
 * `max-width` is deliberately NOT restated here: the shell's whole job is to
 * span the viewport, and a cap on it is the defect, not the feature.
 *
 * ⚠️ THE HEADER LEFT THE FLEX COLUMN ON 2026-09-25 (it is `position: fixed`
 * now — components.css) so the cross-axis trap above no longer reaches it; its
 * width comes from `left: 0; right: 0`. This declaration is kept for it anyway
 * because it agrees with that inset rather than fighting it, and because the
 * FOOTER is still an in-flow flex item and still needs exactly this line. One
 * rule, one thing it says: both bars span the viewport. */
.site-header,
.site-footer { width: 100%; }

/* ── in-page anchors, and the bar they have to clear ─────────────────────── */

/* The header's own height, written as the header's OWN ARITHMETIC rather than
 * as a measurement. `.site-header-row` is `padding: 14px var(--band-pad)`
 * around a CENTRE-ALIGNED row holding two boxes — the wordmark image and the
 * mono nav — so the row's content height is simply the taller of the two, and
 * each is written in terms of --chrome-size (C) rather than as a number:
 *
 *     the mark   --wordmark-nav-h
 *     a .nav a   C x 1.6 (its line box) + 2px (the padding-bottom that holds
 *                the current-page arc off the descenders)
 *
 * ⭐ RE-DERIVED 2026-09-25 — TWICE OVER, AND EVERY TERM IS NEW. The old sum
 * was `2 x 14 + 11 x 1.6 + 2` = 47.6px, which silently assumed the mono links
 * were always the tallest thing in the row. Two owner changes broke that
 * assumption in the same pass: --chrome-size became fluid (12-13px, so "11" is
 * no longer a number this file may write down) and --wordmark-nav-h went from
 * 11px to 30px, so the IMAGE sets the row's height now, not the links. Hence
 * the `max()` — which the old note predicted would be needed.
 *
 * 🔴 AND THE ROW STOPPED BEING BASELINE-ALIGNED IN THE SAME PASS, WHICH IS
 * WHAT MAKES THIS SUM EXACT INSTEAD OF APPROXIMATE. A baseline-aligned row's
 * height contains the FONT'S ASCENT, and that is not a number a stylesheet can
 * know: the first attempt at this used the ~0.78em the old note quoted, landed
 * dead on at 13px, and was 1.04px WRONG at 12px — it computed 66.24 for a bar
 * that measured 65.19. Centring removes the metric from the arithmetic
 * altogether. components.css carries the other half of that argument (a spray
 * wordmark drawn on a diagonal has no baseline to align to in the first
 * place).
 *
 * Checked the way the old one was: with C = 11 and an 11px mark this computes
 * 28 + max(11, 19.6) = 47.60px, bit for bit the value it replaces. With
 * today's tokens it computes 58px — and it MEASURES 58px, at every viewport,
 * because there is nothing left in it to be approximately right about.
 *
 * The 14px is stated as a literal here and in components.css because it is
 * smaller than --space-1 and inventing a --space-0 to hold one value used
 * twice would be worse; the two must move together.
 *
 * ⚠️ THE BAR IS `position: fixed` SINCE 2026-09-25 AND THAT MADE THIS TOKEN
 * MORE LOAD-BEARING, NOT LESS. In flow, a wrong value only mis-landed `#`
 * anchors. Out of flow, the bar reserves NO height at all, so this number is
 * also the top inset the reading pages' `main` pays (below) and the offset
 * the reel's pause control clears the bar by (components.css). Under-measure
 * it and content starts underneath the nav on every page, not just at an
 * anchor.
 *
 * 🔴 WHY THIS IS CSS AND NOT A RESIZE OBSERVER. A fragment link is plain
 * static markup and it has to land correctly with no JavaScript at all — a
 * script-set custom property would fix the measurement and forfeit the
 * guarantee. */
:root {
  --frame-header-h: calc(
    14px * 2
    + max(var(--wordmark-nav-h), var(--chrome-size) * 1.6 + 2px)
  );
}

/* ⚠️ NARROW SCREENS: A DELIBERATE OVER-ESTIMATE, AND WHY IT CANNOT BE EXACT.
 *
 * The bar gets taller when `.nav`'s links wrap to a second line, taking the
 * row from one line of the mono chrome tier to two plus a --space-3 row gap.
 * The row gap is --space-3 because that is `.site-header-row`'s own `gap`,
 * which governs the wrapped rows; an earlier pass used --space-1 here and
 * under-measured the wrapped bar by 16px.
 *
 * ⭐ RE-DERIVED 2026-09-25 alongside the one-line branch, and it is the same
 * two-box `max()`. `.nav` is a single flex ITEM of the header row, so it is the
 * nav that wraps internally, not the row; the nav's box simply becomes two
 * lines plus its own row gap, and the centred row is still the taller of the
 * two boxes:
 *
 *     max( the mark ,  2 x (C x 1.6 + 2px) + --space-3 )
 *
 * Checked the same way: at C = 11 with an 11px mark this computes
 * 28 + max(11, 63.2) = 91.20px, exactly the value it replaces. With today's
 * tokens, at the C = 12 phone floor and a 30px mark, 28 + max(30, 66.4) =
 * 94.40px — and it measures 94.4 at 320 x 568.
 *
 * Where it starts is NOT a fixed number, and no CSS can make it one. The
 * threshold is `2 x pad + wordmark + gaps + nav`, and two of those terms are
 * not ours to pin: the nav's labels come from wireframe.yml's `nav:` registry
 * (add "Pricing" and the wrap point moves), and --font-mono NAMES families and
 * ships no files (theme/tokens.css), so the visitor's own fallback decides the
 * glyph width. Estimated for the shipped registry at IBM Plex Mono's ~0.6em
 * advance plus --tracking-mono: 30 characters plus three gaps plus the inset
 * lands the wrap near 342px.
 *
 * So the boundary stays GENEROUSLY at 24rem (384px) rather than tightly at the
 * estimated 342px, and the trade is the one this block has always made:
 * between ~342 and 384px this over-estimates the bar, which lands an anchored
 * heading lower than it needs to be — air, not occlusion. Under-estimating
 * puts the heading back behind the bar, which is the defect this whole block
 * exists to fix. Room to be wrong in the harmless direction is worth more than
 * polish on one band of phone widths.
 *
 * ⚠️ The chrome shrank a lot in the band redesign (an 18px wordmark bar became
 * an 11px mono rail), so the wrap threshold moved down with it and this query
 * now fires far less often than it used to. Kept, not deleted: the registry
 * can grow a fifth nav item tomorrow.
 *
 * ⚠️ THE WORDMARK IS NOW AN IMAGE AND IT IS THE THIRD TERM NOBODY CONTROLS.
 * The paragraph above names two — the nav registry and the visitor's own mono
 * fallback font — and a brand asset is a third: its WIDTH comes from the file,
 * not from this stylesheet. The threshold is `299.24 + wordmark width`, so
 * this 24rem boundary holds while the mark is under ~84.7px wide. The full
 * arithmetic and the reason it matters are beside --wordmark-nav-h above;
 * re-measure it when the asset changes. Verified 2026-09-25 at 320 x 568: the
 * bar wraps, measures 91.19px, and this block's sum computes 91.2px. */
@media (max-width: 24rem) {
  :root {
    --frame-header-h: calc(
      14px * 2
      + max(var(--wordmark-nav-h), (var(--chrome-size) * 1.6 + 2px) * 2 + var(--space-3))
    );
  }
}

/* Clicking "See how it works" scrolled `#how` to y=0 and parked the heading
 * entirely behind the bar (measured: .steps-title top 0.1px, header bottom
 * 76.8px). `scroll-padding-top` on the SCROLL CONTAINER is the fix — the
 * document's scroller is the root element, not <body>, which is why the
 * attribute the frame is stamped on has to be reached with `:has()` rather
 * than styled directly. Every fragment target on the site inherits it; nothing
 * has to remember to carry `scroll-margin-top` itself.
 *
 * `frame.header: static` opts out and gets no offset, because there is no bar
 * in the way. */
html:has(body[data-frame-header="sticky"]) {
  scroll-padding-top: calc(var(--frame-header-h) + var(--space-2));
}

/* 🔴 THE TOP PADDING IS THE PRICE OF THE FIXED BAR, AND IT IS NOT OPTIONAL.
 *
 * The header was `position: sticky` until 2026-09-25, i.e. IN FLOW: it
 * occupied its own height at the top of the document and every page started
 * underneath it for free. It is now `position: fixed` (components.css — the
 * transparent flyover), which takes it out of flow entirely, so without this
 * inset the first line of every reading page renders BEHIND the nav. Measured
 * before adding it: `/features`'s <h1> box began at y=0 with the bar occupying
 * y 0-66.76.
 *
 * It is stated on `main` rather than on `body` because the two page shapes
 * want opposite things and `main` is where they already diverge: a reading
 * page must clear the bar, and a BAND page must not (the hero is a black field
 * the bar is supposed to fly over — that is the whole point of the change).
 * `main:has(.band)` below zeroes it back out, which is the same selector, the
 * same reason and the same failure mode as the full-bleed opt-out it lives in.
 *
 * `--frame-header-h` is the honest measure of the bar and is re-derived
 * above; nothing here is a magic number. */
main {
  /* The centred content column — see --content-max. Was `--page` (72rem/1152px)
     until 2026-09-30; narrowed so the reading pages share one measure with the
     home page's contained scenes. */
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--frame-header-h) var(--space-3) var(--space-6);
  /* ⚠️ `width: 100%` is load-bearing, not redundant with max-width.
   *
   * The rule two lines above makes `body` a COLUMN flex container, so `main`
   * is a flex item whose WIDTH is the CROSS axis. Cross-axis size is resolved
   * by `align-items: stretch`, which will not shrink an item below its
   * min-content — and `flex-shrink` / `min-width: 0` govern the MAIN axis, so
   * neither one helps here. Any descendant with a hard `min-width` (the
   * /privacy data table's `min-width: 34rem`) therefore became a min-content
   * floor for `main` and pushed the ENTIRE PAGE wider than the viewport:
   * measured `body.scrollWidth` 592px inside a 390px viewport, with body text
   * and the nav clipping. Stating an explicit width resolves the cross-axis
   * against the containing block instead of against content, so a too-wide
   * descendant scrolls inside its own container rather than dragging the page.
   *
   * Verified empirically at 390/480px — `min-width: 0` and `flex-shrink: 1`
   * were both tested first and neither changed the measurement. */
  width: 100%;
}

/* THE FULL-BLEED OPT-OUT, and why it is `:has()` rather than a page flag.
 *
 * `main` above is the capped reading column every page has always used, and
 * /features, /releases, /support and the three legal pages still want exactly
 * that. The home page does not: it is a stack of bands that must touch the
 * viewport's edges, and a 1152px cap with --space-3 of side padding would
 * have inset every one of them — turning eight flat fields into eight centred
 * cards, which is the single thing DESIGN.md's "Field" section forbids.
 *
 * The condition is THE PRESENCE OF A BAND, not the identity of the page. A
 * band flag in wireframe.yml would have to be remembered by whoever adds the
 * ninth band; `:has(.band)` cannot be forgotten, because the thing it tests
 * for is the thing that needs it. It also degrades in the right direction: a
 * browser without `:has()` renders the bands capped and inset — cramped, but
 * whole, legible and correctly coloured, because every band paints its own
 * ground. Nothing depends on this selector for legibility or for contrast.
 *
 * `padding: 0` and `margin: 0` travel with it: a band supplies its own
 * --band-air and its own --band-pad, and the frame must not add a second
 * helping on top. */
main:has(.band) {
  max-width: none;
  margin: 0;
  padding: 0;
}

main > h1 { margin: var(--space-5) 0 var(--space-4); }

/* One main idea per screen (design.md: roomy, low density by default). Bands
 * are separated by air, not by rules. */
main > section,
main > header { margin: 0 0 var(--space-6); }

/* A structural SLOT — scaffolded, not yet written. It renders as nothing at
 * all rather than as an empty box, so an unfinished page looks unfinished in
 * the source and finished on the screen. */
.slot { display: none; }

/* Motion is under 200ms, ease-out, and only ever on a state change
 * (design.md). `prefers-reduced-motion` is honoured here AND in tokens.css,
 * which zeroes --motion-duration at the root — belt and braces, because this
 * rule is the one that survives a theme that forgot. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ⛔️ NO `overflow-x: clip` ON main — REMOVED 2026-09-29, AND ITS ABSENCE IS THE
   POINT. It was added when scene bands sized their drawings by height and hung
   them over both edges on purpose; that is gone (components.css, "THE IMAGE SETS
   THE HEIGHT") and nothing overflows any more, so the guard has nothing to
   guard.

   🔴 Leaving it would be worse than useless. The house rule now is that artwork
   is NEVER cropped — and a standing `clip` is a machine that crops artwork
   silently. If some future drawing or band does overflow, the honest outcome is
   a visible sideways scrollbar that somebody fixes, not a quiet trim nobody
   sees. Verified at 390 and 1440 after removal: scrollWidth equals clientWidth,
   so there is nothing being held back. */

/* components.css — the chrome partials and the section layouts.
 *
 * @docs ../../README.md
 *
 * One class per layout id, in the order layouts/ lists them, so a layout and
 * its styling are findable from either end. Everything reads theme tokens
 * (base.css documents the contract); a literal colour here is a bug.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 🔴 REWRITTEN 2026-09-25 FOR THE BAND SYSTEM (DESIGN.md, "Field").
 *
 * The previous direction — cream ground, one editorial column, content in
 * bordered rounded panels — was rejected by the owner on sight. What replaced
 * it is a product catalogue in the teenage.engineering tradition: a stack of
 * FULL-BLEED horizontal bands, butted edge to edge with no gutter, each one a
 * flat field of colour holding ONE object with a lot of empty space around it.
 *
 * Four things follow from that, and every one of them is a rule rather than a
 * preference. They are stated once here and not re-argued at each selector:
 *
 *   1. NO CARDS. No panel, no box, no container with its own ground sitting on
 *      a different ground. The BAND is the container; a card inside a band is
 *      a container inside a container and it is the exact failure this system
 *      was chosen to escape.
 *   2. NO SHADOWS, NO GRADIENTS, NO BORDERS AS DECORATION. A border draws a
 *      control's boundary (a button, an input) or a data table's rules, and
 *      nothing else. The band edge is the divider; anything laid on top of it
 *      is ornament.
 *   3. BORDER-RADIUS IS 0 EVERYWHERE. The one circle on this site is the
 *      record, and it is inside a video and inside the screenshots — the site
 *      does not draw it. A rounded corner anywhere in this file is a bug, and
 *      it is a LOUD bug on a void band: the reel is a square of true #000000
 *      and rounding its corners lets the ground show through four little
 *      wedges, which is precisely the "shows the square" defect DESIGN.md's
 *      corner-pixel measurement exists to prevent.
 *   4. A SCREENSHOT ONLY EVER GOES ON A VOID BAND. The captures are of a
 *      pitch-black app; on a paper, steel or red band one reads as a black
 *      card, i.e. rule 1 broken by the content rather than by the CSS. That
 *      constraint is enforced in wireframe.yml, where the artifact bands are
 *      the void ones, but it is written here too because this is the file
 *      somebody edits when they want to "just move that screenshot up".
 */

/* ════════════════════════════════════════════════════════════════════════════
   THE NOTE ARC — ONE MARK, ONE DRAWING RULE, THREE USES
   ════════════════════════════════════════════════════════════════════════════

   The shape, its geometry, and the licence that allows it at all are argued in
   src/css/base.css beside --arc-mark. This is the one place it is DRAWN.

   🔴 ONE RULE FOR ALL THREE USES, GROUPED ACROSS SECTIONS ON PURPOSE. The
   selectors below live in three different parts of the site — the nav, a band
   seam, a legal list — and it is tempting to give each its own little mask
   block where it belongs. Don't: three copies of a mask declaration are three
   chances for the sizing mode, the repeat or the `-webkit-` twin to drift, and
   the failure mode of a drifted mask is a mark that is silently tiled, cropped
   or absent rather than one that looks wrong. This file already uses the same
   idiom for the mono chrome tier (`.wordmark, .nav a, .legal-nav a …`) — one
   declaration, many sites, positioned separately.

   Each use site below sets only WIDTH (and where it goes). Height comes from
   --arc-ratio and the stroke comes from the mask, so a size change is one
   number and the shape cannot deform.

   ⚠️ COLOUR IS SET PER USE, NOT HERE. `background` is what paints the mark, and
   two of the three want `currentColor` (so they invert with the band) while the
   nav wants --accent (so the current-page cue stays the site's one hot hue).
   A default here would make the nav's accent look optional.

   ⚠️ CONTRAST TOOLING IS BLIND TO ALL THREE OF THESE. They are generated
   content, and DESIGN.md records that axe and Lighthouse evaluate neither
   ::before nor ::after — the 01/02/03 step numerals shipped at 3.01:1 with
   both tools passing. Measure with `getComputedStyle(el, '::after')` by hand
   after touching anything below. Measured 2026-09-25 by compositing each
   mark's own `backgroundColor` at its own `opacity` over the ground it is
   actually drawn on, read off the live page; all three are non-text cues,
   where 1.4.11 asks 3:1:

     seam   --ink @ 0.62      on --band-void    6.30:1  ✅   28 x 3.81px
     nav    --accent @ 1      on --band-void    6.46:1  ✅   18 x 2.45px
     list   --text-secondary  on --band-paper   7.73:1  ✅   13 x 1.77px

   The seam and the nav mark land on exactly the ratios the chrome they sit in
   already measures (the closing band's own .band-sub is 6.30:1; the nav's
   accent rule measured 6.45:1 before it became an arc), so the marks did not
   introduce a new colour pairing anywhere — only a new shape.

   Every one of them is ALSO supplementary: the seam marks a boundary two
   identical grounds cannot show, `aria-current="page"` carries the nav fact
   programmatically, and a list marker restates a <ul> the markup already
   announces. Nothing here is the only carrier of any information. */
.band--void + .band--void::before,
.band--carbon + .band--carbon::before,
.band--paper + .band--paper::before,
.band--steel + .band--steel::before,
.band--red + .band--red::before,
.nav a[aria-current="page"]::after,
.legal-section li::before {
  content: "";
  display: block;
  aspect-ratio: var(--arc-ratio);
  /* `100% 100%` and not `contain`: the box and the viewBox are the same 22/3
     ratio by construction, so the two agree — and `contain` would quietly
     letterbox (shrinking the mark) the day a use site got its aspect-ratio
     wrong, instead of stretching visibly and being caught. */
  -webkit-mask-image: var(--arc-mark);
          mask-image: var(--arc-mark);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* A mark is a label. None of the three may take a hit: the seam sits inside
     a band that can be one stretched link, and the nav's sits inside the
     anchor it decorates. */
  pointer-events: none;
}

/* ── use 1: THE BAND SEAM ────────────────────────────────────────────────────

   🔴 THE CONDITION IS TWO BANDS SHARING A GROUND, NOT A FLAG SOMEBODY SETS.
   DESIGN.md is right that in a banded system the band edge IS the divider and
   a line drawn on top of it is decoration — which is exactly why this mark is
   scoped to the one case where THERE IS NO EDGE TO SEE: two adjacent bands
   painted the same colour.

   ⭐ IT MATCHES NOTHING ON THE SITE TODAY, AND THAT IS THE RULE WORKING, NOT
   A REGRESSION. Until 2026-09-25 it fired exactly once — `sign` (void
   artifact) followed by `cta-final` (void closing), the "void → void" line in
   wireframe.yml's ground table — and the owner's verdict on the live page was
   that a 28 x 3.81px mark at --plate-alpha was not carrying a whole band
   boundary. The fix was structural: `cta-final` moved to --band-carbon, so the
   two grounds now DIFFER and the edge is visible on its own. This rule is
   keyed on adjacency-plus-same-ground, so it stopped firing by itself, with no
   edit — which is precisely the behaviour it was written for.

   🔴 SO DO NOT DELETE IT AS DEAD. It is a rule about the SYSTEM, not about
   today's page: the moment any two same-ground bands are butted — a second
   carbon band under the closing one, two paper statements in a row, a
   reordering — the mark reappears on its own. The carbon pair was ADDED to the
   selector list in the same edit for that reason, rather than the list being
   narrowed to the grounds that happen to repeat right now.

   Keyed off adjacency rather than off a `class: band--seam` in wireframe.yml
   for the reason `main:has(.band)` is keyed off the presence of a band: a flag
   has to be REMEMBERED by whoever adds the ninth band, and an adjacent-sibling
   selector tests for the thing that needs it. Reorder the bands and the mark
   moves on its own; break the run and it disappears on its own. All four
   grounds are stated, not just void, so the rule is about the system rather
   than about today's page.

   Absolutely positioned, and that is load-bearing: `.band` is
   `display: grid; place-items: center`, so an in-flow ::before would become a
   REAL GRID ITEM — a second row above the band's one object, shoving it off
   centre. Out-of-flow, it costs no track. */
.band--void + .band--void::before,
.band--carbon + .band--carbon::before,
.band--paper + .band--paper::before,
.band--steel + .band--steel::before,
.band--red + .band--red::before {
  position: absolute;
  top: var(--band-pad);
  left: 50%;
  transform: translateX(-50%);
  width: var(--arc-w-seam);
  background: currentColor;
  /* The band's own quiet tier — the same alpha its plates use, so the seam
     sits in the chrome layer rather than competing with the object. */
  opacity: var(--plate-alpha);
}

/* ⚠️ THE HERO IS EXEMPT AND MUST STAY EXEMPT. It is `overflow: hidden` with
   `padding: 0` and a sticky bar pinned over its top edge, so a mark at
   --band-pad would land under the nav. It is also the FIRST band on the only
   page that has bands, so the selector above can never match it — this rule is
   a guard against a future page that opens with two void bands, not a fix for
   anything visible today. */
.band--hero + .band--void::before,
.band--hero + .band--carbon::before { display: none; }

/* ════════════════════════════════════════════════════════════════════════════
   THE CHROME SHELL — header and footer (src/html/_header.html, _footer.html)
   ════════════════════════════════════════════════════════════════════════════

   🔴 TWO ELEMENTS PER BAR, AND THE SPLIT IS THE WHOLE POINT.

   Before the band redesign, ONE element both painted the bar and capped it at
   `--page`. That is fine while every band also lives inside the same cap. It
   is broken the moment a band is full-bleed: on a 1440px viewport the paint
   stopped at 1152px and a black reel scrolled past in the 144px gutters either
   side of the bar. DESIGN.md's "Shape" section predicted this in writing and
   base.css's page-frame block carries the same warning; both now point here.

   So the bar is:
     .site-header      the SHELL — full-bleed (width from base.css), FIXED and
                       UNPAINTED since 2026-09-25. It never carries a
                       max-width. Ever.
     .site-header-row  the CONTENT ROW — wordmark, nav, theme mount. It carries
                       the cap and the inset.

   The footer is the identical pair for the identical reason: it sits directly
   under a full-bleed void band on the home page, and a capped floor under an
   uncapped band draws a visible step. The footer is STILL PAINTED — see the
   note on its own rule; only the header lost its ground. */

/* 🔴 `data-band="void"` IS IN THE MARKUP AND IT IS NOT DECORATIVE. theme/
   tokens.css RE-BINDS the whole text vocabulary inside any of three selectors
   — `[data-band="void"]`, `.band-void`, `.band--void`. Without one of them,
   `--ink` keeps its light-ground value (#111111) and the chrome renders
   near-black on black. src/html/_header.html and _footer.html carry the
   attribute spelling; the bands carry the class spelling. A FOURTH spelling
   silently fails, and no tool will say so: axe and Lighthouse measure whatever
   pairing the markup produces and report it as fine, having no idea the band
   was meant to invert.

   ⚠️ IT STAYS ON THE HEADER EVEN THOUGH THE HEADER NO LONGER PAINTS ANYTHING.
   The attribute was never a paint instruction — it is the INK POLARITY hook,
   and the flyover needs the light polarity more than the painted bar did,
   because it is now the only thing deciding what colour the chrome is over a
   black band. Deleting it as "the bar isn't a void band any more" would
   re-bind --ink to #111111 and render the nav near-black over the black hero.

   Inside that scope --ink is #E3E3E1 (16.34:1 on black) and --accent-text is
   the accent at rest, #FF4F45 (6.46:1). So base.css's global focus ring needs
   no override here: it rings in --accent-text and that is already the correct
   colour on this ground. Semantic names everywhere, no per-band colour
   special-casing — that is the contract, and the ONE exception on this site is
   the red band, documented where it is made. */
.site-header,
.site-footer { color: var(--ink); }

/* ⭐ THE HEADER IS A TRANSPARENT FLYOVER — 2026-09-25, OWNER INSTRUCTION FROM
   READING THE LIVE SITE, and modelled on the sibling app's shipped bar
   (~/Repos/touchtone/site/src/css/components.css, `.site-header`).
   `position: fixed`, edge to edge, NO BACKGROUND AT ALL: the chrome sits over
   whatever band happens to be scrolling underneath it rather than inside a
   painted rail of its own.

   🔴 WHAT WAS DELIBERATELY *NOT* COPIED FROM TOUCHTONE: `mix-blend-mode:
   difference`. That is how the sibling gets legibility for free, and Riffmix
   cannot have it — this site has a RED BAND. Measured: white over --accent
   (#FF4F45) composites under `difference` to cyan #00B0BA, which is 1.23:1
   against the ground it is drawn on. And that is not a bad choice of ink, it
   is the mechanism: `difference` maps a ground of luminance L to |ink - L|, so
   ANY ink collapses toward its ground as that ground approaches mid-luminance,
   and a saturated mid-luminance field like this red has no ink that survives
   it. Touchtone gets away with the trick because touchtone has no red band.
   Do not reintroduce it here, and do not "fix" it by picking a different ink.

   ⚠️ `position: fixed`, NOT `sticky`, AND `data-frame-header` IS NO LONGER
   READ. wireframe.yml still resolves `frame.header: sticky` and the shell
   still stamps it onto <body> (pipeline/model.mjs's resolveFrame is outside
   this file's lease), it is simply not consulted here any more — flagged as a
   deliberate, documented deviation rather than silently repointing
   wireframe.yml at a value ("static") that would then say something false. The
   `frame:` vocabulary has no third word for "fixed, out of flow, unpainted";
   this is that gap's flag, not its fix. The identical note is in touchtone's
   file, for the identical reason.

   🔴 OUT OF FLOW MEANS THE PAGES HAVE TO PAY FOR IT. Bands no longer reserve
   the bar's height, which is exactly what the hero wants (a black field the
   chrome flies over) and exactly what a reading page does not — base.css's
   `main` now pays `--frame-header-h` as top padding and `main:has(.band)`
   zeroes it again. Both halves are needed; either alone is a defect.

   `z-index: 10` keeps it above every band (all `position: relative`, none
   carries a z-index) and below the skip link (z 100), so keyboard focus still
   lands on the skip link first. */
body .site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: none;
}

/* ════════════════════════════════════════════════════════════════════════════
   THE BAR'S TWO PER-GROUND VALUES, AND THE ONE ATTRIBUTE THAT PICKS THEM
   ════════════════════════════════════════════════════════════════════════════

   --plate-alpha  the quiet tier, the same per-ground variable every band sets
                  for its own plates (THE QUIET TIER, below, carries the whole
                  ladder and the measurements). It cannot be one number: the
                  0.62 that measures 6.32:1 on void measures 3.58:1 on steel.
   --chrome-ring  the colour of the stencil ring under the bar's type — see
                  THE HALO immediately below for what the ring is for and why
                  its COLOUR, not its presence, is what varies.

   🔴 BOTH READ ONE ATTRIBUTE, AND THAT IS THE DESIGN. `data-band` already
   decides the bar's ink polarity (theme/tokens.css re-binds --ink inside
   `[data-band="void"]`), and these two are answers to the SAME question —
   "what ground is this bar over?" A second, separately-authored condition for
   either of them would be a second thing to keep in step, and the failure
   mode of drifting apart is silent: axe measures whatever pairing the markup
   produces and reports it as fine.

   ⚠️ THE UNQUALIFIED RULE IS THE DARK CASE, WRITTEN AS THE DEFAULT ON
   PURPOSE. A header carrying no `data-band` at all, or a spelling nothing
   matches, gets the void alpha and the void ring — i.e. the treatment that
   assumes NOTHING about what is underneath, which is the safe direction. The
   paper values are the ones that require a positive claim to be made.

   ⭐ 🔴 AND SINCE 2026-09-26 BOTH OF THESE ARE THE NO-JS FALLBACK, NOT THE
   SHIPPED VALUES. src/js/header-ink.js writes both as INLINE custom properties
   from whatever ground is actually under the bar at the current scroll position
   — `--plate-alpha` copied from that ground's own declaration, `--chrome-ring`
   from its own computed background colour. See THE INK FLIP below. These two
   declarations then answer the question for a visitor with no JavaScript, on
   the authored static claim, exactly as they did before. Which is why they are
   still keyed on `data-band` and why the script must never rewrite it. */
.site-header {
  --plate-alpha: 0.62;
  --chrome-ring: var(--band-void);
}
.site-header[data-band="paper"] {
  --plate-alpha: 0.68;
  --chrome-ring: var(--band-paper);
}

/* ════════════════════════════════════════════════════════════════════════════
   🔴 THE HALO — HOW UNPAINTED CHROME STAYS LEGIBLE OVER ALL FOUR GROUNDS
   ════════════════════════════════════════════════════════════════════════════

   ⭐ AND SINCE 2026-09-26 THE RING HAS A COLOUR PER PAGE INSTEAD OF ONE
   COLOUR FOR ALL OF THEM. Everything below is an argument about a bar that
   flies over MANY grounds, and every word of it holds on the home page. It
   does not hold on the other six: they paint no bands at all, so the only
   GROUND that can ever be under the bar there is --band-paper.
   `src/html/_header.html` stamps what it is actually over (`data-band="void"`
   on a banded page, `"paper"` on a band-less one — the fact is
   pipeline/model.mjs's `hasBands`, the same presence-of-a-band test
   `main:has(.band)` uses), and that ONE attribute carries both halves:

     POLARITY  theme/tokens.css re-binds --ink inside `[data-band="void"]`, so
               on a band-less page --ink goes back to being #111111 — plain
               dark ink, which is the whole fix. The bug it closes: the bar
               claimed void polarity on every page, so /features drew #E3E3E1
               ink — 1.07:1 on paper — and the ring was the only thing making
               it legible. Light ink in a black outline on a white page is an
               outlined smudge, and that is what it looked like.
     THE RING  --chrome-ring, set on the same selector, above. The ring is NOT
               removed on a band-less page — it is drawn in --band-paper
               instead of --band-void.

   🔴 WHY THE RING SURVIVES, AFTER BEING SCHEDULED FOR DELETION. The first cut
   of this change removed it outright, on the argument that with one ground
   the ring buys nothing. That argument is about the page's GROUND and it
   forgets that a FIXED bar also flies over the page's CONTENT. Measured on
   the live /support at 390px, where the form's red submit button scrolls
   fully under the bar: bare dark ink on --accent is 3.68:1 — an AA failure,
   and the ONE such element on any band-less page (every other background
   inside `main` on all six is --band-paper itself). At 1440 the button never
   reaches the bar; on a phone it passes right under the links.

   🔴 AND A PAPER RING COSTS NOTHING TO LOOK AT, WHICH IS WHY THIS IS NOT A
   COMPROMISE. The ring is opaque and drawn behind the glyph, so on the paper
   ground it is --band-paper on --band-paper: not "subtle", IDENTICAL, zero
   visible difference, and the measured pairing is unchanged at 6.18:1 because
   a glyph pixel still composites over the page. The owner's complaint —
   an outlined smudge on /features — is answered by the INK going dark, not by
   the ring going away. Over the red button the ring is the only thing there
   is, and it holds the local ground at paper: 7.26:1, measured.

   ⚠️ SO THE RULE IS: the ring's colour is the ground the bar is over. ⭐ AND
   SINCE 2026-09-26 THAT IS RESOLVED PER SCROLL POSITION, NOT PER PAGE — the
   sentence did not change, the tense did (THE INK FLIP, below). Do not
   "simplify" this back to one colour, and do not delete the ring on the
   band-less pages — there is a screenshot-invisible, phone-only, one-button AA
   failure waiting behind that edit, and it is the exact defect this paragraph
   exists to stop being rediscovered. The home page has five more of them,
   measured in the same terms in the block below.

   ⚠️ `mix-blend-mode: difference` IS STILL NOT THE ANSWER, not even on the
   band-less pages where it would work. Measured: white over --band-paper
   composites to #101412, which is legible — and white over --accent (#FF4F45)
   composites to #00B0BA at 1.20:1, which is not. Two mechanisms for one bar,
   chosen per page, is worse than one ring whose colour is named by data.

   THE PROBLEM, STATED AS ARITHMETIC RATHER THAN AS TASTE. A fixed bar with no
   ground of its own is drawn over every field the page scrolls under it —
   --band-void #000000, --band-carbon #222220, --band-paper #EFEFED,
   --band-steel #C9C9C6 and --accent #FF4F45. 12-13px mono is SMALL TEXT, so
   WCAG 1.4.3 asks 4.5:1. There is no single ink that can do it, and that is
   provable rather than arguable:

     against #000000  needs relative luminance L >= 0.175
     against #EFEFED  needs                    L <= 0.1446

   The two windows do not intersect. Not "no good colour" — NO colour. (Even
   at the 3:1 large-text floor the window is [0.10, 0.2419], and this tier is
   not large text.) So a one-colour transparent bar is not an option that was
   passed over; it is impossible, and anything that appears to work is a
   measurement nobody took.

   THE ANSWER: GIVE THE INK ITS OWN GROUND, GLYPH-SHAPED. Every mark in the bar
   carries a SOLID 1px ring of --band-void drawn behind it — `text-shadow` for
   type, chained `filter: drop-shadow()` for the wordmark image. The ring is
   opaque and offset, never blurred, so the pixel immediately adjacent to every
   stroke is the ring and not the band. The ink's ground is therefore CONSTANT
   at --band-void no matter what is underneath, which is what makes the ratio
   below a measurement and not an estimate. A blurred glow would have looked
   softer and would have had no measurable ground at all.

   🔴 THE BAR ITSELF STILL PAINTS NOTHING. That is the difference between this
   and the painted shell it replaces: the ring is the shape of the letters, so
   over the five void bands and the footer it is BLACK ON BLACK — literally
   invisible, and the hero (the first screenful, and the one the owner is
   looking at) renders pixel-identical to the painted version. The ring only
   becomes visible on the three light-ish bands, where it reads as a stencil
   edge, which is the register of a spray-painted brand rather than a foreign
   effect.

   ── MEASURED, 2026-09-25, ON THE REAL COMPOSITE ─────────────────────────────
   The nav's `opacity: 0.62` composites the WHOLE element — ring included — so
   the honest pairing is 0.62 x #E3E3E1 over (0.62 x #000000 over the band),
   computed per band. That is what is tabulated, not the naive #E3E3E1-on-black:

                                 ink      on ground    with halo   WITHOUT
     nav link on --band-void     #8D8D8C  on #000000      6.30:1    6.30:1 ✅
     nav link on --band-carbon   #9A9A98  on #0D0D0C      6.87:1    5.63:1 ✅
     nav link on --band-paper    #E8E8E6  on #5B5B5A      5.53:1    1.07:1 ✅
     nav link on --band-steel    #D9D9D7  on #4C4C4B      6.05:1    1.18:1 ✅
     nav link on --accent        #EEABA6  on #611E1A      6.43:1    1.70:1 ✅

   All five clear AA for small text. The right-hand column is the same ink with
   the ring removed and it is the whole argument in one number: 1.07:1 on
   paper. The void figure is unchanged to two decimals from the 6.30:1 the
   PAINTED bar measured, which is the other thing worth knowing — the flyover
   costs the ground it spends most of its time over exactly nothing. Carbon is
   the one ground that would have survived without the ring (5.63:1), which is
   a useful sanity check on how dark it is rather than a reason to special-case
   it.

   Computed on the live page by reading the link's own `color` and `opacity`
   and compositing them over each band's computed background, not by eye and
   not by axe — axe sees a transparent element over a transparent element and
   reports nothing at all here. ⚠️ The figures assume the pixel adjacent to a
   stroke is fully the ring, which is what a 0-blur 1px offset buys; a blurred
   shadow would make every number above an estimate.

   ⚠️ THE WORDMARK IS A LOGOTYPE AND IS *EXEMPT* FROM 1.4.3 — its halo is a
   DESIGN requirement, not a compliance one, and it is DESIGN.md's own
   instruction: "riffmix.wordmark.svg carries no ground of its own … rendered
   on --band-paper its yellow/lime third nearly disappears … any future
   placement on paper, steel or red needs its own dark plate, or it must not go
   there." A fixed bar puts it on all three. The chained drop-shadow IS that
   dark plate, cut to the shape of the paint instead of drawn as a rectangle —
   a rectangle would be the card this system forbids. */
.site-header .wordmark,
.site-header .nav a {
  /* Eight offsets, not four: the four diagonals are what close the corners of
     the ring. Every one is 0-blur so the ring is a hard, fully opaque 1px
     edge — the property every ratio in the table above depends on.

     ⚠️ --chrome-ring, NOT --band-void. There is ONE ring declaration and the
     `data-band` selector above names its colour, rather than two copies of
     these eight offsets under two selectors: eight offsets stated twice is
     eight chances for the two to drift, and nothing would report it. */
  text-shadow:
     1px  0    0 var(--chrome-ring),
    -1px  0    0 var(--chrome-ring),
     0    1px  0 var(--chrome-ring),
     0   -1px  0 var(--chrome-ring),
     1px  1px  0 var(--chrome-ring),
    -1px -1px  0 var(--chrome-ring),
     1px -1px  0 var(--chrome-ring),
    -1px  1px  0 var(--chrome-ring);
}

/* The image arm of the same idea. `drop-shadow` follows the ALPHA CHANNEL, so
   this rings the paint itself — the drips and the flicks included — where a
   `box-shadow` would ring the <img>'s rectangle and draw the card.

   🔴 THE FOUR ARE CHAINED, NOT COMMA-SEPARATED, AND THE CHAIN IS WHY THE
   CORNERS CLOSE. `filter` composes left to right, so each drop-shadow shadows
   the OUTPUT of the one before it: after the first two the silhouette is
   already 1px wider on both sides, and the vertical pair then rings that,
   covering the diagonals without four more passes. 1.5px rather than the
   type's 1px because the trace's edges are soft (theme/artwork-note.md
   records that edge softness is the thing the contour trace lost) and a 1px
   ring disappears into that antialiasing on a light band.

   ⭐ 🔴 AND THIS PLATE STAYS --band-void ON EVERY PAGE, WHERE THE TYPE'S RING
   FOLLOWS --chrome-ring — 2026-09-26, decided by looking, and the asymmetry
   is the point. The type's ring is the ink's LOCAL GROUND, so it wants to be
   whatever the bar is over: paper on a paper page, where it is then invisible
   and the dark ink carries itself. This plate is not a ground, it is the only
   DARK thing the mark will ever have. It is a spray lockup whose middle third
   is yellow/lime, and DESIGN.md's measurement stands whatever the bar is
   over: "rendered on --band-paper its yellow/lime third nearly disappears …
   any future placement on paper, steel or red needs its own dark plate, or it
   must not go there." /features IS a paper placement. Removing the plate
   there does not make the mark quieter, it makes a third of it vanish.

   It is also not a contrast obligation either way — a logotype is exempt from
   1.4.3 — so this is a design call, and the two calls can differ because they
   answer different questions. Checked on the live /features at 1440 and 390
   with the filter toggled off: without the plate the lime is gone against
   paper; with it the mark reads exactly as it does on the black hero. The
   ring is cut to the shape of the paint, so on paper it reads as a stencil
   edge — the register of a spray-painted brand — rather than as the card this
   system forbids.

   ⭐ 🔴 AND THE INK FLIP DID NOT CHANGE IT — RE-DECIDED 2026-09-26, EXPLICITLY,
   BECAUSE THE FLIP MADE IT LOOK LIKE IT SHOULD. The type's ring answers a
   LEGIBILITY question and the flip is a better answer to it. This plate never
   answered that question: the mark is an IMAGE whose colours are baked into the
   file, so no amount of flipping the TYPE'S ink reaches it. On paper, steel and
   red it stays for the reason it was added — it is the only dark ground the mark
   will ever have, and DESIGN.md's measurement does not move.

   THE VERSION THAT WAS BUILT AND THEN REVERTED was `filter: none` over the dark
   grounds, gated on the script's `[data-ink="light"]`, on the argument that a
   #000000 plate is 1.32:1 against --band-carbon and --band-graphite — a faint
   dark stencil edge around the mark on the closing band and the footer, bought
   for no legibility at all. Then the content sweep was run: at 768 and 390 the
   HERO REEL'S ARTWORK passes directly under the mark (the full 53px of it), and
   at 390 so does the closing band's own big wordmark. On those frames the plate
   is the only separation the mark has, on the first screenful, on a phone. A
   cosmetic 1.32:1 edge on two bands is a fair price for that, so the plate is
   unconditional and there is ONE rule here instead of two.

   ⚠️ WHICH IS ALSO WHY NOTHING IN THIS FILE READS `[data-ink]`. It was added as
   this rule's gate; the rule declined it. See the note at the end of THE INK
   FLIP block below for why the attribute is still stamped. */
.site-header .wordmark-img {
  filter:
    drop-shadow( 1.5px 0     0 var(--band-void))
    drop-shadow(-1.5px 0     0 var(--band-void))
    drop-shadow( 0     1.5px 0 var(--band-void))
    drop-shadow( 0    -1.5px 0 var(--band-void));
}

/* ════════════════════════════════════════════════════════════════════════════
   ⭐ 🔴 THE INK FLIP — 2026-09-26, AND WHAT IT DID AND DID NOT REPLACE
   ════════════════════════════════════════════════════════════════════════════

   The halo works — 6.30 / 5.53 / 6.05 / 6.43 / 6.87:1, the table above is real
   — and THE OWNER REJECTED THE LOOK TWICE: over the light bands a black ring
   around light ink reads as outlined, embossed type. On the six band-less pages
   that was answered by making the ring --band-paper, where it is invisible. The
   home page was what was left, and there the ring could not be invisible,
   because ONE ring colour was serving five different grounds.

   So the INK now flips. src/js/header-ink.js (inlined in src/html/_header.html)
   resolves which ground is under the bar and copies THAT GROUND'S OWN ink
   vocabulary onto `.site-header` as inline custom properties — --ink, the
   --text-* tiers, --accent-text, the ground's measured --plate-alpha, and
   --chrome-ring. Every ground has a polarity that wins outright, so the bar is
   never the wrong polarity for what it is over:

     ground                     ink (copied)   nav @ its α   the arc
     --band-void     #000000      #E3E3E1        6.30:1        6.46:1
     --band-carbon   #222220      #EFEFED        6.17:1        4.90:1
     --band-graphite #282826      #EFEFED        5.87:1        4.54:1
     --band-paper    #EFEFED      #111111        6.18:1        6.49:1
     --band-steel    #C9C9C6      #111111        5.19:1        4.50:1
     --accent        #FF4F45      #111111        5.81:1        6.27:1

   🔴 THE SCRIPT WRITES NO COLOUR AND NAMES NO GROUND. It copies what the band
   already resolves, so every ratio above is a ratio the BAND already measured —
   the two cannot drift, because there is only one set of numbers. That is also
   why NOTHING is added to theme/build-tokens.mjs for this: no new scope, no new
   selector, no second table. Read that file's header before changing any of
   this; it carries the arithmetic, the reason `mix-blend-mode: difference` is
   not available to a site with a red band, the boundary rule (the ground
   covering the bar's vertical CENTRE, on a half-open interval, so the answer is
   a step function of scroll position and cannot flicker), and the self-check
   that refuses to engage on a pairing under 4.5:1.

   ⚠️ THE SWAP IS INSTANT AND THAT IS A DECISION. DESIGN.md allows a transition
   under 200ms ease-out; there is none. A fade parks the ink mid-grey — the one
   value that measures badly against BOTH grounds — for the fade's duration, and
   does it exactly while the boundary crosses the type. The band edge is a hard
   edge and ink matching it is the shorter failure. The side benefit is that
   there is nothing for `prefers-reduced-motion` to suppress, so the site's four
   reduced-motion blocks stay four.

   ════════════════════════════════════════════════════════════════════════════
   🔴 AND THE RING STAYS. IT IS NOW PAINTED IN THE LIVE GROUND, SO IT IS
   INVISIBLE ON EVERY GROUND INSTEAD OF ON ONE.
   ════════════════════════════════════════════════════════════════════════════

   ⚠️ THIS IS A DELIBERATE DEVIATION from the instruction that came with the
   flip, which was "drop the halo entirely on the home page — no ring, no
   outline, that is the whole point". The OUTCOME that asked for is delivered:
   there is no visible outline anywhere, on any band, at any scroll position.
   What is not delivered is the ring's DELETION, and the reason is measured.

   THE THING THE FLIP CANNOT DO. The flip resolves the BAND under the bar. A
   fixed bar also flies over that band's CONTENT, and on the home page it flies
   over a lot of it. Swept at 6px of scroll across the whole page, the marks in
   the bar overlap: the hero reel's artwork (at every width), all four
   screenshots, `band-say`'s display type, `band-sub`, `band-cta`, both
   `band-plate`s, `band-spec`, and the closing band's own big wordmark. At 390px
   — the phone, which base.css calls this site's whole audience — `band-say`
   passes under two nav links for 1,900px of scroll.

   MEASURED ON REAL PIXELS, 2026-09-26. The bar was hidden, the top 58px
   screenshotted at every 24px of scroll, and the darkest and brightest pixel
   actually found inside each mark's box recorded per ground. Then the link's
   own ink, at its own alpha, composited over those real extremes:

                    worst real backdrop        NO RING    RING = LIVE GROUND
     void      #000000 … #FFFFFF (reel art)     1.16:1 ❌      5.32:1 ✅
     paper     #000000 … #EFEFED (band-say)     1.07:1 ❌      6.18:1 ✅
     steel     #101010 … #C9C9C6 (band-say)     1.01:1 ❌      5.19:1 ✅
     red       #000000 … #FF4F45 (the plate)    1.11:1 ❌      5.81:1 ✅
     carbon    #222220 … #FAFF03 (the wordmark) 1.06:1 ❌      4.22:1 ⚠️

   With the ring deleted, five of six grounds have a real scroll position where
   a nav link is INVISIBLE — 1.01:1, not "low". With the ring painted in the
   live ground, the worst case across the whole page is 4.22:1, and that one is
   the single brightest pixel of a decorative spray wordmark under a link on a
   phone. The four other grounds clear AA outright.

   🔴 AND IT COSTS NOTHING TO LOOK AT, WHICH IS WHY THIS IS NOT A COMPROMISE.
   The ring is opaque and drawn BEHIND the glyph, so on the band's own ground it
   composites to `over(ground, α, ground)` — which is the ground, EXACTLY. Not
   subtle: arithmetically identical, zero visible difference, ΔE 0. That is the
   same argument this file already accepts for the band-less pages' paper ring
   ("--band-paper on --band-paper: not 'subtle', IDENTICAL"), now true on all
   six grounds instead of one. The ring becomes visible ONLY where content
   intrudes under a mark — i.e. only in the frames where it is the only thing
   holding the link legible. It is invisible when it is idle and it appears when
   it works, which is the behaviour the owner's complaint was actually about.

   ⚠️ SO THE RULE IS: the ring's colour is the ground the bar is over, and
   "over" is now resolved per scroll position instead of per page. That is the
   SAME sentence the --chrome-ring block above already carried; only the tense
   changed. Do not delete the ring, and do not pin its colour: the failure
   behind that edit is a nav link at 1.01:1 on a phone, invisible in every
   screenshot taken at a scroll position where nothing happens to be underneath.

   ── the no-JS path, unchanged and unchangeable ───────────────────────────────

   🔴 EVERY PIECE OF THE NEW BEHAVIOUR ARRIVES AS AN INLINE CUSTOM PROPERTY, so
   with no JavaScript there is nothing to fall back FROM. The authored
   `data-band` above still stamps the bar's static claim, the two per-polarity
   declarations above still answer it, and the eight offsets still draw in
   --chrome-ring. Verified 2026-09-26 by stripping the script's output and
   re-measuring: 6.30 / 5.53 / 6.30 / 6.43 / 6.30 / 6.05 / 6.30 / 6.87 —
   the shipped table, to the last decimal, including its composited hexes.
   The script never REWRITES `data-band`, which is what keeps that true; a
   version that tracked the live ground in the attribute would read more
   tidily and would destroy the fallback.

   ⚠️ AND THE BAND-LESS PAGES ARE NEVER TOUCHED. The script's own presence test
   (`document.querySelector(".band")`, the same test `main:has(.band)` uses)
   returns nothing there, so /features, /support and the rest keep their
   `data-band="paper"` polarity and their invisible paper ring — including the
   one thing that ring is there for, which is /support's red submit button
   passing under the bar on a phone (3.68:1 bare, 7.26:1 ringed).

   ── `data-ink`, and why it is on the element with no rule reading it ─────────

   The script also stamps `data-ink="light|dark"` — the polarity it resolved.
   NOTHING IN THIS STYLESHEET SELECTS ON IT TODAY, deliberately: everything the
   flip needs travels as a custom property, and the wordmark's plate turned out
   to want to stay unconditional (the rule above says why). It is kept because a
   scroll-dependent mechanism that leaves no trace is untestable — this is what
   the verification harness reads to assert the boundary rule, and what devtools
   shows when someone asks what the bar thinks it is over. If a future chrome
   rule genuinely needs the polarity, this is the hook; do not re-derive it. */

/* The FOOTER keeps its ground, and that is not an oversight. It is the last
   thing on the page, in flow, under `cta-final`. base.css floors `body` on
   --band-paper, so an unpainted footer would show a near-WHITE strip directly
   under a dark band on the home page: a visible step, and the exact defect the
   shell/row split exists to prevent, arriving from the other side. The flyover
   argument does not apply to it either — nothing scrolls underneath a page's
   floor.

   It moved to --band-carbon with the closing band on 2026-09-25, on the
   argument that the footer and `cta-final` "have always been the same colour
   as each other" and that the redesign meant to add ONE edge, at
   `sign → cta-final`, not two.

   ⭐ AND IT MOVED OFF CARBON AGAIN ON 2026-09-26, TO A GROUND OF ITS OWN —
   OWNER, ON THE LIVE PAGE. Same-as-the-band turned out to be the defect
   rather than the discipline: `cta-final` and the floor under it read as ONE
   continuous dark run, so the page had a close and no floor. The footer is
   not a band — it is the thing the bands stop at — and it now says so.

   --band-graphite is a LIFTED black, #282826 against carbon's #222220: L*
   16.05 against 13.16, ΔL* 2.89. Chosen the way carbon was, against
   constraints rather than by eye, and the full derivation is in
   theme.config.json's `bands._note`. The short version is that the window is
   CLOSED AT THE TOP by the generator: `--accent-text` on a dark ground is the
   accent at rest (#FF4F45), 4.5:1 needs a ground under L 0.02174, and
   #282826 is L 0.02109 — the last 2-step-warm neutral under that ceiling
   (4.54:1). One step further, #292927, measures 4.48:1 and makes
   build-tokens.mjs's raiseHex floor FIRE, repainting the brand red on this
   ground alone. ΔL* 2.89 is the whole window the constraint leaves.

   🔴 THE SEAM RULE IS NOT INVOLVED AND MUST NOT BE MADE TO BE. The note-arc
   band seam above is keyed on `.band--x + .band--x` — two adjacent BANDS
   sharing a ground. The footer is not a `.band`, so it was never eligible,
   and the grounds now differ anyway. `graphite` is deliberately absent from
   that selector list: adding it would imply a `.band--graphite` that does not
   exist. (theme/tokens.css does emit the `.band-graphite` polarity spellings,
   so a future band that adopts this ground inverts correctly; that is the
   token layer, not this one.)

   ⚠️ src/html/_footer.html carries `data-band="graphite"` to match — that
   attribute is the INK POLARITY hook (theme/tokens.css, via DARK_BAND_SCOPES
   in theme/build-tokens.mjs), not the paint, and a footer painted graphite
   while still scoped `carbon` would draw its links in carbon's ladder on the
   wrong ground. Both spellings have to move together, and the ground has to
   be in that table before either moves at all. */
.site-footer { background: var(--band-graphite); }

.site-header-row,
.site-footer-row {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
}

/* ⭐ `align-items: center`, NOT `baseline` — CHANGED 2026-09-25, AND IT IS A
   CORRECTION, NOT A PREFERENCE.
   Baseline was right while the wordmark was 11px and read as one more word in
   a row of words. It is wrong at 30px for two separate reasons:

   1. THE MARK HAS NO BASELINE. riffmix.wordmark.svg is a spray lockup whose
      letters run on a DIAGONAL — there is no line for a baseline to be. What
      `align-items: baseline` actually aligned was the image's BOTTOM MARGIN
      EDGE (a replaced element's synthesised baseline), i.e. the lowest paint
      drip, to the nav's type baseline. At 11px nobody could see the
      difference; at 30px the mark visibly hangs below the words.

   2. IT MADE --frame-header-h UNMEASURABLE. Baseline alignment puts the FONT'S
      ASCENT METRIC inside the row's height, and that metric is not something
      this stylesheet can know: base.css's arithmetic assumed ~0.78em and
      measured right at 13px and 1.04px WRONG at 12px — it predicted 66.24 for
      a bar that rendered 65.19. A token that drives `scroll-padding-top` and
      every reading page's top inset cannot be approximately right.

   Centred, the row's content height is `max(the mark, one nav line)` — two
   boxes this file already states — so the arithmetic is exact at every size
   with no font metric in it at all. Verified: it still computes 47.60 / 91.20
   for the pre-2026-09-25 tokens, the same two numbers the baseline version
   was checked against. */
.site-header-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  /* 14px is stated as a literal here and in base.css's --frame-header-h, and
     the two must move together. It is smaller than --space-1 and inventing a
     --space-0 to hold one value used twice would cost more than it saves. */
  padding: 14px var(--band-pad);
}

/* 🔴 THE `body[data-frame-header="sticky"] .site-header { position: sticky }`
   RULE THAT LIVED HERE IS DELETED, NOT COMMENTED OUT, AND IT HAS TO STAY
   DELETED. It out-specified the flyover rule above ((0,2,1) against (0,1,1)),
   so leaving it in place would have silently kept the bar sticky and in flow
   while every comment on this page described a fixed one — the worst possible
   version of this change. The positioning now lives in ONE rule, above, and
   `data-frame-header` is no longer read by this stylesheet at all (see the
   deviation note there). */

/* ── the chrome type tier ────────────────────────────────────────────────────
   DESIGN.md: "Labels, captions, nav — IBM Plex Mono, 11px, letter-spacing:
   0.14em. Band captions sit in a corner like a plate stamped on a machine."
   The nav is the same tier as the plates on purpose: it is not a navigation
   BAR with a brand in it, it is one more stamped label, and it aligns to
   --band-pad so it sits in the same column every plate below it does. */

/* ⚠️ `.footer-contact a` LEFT THIS LIST 2026-09-26, AND IT WAS DELETED RATHER
   THAN KEPT AS A FALLBACK — the opposite call from `.wordmark` two rules
   down, and for a reason that is worth stating because the two look alike.
   `.wordmark`'s type rules still reach a real element (the <img>'s alt text)
   if the brand asset 404s, so deleting them would delete a degradation path.
   `.footer-contact` reaches nothing: src/html/_footer.html no longer contains
   the element under any condition, because the mailto block was removed
   outright rather than guarded. CSS for an element that cannot be rendered is
   not a fallback, it is a claim that the element still exists. */
.wordmark,
.nav a,
.legal-nav a,
.footer-group-title,
.footer-links a,
.footer-note,
.colophon {
  font-family: var(--font-mono);
  font-size: var(--chrome-size);
  letter-spacing: var(--tracking-mono);
  line-height: 1.6;
  text-decoration: none;
  color: var(--ink);
}

/* ⚠️ THE TYPE RULES BELOW ARE NOW THE FALLBACK, NOT THE DESIGN. The wordmark
   is an IMAGE (src/html/_header.html); these styles reach it only through the
   <img>'s alt text, which every engine renders inline in the parent's font
   when the asset is missing. Keeping them means a 404 on the brand asset
   degrades to the mono chrome wordmark this replaced instead of to a broken
   image icon. Deleting them as "dead" would delete that fallback. */
.wordmark {
  font-weight: 500;
  /* content/app.json carries the wordmark as "Riffmix" and this lowercases it
     for display only. DESIGN.md sets the whole display register lowercase; the
     DATA stays capitalised so the <title>, the JSON-LD and the og:site_name —
     none of which pass through this stylesheet — still say Riffmix. */
  text-transform: lowercase;
}

/* ── the wordmark image, place 1 of 2: THE NAV ───────────────────────────────

   🔴 HEIGHT IS THE ONLY DIMENSION SET, AND ITS CEILING IS ARITHMETIC. See
   --wordmark-nav-h in base.css for the full derivation: `.site-header-row` is
   baseline-aligned, a replaced element's baseline is its bottom margin edge, so
   this image contributes its own height above the row's baseline against the
   mono links' 11.88px of ascender-plus-half-leading. At 11px the links still
   set the bar's height and --frame-header-h — and therefore every `#` anchor's
   landing point — is untouched. Raise it past 11.88px and that arithmetic has
   to gain a max() term for this element.

   `width: auto` because the asset's aspect ratio is ITS data, not this file's.
   No `aspect-ratio` literal here for the same reason .band--hero states none
   for the reel: a re-drawn wordmark must be a file swap, not a CSS edit.

   There is no CLS exposure from the missing width: the height is fixed in CSS
   before the asset arrives, so nothing moves VERTICALLY, and the row is
   left-aligned so the nav beside it shifts horizontally once and only on first
   paint. */
.wordmark-img {
  display: block;
  height: var(--wordmark-nav-h);
  width: auto;
  /* base.css sets `img { max-width: 100% }`; harmless here but restated to
     none so a narrow viewport shrinks the NAV (which wraps) rather than
     squashing the mark below its measured height. */
  max-width: none;
}

.nav {
  display: flex;
  flex: 1;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.nav a {
  text-transform: lowercase;
  /* The containing block for the current-page arc below. Nothing else needs
     it; without it the mark positions against the nearest positioned ancestor,
     which is `.site-header` (sticky), and lands in the middle of the bar. */
  position: relative;
  /* ⭐ READS --plate-alpha SINCE 2026-09-26, WHERE IT USED TO STATE 0.62.
     The quiet tier is MEASURED and it is PER GROUND — THE QUIET TIER below
     has the whole ladder and the reason it cannot be one number: the same
     alpha that measures 6.32:1 on void measures 3.58:1 on steel. Every other
     chrome selector on the site already reads this variable; the nav was the
     one that stated its own, which was invisible while the bar always claimed
     the void ground and became wrong the moment it stopped (the band-less
     pages are on paper, whose measured alpha is 0.68). The declarations are
     on `.site-header` per polarity, beside the halo. The fallback keeps this
     rule correct on its own if it is ever lifted out of the bar. */
  opacity: var(--plate-alpha, 0.62);
  padding-bottom: 2px;
}
.nav a:hover { opacity: 1; }
.nav a[aria-current="page"] { opacity: 1; }

/* ── use 2: THE CURRENT-PAGE INDICATOR ───────────────────────────────────────

   Was `border-bottom: 1px solid var(--accent)` — a plain rule, and the owner's
   instruction is that the note arc replaces those. It is supplementary
   regardless, because `aria-current="page"` carries the fact programmatically
   and the link is also the only one in the row at full opacity.

   ⭐ 🔴 THE COLOUR IS --accent-text SINCE 2026-09-26, WHERE IT STATED --accent,
   AND IT IS THE SAME KIND OF DEFECT THE INK FLIP EXISTS TO FIX. A cue drawn on
   a ground the bar no longer owns has to survive that ground, and the accent
   at rest does not. Measured, #FF4F45 against each ground the bar flies over:

     --band-void     6.46:1  ✅     --band-paper   2.83:1  ❌  under 1.4.11's 3:1
     --band-carbon   4.90:1  ✅     --band-steel   1.96:1  ❌
     --band-graphite 4.54:1  ✅     --accent       1.00:1  ❌  the accent on itself

   --accent-text is the token whose ENTIRE JOB is that: theme/build-tokens.mjs
   floors it at 4.5:1 per ground with `raiseHex`, and theme/tokens.css re-binds
   it to the accent at rest inside every dark scope. So this one name resolves
   to #FF4F45 on void, carbon and graphite — byte-identical to what shipped, so
   the dark chrome does not change at all — and to #99302A on paper (6.49:1) and
   steel (4.50:1). The red band is the one ground no accent-derived value can
   mark, and it is handled where that exception already lives: `.band--red`
   re-binds --accent-text to --ink-on-accent (6.27:1) — see the rule there.

     void 6.46 · carbon 4.90 · graphite 4.54 · paper 6.49 · steel 4.50 · red 6.27

   Every ground now clears 4.5:1, i.e. the TEXT floor, on a cue that only owes
   3:1. The same one-name change fixes the focus ring on the red band for free,
   because base.css rings in --accent-text too.

   ⚠️ IT REACHES THE SIX BAND-LESS PAGES AS WELL, and that is deliberate rather
   than collateral: their bar is over --band-paper, where the old --accent was
   the 2.83:1 in that table. The visible consequence is that the current-page
   mark on /features, /releases and /support is the darker brick red instead of
   the brand red — a real change to look at, flagged as such, and revertible in
   one word if the owner prefers the bright mark and accepts 2.83:1 on a
   supplementary cue. There is no current-page mark on the home page at all:
   wireframe.yml's `nav:` registry has no Home item, so nothing in the bar
   carries `aria-current` there. This rule is therefore correct-by-construction
   work for the day it does, not something visible on the page it was found on.

   🔴 THE SCRIPT PUTS THE GROUND'S OWN --accent-text ON THE BAR, which is what
   makes the per-ground values above real rather than theoretical: without the
   flip, `.site-header[data-band="void"]` would pin the void scope's #FF4F45
   over all five home-page grounds and two of the six numbers would be the
   failures in the first table. Do not restate a hex here to "fix" that.

   🔴 ABSOLUTE, AND THAT IS ABOUT `--frame-header-h`, NOT ABOUT AESTHETICS.
   The border it replaces was part of the link's BORDER BOX: `.nav` is a flex
   row at the default `align-items: stretch`, so the one bordered link made
   EVERY link 20.59px tall and the bar 48.59px — while base.css's
   --frame-header-h arithmetic (2 x 14 + 11 x 1.6 + 2) computes 47.6px and does
   not count a border. The bar was a pixel taller than the offset every `#`
   anchor is scrolled by. An out-of-flow mark adds nothing to the box, so the
   links are a uniform 19.59px, the bar measures 47.59px, and the existing
   arithmetic is now EXACTLY right rather than one pixel short. Verified by
   measurement at 1440 x 900, /features — see the note in base.css.

   A 1px full-width rule became an 18px mark centred under the word. That is
   deliberate: a rule under a label is the same object as the label's own
   underline, while a mark centred beneath it reads as a stamp — and at
   --arc-w-nav the stroke is 1.64px, so it is still a hairline in weight. */
.nav a[aria-current="page"]::after {
  position: absolute;
  left: 50%;
  /* Flush with the link's bottom edge, which is where the border used to draw
     — so the cue did not move, only its shape. The 2px `padding-bottom` above
     is what holds it off the descenders and it is still doing that job. */
  bottom: 0;
  transform: translateX(-50%);
  width: var(--arc-w-nav);
  background: var(--accent-text);
}

/* The theme mount point. In single and paired modes nothing ever renders into
   it — an empty seam should cost no layout, so it collapses rather than
   reserving a gap that never gets used. */
.site-theme-picker:empty { display: none; }

.site-footer {
  /* ⭐ NO TOP BORDER, AND THE ARGUMENT CHANGED SHAPE ON 2026-09-26 WITHOUT
     CHANGING ITS ANSWER. It used to read "the band above the footer is void
     and so is the footer, so a rule between them would be the only line on
     the page drawn for its own sake". The two grounds now DIFFER
     (--band-graphite under --band-carbon), so there is a real edge where
     there wasn't one — which is the whole point of the change, and which
     makes a drawn rule even less defensible: the tonal step IS the divider,
     exactly as it is at every band boundary above. Ornament, by rule 2. */
  border-top: 0;
  /* The floor's quiet tier, stated once for everything inside it — the same
     per-ground variable the bands set (THE QUIET TIER, below). 0.62 of --ink
     on --band-graphite measures 5.85:1, against 6.17:1 on the carbon it
     replaces, so the chrome does not change weight across the new edge and
     the number did not need to move with the ground. Every selector in this
     block reads it rather than restating a literal; they all stated 0.62
     before, which was the same value by coincidence rather than by contract. */
  --plate-alpha: 0.62;
}
.site-footer-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-5) var(--band-pad);
}

/* ── the floor's two groups ──────────────────────────────────────────────────

   ⭐ 2026-09-26. `.footer-groups` is ONE full-width flex row inside the
   footer's existing wrap-flow, so the floor reads top to bottom as: the
   groups, the legal row, the colophon. `width: 100%` is what forces that
   break — `.site-footer-row` is already `flex-wrap: wrap`, so a 100%-wide
   item claims its own line and everything after it starts a new one. No
   second grid, no media query, and the phone shape falls out of the same
   `flex-wrap` rather than being declared twice.

   THE TWO GROUPS ARE NOT COLUMNS OF ONE LIST. One is a nav of three product
   pages; the other is a plate about a different app. They sit side by side on
   a wide viewport and stack on a narrow one, and `flex-basis` is what decides
   where: the link list asks for the width of its longest word and no more,
   and the sibling plate asks for its own reading measure and takes the slack. */
.footer-groups {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-5);
  margin-bottom: var(--space-4);
}
.footer-group { flex: 0 1 auto; }
/* 26rem ≈ the 52ch the mono tier's own prose measure works out to (see
   `.footer-note`), so the plate asks for exactly the width it can use and
   wraps under the links the moment the viewport cannot give it that. */
.footer-sibling { flex: 1 1 26rem; min-width: 0; }

/* A real heading, in the chrome tier. It is a LABEL — the same stamped-plate
   register as the nav and the band captions (DESIGN.md), not a section title
   in the display face, which at the foot of a page would out-shout the
   closing band directly above it. Full --ink rather than --plate-alpha: the
   two labels are what make the groups scannable, and a label quieter than the
   links under it inverts the hierarchy.

   `text-transform` lowercases for DISPLAY only, exactly as `.wordmark` does
   and for the same reason — the display register is lowercase, while the DATA
   stays capitalised so `{{app.name}}` is still "Riffmix" in the <title>, the
   JSON-LD and the og:site_name, none of which pass through this stylesheet. */
.footer-group-title {
  margin: 0 0 var(--space-2);
  font-weight: 500;
  text-transform: lowercase;
}

.footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.footer-links a { text-transform: lowercase; }

/* The sibling plate's prose. 52ch is not chosen here — it is `.band-sub`'s
   measured cap, reused: the mono tier's `ch` is 6.6px at --chrome-size, and
   that value was swept against the authored subs and against the 350px a
   390px viewport leaves inside --band-pad. One prose measure for the whole
   site's chrome tier; a second one invented here would drift from it. */
.footer-note {
  margin: 0;
  max-width: 52ch;
  opacity: var(--plate-alpha);
}

.legal-nav { display: flex; gap: var(--space-3); flex: 1; }
.footer-links a,
.legal-nav a { opacity: var(--plate-alpha); }
.footer-links a:hover,
.legal-nav a:hover { opacity: 1; }
/* The current page is at full strength in the floor for the same reason it is
   in the bar: `aria-current="page"` carries the fact programmatically, and the
   opacity is the supplementary, non-text cue that agrees with it. */
.footer-links a[aria-current="page"] { opacity: 1; }
.colophon { opacity: var(--plate-alpha); width: 100%; }

/* ════════════════════════════════════════════════════════════════════════════
   THE BAND SYSTEM
   ════════════════════════════════════════════════════════════════════════════

   One class, four grounds, and everything a band can hold. Which ground a band
   wears is decided in wireframe.yml by the section's `class:` and passed
   through the layout's `#{plot@class!}` — the LAYOUT has no opinion about
   colour, because the same statement band is paper on one page and steel on
   the next. That is the existing `class: band-alt` convention, generalised.

   ── THE QUIET TIER, AND WHY IT IS A PER-GROUND VARIABLE ──────────────────────

   Mono chrome is drawn at reduced alpha so it recedes from the object. The
   alpha CANNOT be one number: it composites against the band's own ground, and
   the same 0.62 that measures 6.92:1 on void measures 3.58:1 on steel — a
   straight AA failure at 11px. So `--plate-alpha` is set per ground, and every
   selector that draws chrome reads it rather than stating its own.

   Measured 2026-09-25, all against the band each value is used on, all for
   11px type where AA asks 4.5:1. Note --ink is a DIFFERENT COLOUR on the void
   band (#E3E3E1, re-bound by theme/tokens.css) than on the light ones
   (#111111) — one name, two polarities, which is exactly why these selectors
   can all say `var(--ink)`:

     void   --ink           @ 0.62   6.32:1  ✅
     paper  --ink           @ 0.68   6.19:1  ✅
     steel  --ink           @ 0.68   5.18:1  ✅
     red    --ink-on-accent @ 1.00   6.27:1  ✅

   ── 🔴 THE RED BAND IS NEAR-BLACK, NOT WHITE, AND THAT IS A MEASUREMENT ──────

   The approved mockup sets the red band's type in #fff. It does not clear AA,
   and neither does the obvious token substitute:

     #ffffff          on --accent   3.25:1   large text only, and only just
     --band-paper     on --accent   2.83:1   ❌ fails even the 3:1 large floor
     #fff @ 0.55      on --accent   1.90:1   ❌ the mockup's plate, badly
     --ink-on-accent  on --accent   6.27:1   ✅ passes BOTH large and small

   DESIGN.md's instruction for this exact situation is "when a tier fails,
   darken the tier — do not shrink its usage and do not argue the aesthetics."
   --ink-on-accent (#050505) is the token theme/tokens.css already emits for
   type on an accent GROUND, so nothing is hand-picked here. It is also the one
   place on the site where a band names a colour token other than --ink, and
   that is unavoidable: the void re-binding covers black grounds, and the red
   band is neither black nor light. Black on hot red is the more honest
   industrial register anyway — it is what a warning plate looks like. */

.band {
  position: relative;
  width: 100%;
  display: grid;
  place-items: center;
  padding: var(--band-air) var(--band-pad);
  /* 🔴 NO GROUND HERE, AND THAT IS DELIBERATE. Every band must name one, with
     a `class:` in wireframe.yml or hard-coded in its layout, because the
     ground is not only paint: `.band--void` is one of the three selectors
     theme/tokens.css keys the whole text vocabulary's polarity on. A default
     ground on this rule would paint a band black without flipping its type,
     which is a contrast failure that renders as a perfectly normal-looking
     band and that no tool reports. Failing to a transparent band on the
     page's own paper ground is the safe direction: legible, obviously wrong. */
  --plate-alpha: 0.62;
  /* THE DISPLAY MEASURE — the only thing controlling where a statement breaks.
     No <br> and no lines-array in content/copy.json: a break position is
     markup owning copy, which site/CLAUDE.md forbids.

     🔴 6.8ch IS MEASURED IN THE SHIPPED FACE, WITH THE WEBFONT LOADED, and
     that is why it is not the 13ch that was simulated. `ch` is the width of
     "0", and Cabinet Grotesk 800 is a heavy grotesk whose zero is 0.64em —
     far wider than the ~0.5em a simulation assumes. 13ch resolves to 730px at
     the desktop size, which is wider than three of the four statements and
     breaks none of them.

     Rather than compute it, the cap was SWEPT: `max-width` was stepped from
     4.0ch to 18.0ch in 0.1 increments and the resulting lines read back off
     the live text with a Range, per statement. The windows that produce the
     authored break exactly:

       made       "you made / something"     [6.0, 12.5]
       finish     "a riff you / can finish"  [5.8,  7.7]   ← the upper bound
       scale      "no wrong / notes"         [5.8,  9.3]
       cta-final  "hear one. / make one."    [6.2,  9.2]   ← the lower bound

     The intersection is [6.2, 7.7] and 6.8 sits inside it with 0.6ch of margin
     below and 0.9ch above. ONE value serves all four statements, so no band
     needs an override — see the note below .band-say for the copy change that
     made that true.

     ⚠️ MEASURE WITH THE FONT LOADED. The first pass of this was taken before
     Cabinet Grotesk arrived and read `ch` as 46.27px instead of 53.25px — a
     15% error, enough to put every statement on three lines and to send the
     cap to a value that was wrong for the page anyone would actually see.

     Stated in `ch` rather than px so it tracks the fluid font-size — verified
     holding at 390px, where the clamp bottoms out at 36.8px. */
  --say-cap: 6.8ch;
}

/* Bands are BUTTED, edge to edge, no gutter. base.css spaces `main > section`
   by --space-6 for the capped reading pages; a band cancels it. Same
   specificity as the rule it overrides, and this file is concatenated after
   base.css, so source order decides — which is the documented contract for
   src/css/*.css (pipeline/emit.mjs concatenates in filename order). */
main > .band { margin: 0; }

/* ⚠️ `.band--void` IS ALSO A THEME HOOK. theme/tokens.css re-binds --ink,
   --accent-text, --link, --border and --border-bright inside it (and inside
   `[data-band="void"]` / `.band-void`, its two accepted twins). Renaming this
   class, or inventing a fourth spelling for a black band, silently gives that
   band the light-ground ink ladder on a black field — a straight AA failure
   that looks fine in a screenshot and measures fine in axe. If a fourth
   spelling is ever genuinely needed, it goes in VOID_BAND_SELECTORS in
   theme/build-tokens.mjs; it never gets its colours restated here. */
.band--void {
  background: var(--band-void);
  color: var(--ink);
  --plate-alpha: 0.62;
}

/* ⭐ THE SECOND DARK GROUND — 2026-09-25, OWNER DECISION.
   `sign` (a void artifact band) and `cta-final` (the void closing band) butt
   against each other, and two true-black full-bleed fields have NO VISIBLE
   BOUNDARY: the bottom third of the page read as one enormous black run. The
   note-arc seam below exists for exactly that case and was not carrying the
   weight — a 28 x 3.81px mark at --plate-alpha against a whole band edge.

   Neither band could go light. `sign` holds a capture of a pitch-black app,
   which has to dissolve into a `#000000` ground or it reads as a black CARD
   (rule 1, and DESIGN.md's corner-pixel measurement). `cta-final` holds the
   spray wordmark, which needs a DARK ground — DESIGN.md: "rendered on
   --band-paper its yellow/lime third nearly disappears" — but not a PURE one.
   So the closing band got a ground of its own, one shade up.

   🔴 AND THIS IS NOT THE "NEAR-BLACK SHOWS A RECTANGLE" FAILURE DESIGN.md
   WARNS ABOUT, for two independent reasons. (1) That failure is about a band
   holding a BLACK-CORNERED RASTER; this band holds transparent art (the
   wordmark PNG fallback measures alpha 1 at all four corners, checked). (2) A
   card is a shape INSIDE a field; this is the field itself, running edge to
   edge, so there is no rectangle for an eye to find — a full-bleed tonal step
   reads as the next band, which is the whole vocabulary of this page.

   The value is #222220 and it was chosen against three constraints, not
   picked:
     · DISTINCT. L* 13.16 against black's L* 0.00 — a tonal step you cannot
       miss at a band edge. (Contrast RATIO is the wrong metric this deep:
       #222220 is 1.32:1 on black, which sounds like nothing and looks like a
       clear change, because ratio compresses hard at the dark end. L* is the
       perceptual scale and it is the one this value was chosen on.)
     · STILL DARK ENOUGH FOR THE MARK. The wordmark's lime/yellow third is what
       dies on a light ground; at L 0.0159 this is 98% of the way from paper to
       black and the mark reads exactly as it does on the void bands.
     · ABOVE ALL THE FLOORS, WITH MARGIN. The binding tier is --accent-text,
       which on a dark band is the accent AT REST (#FF4F45): 4.5:1 needs a
       ground under L 0.0217 and this is L 0.0159, so the generator's raiseHex
       floor is a NO-OP here rather than something that would have quietly
       repainted the brand red on one band. Every tier's measured ratio is in
       the table below.
     · It keeps the band family's own neutral-warm relationship — B = R-2, the
       same as --band-paper (#EFEFED) and within a point of --band-steel
       (#C9C9C6, B = R-3) — so it reads as a member of this palette and not as
       a grey borrowed from somewhere else.

   MEASURED 2026-09-25 on #222220, every tier the closing band and the footer
   actually draw (AA asks 4.5:1 for text, 1.4.11 asks 3:1 for a non-text cue):

     --ink            #EFEFED   13.84:1  ✅  the statement, the secondary CTA
     --text-secondary #B5B5B3    7.76:1  ✅
     --text-muted     #A8A8A6    6.69:1  ✅
     --accent-text    #FF4F45    4.90:1  ✅  focus rings, and the floor's no-op
     --border-bright  #EFEFED   13.84:1  ✅  1.4.11 cue tier
     plate @ 0.62     #A1A19F    6.17:1  ✅  the sub, the colophon, the inert
                                              CTA, both footer link rows

   Read off the live page by compositing each element's own computed colour and
   opacity over the band's computed background, the same way the header halo's
   table was — not by eye, and not by axe, which would have measured the light
   ladder's values if this band had been left out of DARK_BAND_SCOPES and told
   you they were fine.

   🔴 THE PLATE ALPHA STAYS 0.62 AND THAT IS A MEASUREMENT, NOT AN INHERITANCE.
   The quiet tier composites against the band's own ground, which is why
   --plate-alpha is a per-ground variable at all (see THE QUIET TIER above).
   0.62 of --ink over #222220 is #A1A19F, 6.17:1 — within 0.13 of the 6.30:1 it
   measures on the void band, so the same number is correct on both and the
   chrome does not change weight across the boundary. (It lands that close
   because --ink is itself re-bound one step brighter on this ground, #EFEFED
   against the void's #E3E3E1, which is the mirrored ladder doing its job.)

   ⚠️ THE GROUND IS HALF THE STORY. The other half is theme/tokens.css, which
   re-binds the entire ink vocabulary inside `.band--carbon` (and its two
   `[data-band="carbon"]` / `.band-carbon` twins) — the list lives in
   DARK_BAND_SCOPES in theme/build-tokens.mjs. Painting a band dark WITHOUT
   being in that list gives it the light ladder on a dark field, and axe and
   Lighthouse both report that as fine. Never restate those values here. */
.band--carbon {
  background: var(--band-carbon);
  color: var(--ink);
  --plate-alpha: 0.62;
}
.band--paper {
  background: var(--band-paper);
  color: var(--ink);
  --plate-alpha: 0.68;
}
.band--steel {
  background: var(--band-steel);
  color: var(--ink);
  --plate-alpha: 0.68;
}
.band--red {
  background: var(--accent);
  color: var(--ink-on-accent);
  --plate-alpha: 1;
  /* ⭐ 🔴 THE ACCENT'S JOB PASSES TO --ink-on-accent ON THIS ONE GROUND —
     2026-09-26, and the rule below this block has been saying it in a narrower
     form since the band was built: `:focus-visible` already forced
     --ink-on-accent here, because "--accent-text on --accent is the accent
     against itself". That is a statement about the GROUND, not about focus
     rings, so it belongs on the ground — re-bound once, where the exception is
     made, rather than overridden at every site that reads the token.

     It became load-bearing with the ink flip: the bar now copies the ground's
     own --accent-text (src/js/header-ink.js), so this is what the current-page
     arc and the focus ring resolve to while the bar is over this band.
     Measured: #99302A on #FF4F45 is 2.30:1 ❌ and
     --ink-on-accent (#050505) is 6.27:1 ✅.

     ⚠️ SEMANTIC TOKEN TO SEMANTIC TOKEN, no hex, and the generator is not
     involved: theme/build-tokens.mjs's per-scope blocks cover the DARK grounds
     (DARK_BAND_SCOPES), and this ground is neither dark nor light — it is the
     one documented exception on this site, and this file is where it is made.
     Nothing else inside a red band reads --accent-text: the band carries a
     statement, a sub and two plates, all of which inherit `color` above. */
  --accent-text: var(--ink-on-accent);
}

/* base.css rings focus in --accent-text and that is now correct on THREE of
   the four grounds with no help from this file: theme/tokens.css floors it at
   4.5:1 against the light bands and re-binds it to the accent at rest
   (#FF4F45, 6.46:1) inside the void scope.

   The RED band is the one ground the token ladder cannot cover, because it is
   neither light nor black: --accent-text on --accent is the accent against
   itself. --ink-on-accent is the token emitted for type on this ground and it
   measures 6.27:1, twice 1.4.11's 3:1 floor for a non-text indicator. Width
   and offset stay the global rule's; only the hue changes, and only here. */
.band--red :focus-visible { outline-color: var(--ink-on-accent); }

/* ── the statement ───────────────────────────────────────────────────────── */

/* DESIGN.md: "Display — Cabinet Grotesk (800). Set lowercase, tracking about
   -0.035em, line-height under 1. Used sparingly and only on reading bands."
   Sparingly is structural here — there are three statement bands on the whole
   site and the layout can render exactly one statement each. */
.band-say {
  /* 🔴 THIS IS AN <h2> (2026-09-30), AND THE TWO LINES BELOW ARE WHAT THAT COST.
     It was a <p>, which meant the home page's whole argument — seven statements,
     one per band — was invisible to the document outline: one hidden <h1> and
     nothing else. The MARKDOWN TWIN had been emitting `## {{copy.statement}}`
     all along, so the two renderings of the same page disagreed about its
     structure, and the markdown one was right. This site's stated priority is
     AI-first (DESIGN.md), and an agent reading the HTML got a flat page.

     ⚠️ base.css's `h1, h2, h3` rule now applies, and it sets two things this
     element must NOT have:

       `text-wrap: balance` — forbidden here, see the note further down. It
       fights --say-cap for control of the break and wins unpredictably. That
       prohibition predates the <h2> and was written about a <p> that could
       never have inherited it; as an <h2> it inherits it by default, so the
       comment alone no longer enforces anything and the property has to be
       stated back.

       `color: var(--text-bright)` — the statement takes the band's own --ink,
       and a heading colour would quietly lift it off every scrim it sits on. */
  text-wrap: wrap;
  color: inherit;
  margin: 0;
  max-width: var(--say-cap);
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: lowercase;
  letter-spacing: var(--tracking-display);
  line-height: 0.92;
  font-size: clamp(2.3rem, 7.4vw, 5.2rem);
  text-align: center;
  /* 🔴 NO `text-wrap: balance`. It would fight --say-cap for control of the
     break and win unpredictably at some widths, which is the one thing a
     deliberately-tuned ch measure cannot tolerate. The cap IS the balancer. */
}

/* ✅ NO PER-BAND OVERRIDE, AND THE ABSENCE IS THE RESULT OF A COPY FIX.
 *
 * `.band--closing` carried its own `--say-cap: 10.4ch` for one build, because
 * the closing statement read "hear one, then make one" and that phrase cannot
 * break at its comma AT ANY CAP: line two, "then make one", is 9.41ch wide
 * while line one, "hear one, then", is only 9.16ch, so any cap wide enough to
 * hold the second line is already wide enough to pull "then" up onto the
 * first. The sweep found no value between 5.0ch and 18.0ch that produced it.
 *
 * That measurement went back to the copy owner and the LINE changed rather
 * than the number: it now reads "hear one. make one." — two parallel
 * imperatives whose candidate lines are near-identical widths. Swept fresh
 * against the new string, its window is [6.2, 9.2], which overlaps every other
 * statement's, so the page default covers all four and the special case is
 * gone.
 *
 * 🔴 THE EDITING RULE THIS LEAVES BEHIND, recorded in content/copy.json too:
 * LINE TWO MUST BE NO WIDER THAN LINE ONE. A statement that violates it has no
 * cap that can break it, and no amount of tuning here will find one. The fix
 * is always the sentence. */

.band-sub {
  margin: var(--space-3) 0 0;
  /* 52ch, measured, not chosen. The mono tier's `ch` is 6.6px at
     --chrome-size, and the longest authored sub — "ios 17 or later.
     testflight for now." — is 36ch / 293px. The cap was 44ch (290.4px) and
     missed it by 2.6px, which wrapped one word onto a second line and left
     "now." orphaned under the closing statement. 52ch is 343px: comfortably
     over every authored sub, still narrow enough to cap a runaway line at a
     readable mono measure, and still inside the 350px a 390px viewport leaves
     between the --band-pad insets, so the fix holds on a phone too. */
  max-width: 52ch;
  font-family: var(--font-mono);
  font-size: var(--chrome-size);
  letter-spacing: var(--tracking-mono);
  line-height: 1.6;
  text-align: center;
  opacity: var(--plate-alpha);
}

/* ── the plates ──────────────────────────────────────────────────────────── */

/* A caption stamped in a corner like a plate on a machine, never a caption
   under a picture. Absolutely positioned so it belongs to the BAND rather than
   to the object — the object floats in the middle with nothing attached to it,
   which is what makes it read as an object rather than as a figure.
   `pointer-events: none` because a plate is a label, and on the hero band the
   whole field is one stretched link that a label must not punch a hole in. */
.band-plate {
  position: absolute;
  font-family: var(--font-mono);
  font-size: var(--chrome-size);
  letter-spacing: var(--tracking-mono);
  line-height: 1.6;
  opacity: var(--plate-alpha);
  pointer-events: none;
  /* Two plates can share a band (`wall` carries both). Capping each at just
     under half the band stops a long one from running under its opposite
     number on a phone — they collide invisibly otherwise, because absolute
     positioning cannot push. */
  max-width: calc(50% - var(--band-pad) * 1.5);
}
.band-plate--bl { left: var(--band-pad); bottom: var(--band-pad); }
.band-plate--br { right: var(--band-pad); bottom: var(--band-pad); text-align: right; }

/* The spec column — a mono block reading DOWN, like a serial plate stamped on
   a machine. `line-height: 2.2` is the whole look: the lines are deliberately
   far apart so the block reads as a spec plate and not as a paragraph.

   ⚠️ IT WAS `top: 50%; transform: translateY(-50%)` — PINNED LEFT-CENTRE — AND
   THE OWNER MOVED IT, 2026-09-25, from looking at the live site: centred on
   the left edge it read as a label floating beside the disc, where every other
   piece of chrome on this site is a plate stamped in a band's CORNER. So it
   joins the `.band-plate--bl` grammar: same corner, same --band-pad column as
   the nav above it and the footer below it.

   This generic rule is the NON-HERO placement (the frame-grid branch of
   layouts/showcase-band.yml). The hero overrides it completely — see
   `.band--hero .band-spec`, which takes it out of absolute positioning
   altogether, because in the hero the bottom-left corner already belongs to
   the reel's caption and the two have to stack rather than collide. */
.band-spec {
  position: absolute;
  left: var(--band-pad);
  bottom: var(--band-pad);
  margin: 0;
  max-width: none;
  font-family: var(--font-mono);
  font-size: var(--chrome-size);
  letter-spacing: var(--tracking-mono);
  line-height: 2.2;
  opacity: var(--plate-alpha);
  pointer-events: none;
}
.band-spec span { display: block; }

/* ── the artifact ────────────────────────────────────────────────────────── */

/* One or two screenshots, floating, with nothing drawn around them. No frame,
   no border, no radius, no shadow — a device mockup or a rounded card here
   would undo the entire section.

   ⚠️ NO `background` ON .band-shot. The captures are of a pitch-black app and
   they sit on a true-black band; any ground at all behind one would show at
   the moment the file is still loading as a rectangle of a DIFFERENT black,
   which is the card this system forbids, appearing and then vanishing. */
.band-objects {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(16px, 3.4vw, 44px);
  justify-content: center;
  align-items: center;
  width: 100%;
}

.band-shot {
  width: min(66vmin, 470px);
  max-width: 100%;
  height: auto;
  border-radius: 0;
}

/* A band holding TWO shots sizes them down so the pair still reads as one
   object with air around it rather than as a crowded row. Derived from the
   CONTENT, with `:has()`, rather than from a count the layout would have to
   pass: the band renders however many frames its showcase carries, and the
   only honest source for "how many" is how many arrived. Without `:has()` both
   shots render at the single-shot size and wrap to two rows — larger than
   intended, still correct, still legible. */
.band-objects:has(.band-shot + .band-shot) .band-shot {
  width: min(40vmin, 310px);
}

/* ── the wordmark image, place 2 of 2: THE CLOSING BAND ──────────────────────

   The site signing itself — the app's pride-of-authorship moment, borrowed.
   layouts/cta-final.yml argues the placement and the empty alt; this is only
   how big it is.

   🔴 SIZED AGAINST THE STATEMENT IT SITS OVER, NOT AGAINST THE VIEWPORT ALONE.
   `.band-say` is `clamp(2.3rem, 7.4vw, 5.2rem)` capped at --say-cap (6.8ch,
   which resolves to about 362px at the desktop size). A wordmark WIDER than
   the statement block turns the closing band into a logo with a caption under
   it; a much narrower one reads as a stray sticker. `min(58vw, 380px)` lands
   it just over the statement's own measure at every width, so the two stack as
   one object. The vw term is what keeps that true on a phone, where --say-cap
   is measured in a much smaller `ch`.

   ⚠️ NO `aspect-ratio`, SO THERE IS NO VERTICAL BOX UNTIL THE ASSET LOADS. The
   file does not exist yet (it is the artwork pass's contract path) so its ratio
   cannot be stated here honestly, and `height: auto` on a width-sized image
   reserves nothing — the statement below it will shift down once on first
   paint. That is a real, measurable CLS cost and the fix is DATA, not CSS: once
   the SVG exists, bind its intrinsic `width`/`height` onto the <img> in
   layouts/cta-final.yml, exactly as layouts/artifact-band.yml already does for
   every screenshot. Stated here rather than left as a silent defect.

   The band is `display: grid; place-items: center`, so this is its own row and
   centres itself; the margin is the only spacing it needs. */
.band-wordmark {
  display: block;
  margin: 0 0 var(--space-4);
}
.band-wordmark img {
  width: min(58vw, 380px);
  height: auto;
  max-width: 100%;
}

/* ── the closing actions ─────────────────────────────────────────────────── */

.band-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: center;
  align-items: center;
  margin-top: var(--space-4);
}

.band-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;              /* 44px — the tap-target floor */
  padding: 0 var(--space-3);
  border: 1px solid transparent;
  border-radius: 0;
  /* 🔴 THE DISPLAY FACE, NOT THE MONO ONE (owner, 2026-09-30: "the text in the
     buttons is hard to read"). It was --font-mono at the chrome tier with
     --tracking-mono's 0.14em, and three things were stacking against it: a
     monospace face gives every letter the same width so word-shapes flatten,
     0.14em pulls those letters apart until the word stops reading as one unit,
     and the primary button sets all of it on a saturated --accent ground where
     thin mono strokes have the least support. Cabinet Grotesk 800 is heavy by
     construction, so the strokes hold at this size and on that ground.

     ⚠️ THE TRACKING HAD TO GO WITH THE FACE, and that is the half that
     actually fixes it — 0.14em is a MONO-tier value (it exists to stop
     IBM Plex Mono's caps closing up), and inherited onto a heavy grotesk it
     reads as a headline that has come apart. Display tracking is negative on
     this site; a button is small, so this sits at 0 rather than at the
     statement's -0.035em.

     Size steps up with the face. The old +1px existed to keep a control from
     measuring SMALLER than the captions beside it once --chrome-size went
     fluid; that relationship still holds and the button just sits further up
     it, because display type at button scale can carry the extra. */
  font-family: var(--font-display);
  /* 🔴 STATE THE WEIGHT. Cabinet Grotesk ships to this site as ONE face —
     theme/webfonts.json loads `cabinet-grotesk@800` and nothing else — so an
     undeclared `font-weight` computes to 400, asks for a face that was never
     loaded, and survives only because CSS font-matching happens to fall back to
     the single available one. Measured: a 400 and an 800 request render the
     same 319px string, distinct from both fallback widths, so the real face IS
     being used either way. It is still a coincidence to depend on — the day a
     400 is added to the stylesheet, every button on the site silently goes
     light. .band-say states 800 for the same reason. */
  font-weight: 800;
  font-size: calc(var(--chrome-size) + 3px);
  letter-spacing: 0;
  text-decoration: none;
  color: inherit;
  transition: background var(--motion-duration) var(--motion-ease),
              border-color var(--motion-duration) var(--motion-ease),
              color var(--motion-duration) var(--motion-ease);
}

/* 🔴 RANK WITHOUT ORNAMENT. The closing band carries two intents and the
   primary has to read as the stronger one — with no gradient, no shadow and no
   bubble radius available, because all three are forbidden. The levers left
   are FILL and OUTLINE, which is the oldest and flattest answer there is: the
   primary is a solid field of the one accent, the secondary is a hairline.
   --ink on --accent measures 5.85:1. Note the accent is used as a GROUND here
   and never as small type — DESIGN.md's --accent-text exists for the latter
   and is the wrong token for a fill. */
.band-cta--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink-on-accent);
}
/* Hover empties the fill rather than reaching for a second hue: the accent
   survives as the border, the label reverts to the band's own type colour
   (18.2:1 on void). One hue, two states. */
.band-cta--primary:hover {
  background: transparent;
  color: inherit;
}

.band-cta--secondary { border-color: currentColor; }
.band-cta--secondary:hover { border-color: var(--accent); }

/* 🔴 AN INTENT WITH NOWHERE TO POINT IS PLAIN TEXT, NOT A DEAD BUTTON.
 *
 * `get-riffmix` has `href: null` today — there is no App Store listing and no
 * public TestFlight URL — so pipeline/model.mjs's queryIntents resolves it
 * `isLink: false` and the layout renders a <span> instead of an <a>. The owner's
 * instruction is that it must not look like a control that goes nowhere, and
 * the accessibility answer agrees: it is not focusable, it carries no ARIA
 * disabled state (that would announce a disabled widget where there is no
 * widget), and it is not styled as a box. It is a sentence, in the chrome tier,
 * saying what is true.
 *
 * The day `href` becomes a real URL, `isLink` flips, the identical label
 * renders as `<a class="band-cta band-cta--primary">`, and nothing else moves. */
.band-cta--inert {
  min-height: 0;
  padding: 0;
  border-color: transparent;
  opacity: var(--plate-alpha);
}

/* ════════════════════════════════════════════════════════════════════════════
   showcase-band — THE HERO (layouts/showcase-band.yml + content/showcase.json)
   ════════════════════════════════════════════════════════════════════════════

   design.md licenses exactly one piece of real motion on this site — "a
   Riffmix spinning" — and this is it, now as the opening band: the reel
   oversized and bleeding off both edges of a void field, a mono spec column
   pinned left, and no headline anywhere near it.

   The clip carries its OWN pitch-black ground and full-saturation rainbow; it
   is never recoloured toward the site palette. */

/* 🔴 THE HERO IS NEVER THE FULL VIEWPORT, AND THE RULE IS STRUCTURAL.
   Owner instruction, 2026-09-25: "the hero must NEVER be 100vh — the next band
   must always be partially in view, so the page reads as continuing."
   --hero-peek (base.css) is the chosen amount and carries the derivation;
   80-132px of the paper `made` band is always under the fold.

   ⚠️ THE OLD SUM IS GONE AND IT HAD TO GO. It read `100vh -
   var(--frame-header-h)`, which was not a peek — it was the band getting out
   of the way of an IN-FLOW bar that pushed it down by exactly that much, so
   the hero still filled the visible area and nothing showed beneath it. The
   bar is `position: fixed` now and reserves no height at all, so subtracting
   its height would have been subtracting a number for a reason that no longer
   exists, and it would have coincidentally produced a peek of whatever the nav
   happened to measure that week. The peek is now a decision with a name.

   `100svh` and not `100vh`: on iOS Safari `vh` is the LARGEST viewport
   (toolbars retracted), so a `100vh - peek` hero overflows the visible area
   while the toolbar is showing and the peek vanishes at exactly the moment the
   visitor first looks at it. The `vh` line above is the fallback for engines
   without `svh` and must stay first. */
.band--hero {
  min-height: calc(100vh - var(--hero-peek));
  min-height: calc(100svh - var(--hero-peek));
  /* The band's own air would inset the reel; the hero's object is supposed to
     run past the edges. The plates position against the band box regardless,
     so they keep their --band-pad inset. */
  padding: 0;
  overflow: hidden;
}

/* The figure fills the band, and that is what makes the stretched link below
   cover the whole field. Being absolutely positioned, it is the containing
   block for BOTH the link's ::after overlay and the pause button — the
   relationship src/js/reel-pause.js documents and depends on.
 *
 * 🔴 ONE GRID CELL, EVERY CHILD STACKED IN IT — AND NOTHING INSIDE IS
 * `position: absolute`. This is not a styling preference, it is the fix for a
 * real defect found by hit-testing this exact build.
 *
 * The obvious way to put the caption in the bottom-left corner is
 * `position: absolute` on the figcaption. Do that and THE STRETCHED LINK
 * SILENTLY STOPS WORKING: the anchor lives inside the figcaption, so an
 * absolutely positioned figcaption becomes the anchor's containing block, and
 * `::after { inset: 0 }` then covers the CAPTION rather than the field.
 * Measured with `document.elementFromPoint` at the reel's centre: it returned
 * `.showcase-reel-video`, i.e. the link's hit area had collapsed to a few
 * characters of text and the whole point of the pattern was gone. Nothing
 * visible changes when this breaks, which is why it has to be hit-tested
 * rather than eyeballed.
 *
 * Placing every child into the SAME `grid-area: 1/1` gives the corner
 * placement with `place-self` and no positioned ancestor in between, so the
 * overlay resolves against the figure and covers the field. The pause button
 * is unaffected: it IS absolutely positioned, which takes it out of the grid
 * flow entirely, and it positions against the figure exactly as before. */
/* The stage is inert outside the hero — `display: contents` removes it from
   the box tree entirely, so a frame-grid or a differently-framed reel mounted
   on this layout sees exactly the DOM it saw before the element existed. */
.showcase-reel-stage { display: contents; }

.band--hero .showcase-reel {
  position: absolute;
  inset: 0;
  margin: 0;
  max-width: none;
  display: grid;
  /* ⚠️ `minmax(0, 1fr)`, NOT `1fr`, and the zero is doing real work. `1fr` is
     shorthand for `minmax(auto, 1fr)`, and that `auto` minimum means the track
     cannot shrink below its items' min-content size. The reel is deliberately
     OVERSIZED — `min(128vmin, 1240px)`, i.e. 1152px tall in a 900px viewport —
     so the row inflated to 1152 and the caption, placed at the row's end, was
     parked 300px below the band and clipped away by `overflow: hidden`.
     Measured: the caption's top sat at y=1142 inside a band ending at y=900.
     A zero minimum pins the track to the figure's own height, and the reel
     overflows the CELL instead of stretching it — which is exactly the bleed
     this band wants.

     ⚠️ THREE ROWS SINCE 2026-09-25, NOT TWO. Row 1 is the clipped stage, row 2
     is the spec column and row 3 is the caption. The spec column moved into
     this grid when the owner moved it to the bottom-left corner: the corner
     already held the caption, so the two have to STACK, and the only way to
     stack them without encoding the caption's pixel height somewhere is to put
     both in the same track list and let the browser measure. Both `auto` rows
     take exactly their content; the `minmax(0, 1fr)` stage absorbs the rest,
     so adding the row costs the reel some height and costs the layout no
     magic numbers. */
  grid-template: minmax(0, 1fr) auto auto / minmax(0, 1fr);
}

/* 🔴 THE HERO'S SPEC COLUMN IS A GRID ITEM, NOT A POSITIONED PLATE, AND THAT
   IS THE CONTRAST FIX — see the long note in layouts/showcase-band.yml.
   Absolutely positioning it at `bottom: var(--band-pad)` would have been one
   line and would have put 11px mono back inside the stage's clip box, i.e. on
   the spinning rainbow: measured 3.57:1 against a purple lane arc, and
   invisible to every contrast tool because the ground is a video frame. In row
   2 it is BELOW the clip, on the band's own --band-void, at every viewport and
   with no media query holding it there.

   `position: static` is stated rather than assumed: the generic rule above is
   `position: absolute`, and an absolutely positioned grid item is taken out of
   flow — it would still paint over the stage and the row would collapse to
   zero, which looks like the rule did nothing. */
.band--hero .band-spec {
  position: static;
  grid-row: 2;
  justify-self: start;
  /* The same --band-pad column as the nav above it.
     ⚠️ THE BOTTOM MARGIN IS --band-pad SINCE 2026-09-25, NOT --space-1. It used
     to be the 8px gap to the caption plate stacked under it, and --band-pad was
     the caption's own bottom inset. The caption lost its words (owner decision)
     and its row collapsed to zero, so this plate is the lowest thing in the
     corner and has to carry the band's inset itself — otherwise it sits 8px off
     the band's bottom edge instead of 20px, out of the --band-pad column every
     other plate on the page shares. The 8px gap went with the plate it
     separated; nothing is left to stack against. */
  margin: 0 var(--band-pad) var(--band-pad);
  max-width: calc(100% - var(--band-pad) * 2);
}

/* 🔴 THE STAGE IS THE CLIP, AND IT IS WHAT KEEPS 11px TYPE OFF A SPINNING
 * RAINBOW.
 *
 * The reel is oversized on purpose and something has to cut it. Letting the
 * BAND do the cutting (`overflow: hidden` on .band--hero, which is still there
 * for the horizontal bleed) means the reel also runs underneath the caption in
 * the bottom-left corner — and that corner is INSIDE the disc, not in the
 * square's black corner: at 1440x900 the caption's box maps to a point 478px
 * from the disc's centre against a 576px radius. Measured by drawing the live
 * video to a canvas and sampling every 4px under the caption: the brightest
 * pixel was a purple lane arc at #5C1962, which puts the caption's quiet tier
 * at 3.57:1 — an AA failure at 11px, and one that NOTHING would report,
 * because the ground is a video frame and every contrast tool measures a
 * computed background-color.
 *
 * The stage gives row 1 its own clipping box, so the reel is cut at the
 * caption's top edge instead of the band's bottom edge — about 40px higher,
 * visually indistinguishable — and the caption sits on --band-void with
 * nothing behind it. `min-height: 0` is required: a grid item's automatic
 * minimum size would otherwise stop the row from shrinking below the reel and
 * there would be nothing to clip. */
.band--hero .showcase-reel-stage {
  /* ⚠️ `position: relative` + an absolutely centred reel, NOT `display: grid;
     place-items: center`. An overflowing item inside an `overflow: hidden` box
     is CLAMPED TO THE START EDGE — the classic overflow-centring trap — so the
     grid version bled only right and only down: measured at 1440x900 the disc's
     centre sat 179px below the stage's, cropped at the bottom and untouched at
     the top, which reads as a mistake rather than as a crop. Absolute centring
     has no start-edge clamp, so the reel overflows symmetrically on both axes
     and the field crops it evenly, which is the approved rendering. */
  /* ⚠️ `display: block` RESTATED, and it is not redundant. The base rule two
     blocks up sets `display: contents` so the stage is invisible outside the
     hero; `display: contents` removes the element from the box tree entirely,
     which means `position`, `overflow` and `grid-row` on it are all silently
     ignored. Measured before this line: the stage's own
     getBoundingClientRect() was 0x0 at 0,0 and the reel was positioning
     against the figure instead, so nothing was clipped at all. */
  display: block;
  position: relative;
  grid-row: 1;
  min-height: 0;
  overflow: hidden;
  /* ⭐ THE STAGE IS A SIZE CONTAINER SINCE 2026-09-25, and that is what lets
     the reel be sized off THE HERO BOX rather than off the viewport — see the
     reel's own rule below for the whole argument. `size` (not `inline-size`)
     because the number the reel needs is `100cqh`, the stage's HEIGHT, and an
     inline-size container does not publish one.

     Size containment is safe here by construction: this element's height comes
     from the figure's `minmax(0, 1fr)` grid row and its width from the single
     column, so it was already laid out independently of its contents — which
     is exactly what `overflow: hidden` and `min-height: 0` above are for.
     Nothing inside it has ever been allowed to set its size. */
  container-type: size;
  container-name: reel-stage;
}

/* ⭐ THE OBJECT IS AN INLINE <svg> SINCE 2026-09-25 — it was a <video> plus a
 * poster <img> twin. `.showcase-reel-vector` is written onto the root <svg> by
 * pipeline/model.mjs's inlineVector as it reads the file in; there is no
 * `.showcase-reel-video` and no `.showcase-reel-still` on the page any more,
 * and no poster to hand a motion-off twin. The geometry below is UNCHANGED —
 * the same absolute centring, the same 128vmin bleed — because none of it ever
 * had anything to do with the element being a media element.
 *
 * 🔴 IT IS INLINED, NOT `<img src="….svg">`, AND THAT IS NOT NEGOTIABLE. An
 * <img> renders the drawing into a CLOSED shadow tree: no rule in this file can
 * reach a node inside it, `--rm-play` cannot be set from outside, and the WCAG
 * 2.2.2 pause control further down becomes impossible to build.
 *
 * ⚠️ THE DRAWING CARRIES ITS OWN <style> AND IT IS DOCUMENT-SCOPED. An inline
 * SVG's <style> is not sandboxed — its rules and its @keyframes join this
 * stylesheet's cascade. Everything in the asset is `rm-` prefixed for exactly
 * that reason; do not introduce an `.rm-*` class here and do not assume a
 * selector in this file cannot reach inside the drawing (it can, which is the
 * whole point).
 *
 * 🔴 `background: transparent` and `border-radius: 0`, and BOTH are
 * corrections, not defaults.
 *
 * This element used to carry `background: var(--scrim)` and `border-radius:
 * var(--radius-lg)`, which were right for the cream band it was written for
 * and are wrong here twice over. The reel is a SQUARE of true #000000 — the
 * video's corner pixels were sampled with ffmpeg and measured exactly that, and
 * the SVG that replaced it opens with a full-frame `<rect fill="#000000">` — so
 * a rounded corner lets the band show through four wedges, and a scrim ground a
 * few points off #000 paints a visible rectangle behind it. Either one renders
 * the reel as a card floating on the field, which is the defect DESIGN.md's
 * whole measurement section exists to prevent.
 *
 * No aspect-ratio literal either: the asset's real shape is DATA. The root
 * <svg> carries its own `viewBox` and intrinsic `width`/`height` (1080 x 1080,
 * left alone by the inliner), so `height: auto` resolves the ratio off the
 * asset. Swapping in a differently-shaped drawing needs no CSS edit.
 *
 * ════════════════════════════════════════════════════════════════════════════
 * ⭐ THE FRAME FOLLOWS THE HERO BOX — 2026-09-25, OWNER INSTRUCTION
 * ════════════════════════════════════════════════════════════════════════════
 *
 * "Scale back to fit the full record where the area allows, and where the
 * aspect ratio leaves no room for the whole disc, crop into the RIGHT side
 * rather than shrinking it." The reference for what that looks like is the
 * app's own zoomed-in deck posture — _proto/ipad-screens/
 * deck-notes-empty-head-beads-zoomed-in.webp: the disc's centre sits off-canvas
 * to the LEFT, the lane arcs sweep in from the left edge, and the horizontal
 * head-bead row stays in view toward the right. Not an invention; the app's
 * real behaviour.
 *
 * ── WHAT WAS THERE BEFORE, AND WHY IT COULD NOT DO THIS ─────────────────────
 * `width: min(128vmin, 1240px)`: a square sized off the VIEWPORT's short edge
 * and centred, deliberately oversized so the field cropped it. Two problems,
 * both the owner's. It cropped at EVERY size, so the whole record was never
 * visible even on a 1440-wide band with 375px of empty black either side of the
 * disc. And when it did crop it cropped SYMMETRICALLY — a ring cut evenly off
 * all four sides, which is a vignette, not the app's composition. `vmin` also
 * could not see the hero box: the band's height is `100svh - peek` minus a spec
 * plate whose height moves with --chrome-size, and no viewport unit knows that.
 *
 * ── THE RULE, IN TWO LINES, AND WHY IT PRODUCES BOTH BEHAVIOURS ─────────────
 * `width: 100cqh` sizes the drawing's FRAME to the stage's own HEIGHT — the
 * hero box, measured, not a viewport approximation. The asset is square, so
 * the frame is then a square of exactly the stage's height, and the disc
 * (which is 87.87% of the frame: the `rm-spin` group's bbox measures 949 of
 * 1080) is 0.8787 x stage height with ~6% of air above and below it.
 *
 *   LANDSCAPE STAGE (height <= width) — the square fits inside the box, so
 *   NOTHING IS CROPPED. The full record, floating, with the leftover width as
 *   empty field either side. That is "scale back to fit the full record", and
 *   it is also DESIGN.md's "Field" literally: one object, a lot of space.
 *
 *   PORTRAIT STAGE (height > width) — the square is WIDER than the box by
 *   construction, so it overflows horizontally and the stage's `overflow:
 *   hidden` cuts it. Nothing is cropped vertically at any aspect, because the
 *   frame's height always equals the stage's. The crop is horizontal only —
 *   exactly like the reference frame.
 *
 * ── AND THE CROP GOES RIGHT, WHICH IS THE OTHER HALF OF THE ASK ─────────────
 * The `@container` block below right-aligns the square when (and only when)
 * the stage is taller than it is wide, so the square's RIGHT edge meets the
 * stage's and the overflow falls off the LEFT. The disc's centre lands at
 * `stageW - stageH/2`, i.e. at or past the left edge, and the bead row runs
 * from there toward the right with the outermost bead inside the field. At
 * 390 x ~660 that puts the centre 60px INSIDE the left edge with the beads
 * ending ~100px short of the right one: the reference composition.
 *
 * 🔴 WHY A CONTAINER QUERY AND NOT A MEDIA QUERY. The thing being tested is
 * the shape of the STAGE, and the stage is the hero minus the peek minus the
 * spec plate — three terms a viewport query cannot see. The file already
 * carries the archaeology of getting this wrong with viewport units twice
 * (`max-width: 48rem` missed the iPad; `max-aspect-ratio: 1/1` missed short
 * landscape). A container query tests the box itself, so it cannot miss.
 *
 * ⚠️ AND IT DEGRADES IN THE SAFE DIRECTION. An engine without container size
 * queries drops `100cqh` as an invalid value and falls back to the `width:
 * 100%` declaration immediately above it — the drawing then fits the stage's
 * WIDTH, centred, whole, legible, just smaller than intended on a phone. The
 * `@container` block never matches, so the crop simply does not happen. No
 * blank hero, no stretched disc, no @supports needed.
 *
 * ── ⚠️ THE ASSET-SIDE TWIN, AND WHY IT IS NOT WHAT SHIPS TODAY ──────────────
 * The natural home for this is the drawing's own
 * `preserveAspectRatio="xMaxYMid slice"` with the element simply filling the
 * stage — one attribute doing what this rule does with a box. pipeline/
 * record-svg/ has grown a `--aspect` flag that emits exactly that, and its
 * README documents the pairing. It is NOT in the shipped
 * assets/showcase/hero-riffmix.svg because regenerating that file is outside
 * this pass's write lease (assets/ has its own owner), and neither
 * pipeline/model.mjs's inliner nor any CSS property can add the attribute
 * after the fact. The two are compatible by construction, which is the point:
 * with a SQUARE element box `slice` and `meet` resolve identically, so the day
 * the asset is regenerated this rule keeps producing the same frame and the
 * attribute becomes the belt to its braces. Do not delete one on discovering
 * the other. */
.band--hero .showcase-reel-vector {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* The fallback, and the only thing a no-container-query engine sees. */
  width: 100%;
  width: 100cqh;
  max-width: none;
  height: auto;
  /* An <svg> is inline-level by default, so without this it sits on a baseline
     and the box picks up descender space. Absolute positioning hides most of
     it; stating `block` means the box is the drawing and nothing else. */
  display: block;
  background: transparent;
  border-radius: 0;
}

/* THE RIGHT-SIDE CROP. `left: 100%` + `translateX(-100%)` puts the square's
   right edge on the stage's right edge; the vertical half of the transform is
   unchanged because there is never a vertical crop to bias.

   The condition is the one the owner stated — "where the aspect ratio leaves
   no room for the whole disc" — written as the box question it actually is:
   is the stage taller than it is wide? Below 1:1 the square (side = height)
   cannot fit the width, so something must be cut; at or above 1:1 it fits and
   nothing is. There is no tuned breakpoint here and there must not be one: the
   threshold is where the geometry changes, not where a phone happens to end. */
@container reel-stage (aspect-ratio < 1) {
  .band--hero .showcase-reel-vector {
    left: 100%;
    transform: translate(-100%, -50%);
  }
}

/* ⚠️ THE PAUSE LEVER, AND IT IS ONE DECLARATION BY DESIGN — the SVG's own
   published contract (pipeline/record-svg/README.md). Every animated group in
   the drawing reads `var(--rm-play, running)` as its `animation-play-state`:
   `.rm-spin` for the disc, `.rm-flash`/`.rm-f<lane>` for the head beads'
   playback flash. Setting the custom property ONCE on the root lets it inherit
   to all of them, so they stop and start TOGETHER AND IN PHASE — which is why
   src/js/reel-pause.js toggles a class and does nothing else.

   `animation-play-state: paused` FREEZES, it does not reset, so resuming
   continues mid-revolution. That is deliberate: it is what `<video>.play()`
   after `.pause()` did, and a reel that snapped back to frame 0 on resume would
   read as a glitch.

   🔴 SCOPED TO `.showcase-reel-vector`, NOT TO `svg`. The README writes the
   contract as `.reel.is-paused svg` for brevity, but the pause BUTTON contains
   two inline glyph <svg>s of its own and a bare descendant selector would set
   the property on those too. Harmless today — they carry no animation — and
   exactly the kind of accident that stops being harmless later. */
.showcase-reel.is-paused .showcase-reel-vector { --rm-play: paused; }

/* ⭐ THE CAPTION CARRIES NO VISIBLE WORDS AND TAKES NO HEIGHT — 2026-09-25,
 * owner decision. It held two lines of 11px mono: the reel's note ("One
 * Riffmix, exported from the app exactly as it plays.") and the one link's
 * text. variant-R's hero carried only the spec plate and the ↓, so both went.
 *
 * MEASURED AT 1440x900: the corner block was 158px (spec 121 + caption 37.2)
 * and is now 121px. With the 8px inter-plate gap that went with it, the stage —
 * and therefore the disc — gets 45px of band height back, and the reel's
 * vertical crop falls from 42.2% to 38.2%.
 *
 * 🔴 THE ELEMENT ITSELF STAYS, AND STAYS STATIC. It is the grid row the
 * stretched link lives in, and all three obvious ways to "clean it up" are
 * defects:
 *   - `display: none` removes the link from the page AND from the accessibility
 *     tree, i.e. deletes the band's only affordance;
 *   - `position: absolute` makes it the anchor's containing block, so
 *     `::after { inset: 0 }` covers the CAPTION instead of the field — the
 *     measured defect written up in the red block above;
 *   - deleting the <figcaption> and leaving the bare <a> as a grid item works
 *     only until something gives the anchor a position, at which point it is
 *     the same bug with fewer places to notice it.
 * Zero height via `height` + `line-height`, with the element in normal flow, is
 * the version that costs nothing and breaks nothing.
 *
 * `pointer-events` is deliberately NOT `none` here (unlike .band-plate): the
 * link inside is the whole field's hit area.
 *
 * The `.showcase-reel-note` and `.showcase-reel-link` rules that used to sit
 * under this one are DELETED, not left inert — there is no note element any
 * more, and `color`/`text-decoration` on an anchor with no text is a rule that
 * reads as live and does nothing. */
.band--hero .showcase-reel-caption {
  /* Its own grid row, below the stage AND below the spec column. NOT
     `position: absolute` — see above. Row 3 since 2026-09-25, when the spec
     column took row 2; the row is now zero-tall, which is why the stage's clip
     boundary is the spec plate's top edge rather than this element's. */
  grid-row: 3;
  justify-self: start;
  margin: 0;
  height: 0;
  line-height: 0;
  z-index: 0;
}

/* 🔴 PLACED HERE, AFTER THE HERO RULES, AND THAT POSITION IS LOAD-BEARING.
   A media query adds NO specificity. This block and the `.band--hero
   .showcase-reel-video` rule above it have identical selectors, so source
   order is the only tiebreak — and when this lived up in the band-system
   section it lost silently: the spec column moved to the top-left on a phone
   while the reel stayed at its full 128vmin, which is the half of the fix
   that does nothing on its own. Measured before the move: the reel was 500px
   wide at 390 (i.e. still 128vmin) with the row treatment already applied.
   Keep it below the rules it overrides. */
/* ⚠️ NARROW SCREENS — AND HALF OF THIS BLOCK'S ORIGINAL JOB NO LONGER EXISTS.
 *
 * ── WHAT IT USED TO DO, AND WHY THAT IS RETIRED ─────────────────────────────
 * The spec column used to be an absolutely positioned plate at left-centre, so
 * on any viewport where the reel was WIDER than the band — which is every
 * phone, and the iPad — the column landed directly on the spinning disc. Type
 * on a moving rainbow is a contrast pairing no tool can measure and no value
 * of --plate-alpha can make safe. This query fixed it by relocating the column
 * to a top-left ROW and reserving a strip for it with a 5rem `margin-top` on
 * the stage.
 *
 * 🔴 BOTH OF THOSE ARE GONE, because the column is now a GRID ITEM under the
 * stage (see `.band--hero .band-spec`). It is below the clip at every viewport,
 * so there is no aspect ratio at which it can meet the disc, and there is
 * nothing left to relocate or to reserve room for. A structural fix retired a
 * media query, which is the right direction of travel. Deleting the rules
 * rather than leaving them inert: `top`/`right`/`transform` on a static grid
 * item do nothing, and an inert rule reads as a live one to the next person.
 *
 * ⚠️ THE ARCHAEOLOGY IS KEPT BECAUSE THE BREAKPOINT IS STILL LOAD-BEARING and
 * two earlier attempts at it were wrong in instructive ways. `max-width: 48rem`
 * misses the iPad: at 834 x 1112 the viewport is 834 wide — well over 48rem —
 * but its vmin is 834, so a 128vmin reel is 1067px square in a 1064px band and
 * swallows the field. `max-aspect-ratio: 1/1` then missed short landscape: at
 * 1024 x 768 the gutters are only 20px wide. The condition that actually holds
 * is "is the band wide enough that (100vw - reel) / 2 leaves a real gutter",
 * and 3/2 is where that becomes true for every common desktop size — checked
 * at 1280x800, 1440x900, 1500x1000, 1512x982 and 1600x900. 🔴 DO NOT REPLACE
 * THIS WITH A WIDTH QUERY. The thing being tested is the relationship between
 * a vmin-sized square and the band, and width alone cannot see it.
 *
 * ── WHAT IT STILL DOES, AND WHY EACH HALF EARNS ITS KEEP ────────────────────
 *
 * 1. ⚠️ THE REEL TRIM (`128vmin → 104vmin`) IS ALSO GONE NOW, 2026-09-25, and
 *    for the same kind of reason the relocation retired the other half: the
 *    defect it existed to soften does not exist any more. It was there because
 *    the reel was sized in `vmin` and OVERSIZED ON PURPOSE, so on a narrow band
 *    the crop ate the disc's left and right extremes instead of the square's
 *    black corners, and the trim traded bleed for less of that. The reel is
 *    sized off the STAGE's own height now (`100cqh`) and cropped deliberately
 *    toward the right, so the crop is the composition rather than a cost to be
 *    minimised — a second, viewport-keyed width here would silently fight the
 *    container query for the same property. Deleted rather than left inert, on
 *    this block's own stated principle: "an inert rule reads as a live one to
 *    the next person."
 *
 * 2. THE SPEC COLUMN LIES DOWN. Set ACROSS instead of down on a narrow band:
 *    five lines at line-height 2.2 is 121px of a phone's field, and the corner
 *    block is now two plates deep (spec + caption). The same five strings, one
 *    wrapping row, ~85px of the band handed back to the disc. Nothing is
 *    hidden and nothing shrinks below the mono tier's floor — only the axis
 *    changes. It stays in grid row 2, in the corner, where it was put.
 *
 *    ⚠️ AND IT IS NOW LOAD-BEARING FOR THE REEL, WHICH IT WAS NOT BEFORE.
 *    The reel's frame is the STAGE's height, and the stage is the hero minus
 *    whatever this plate takes. Standing the column up again on a phone would
 *    cost the stage ~85px and shrink the disc by the same 85px — so this is no
 *    longer only about the plate's own footprint. */
@media (max-aspect-ratio: 3 / 2) {
  .band--hero .band-spec {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.4em;
    line-height: 1.6;
  }
  .band--hero .band-spec span { display: inline; }
}


/* THE STRETCHED LINK. The reel itself is the affordance — you watch a silent
   Riffmix spin, you tap it, you hear a real one play (GOALS.md: "Sound is the
   reward for a tap, never the entry condition"). The hit area is a
   pseudo-element, NOT an invisible anchor laid over the video, so the
   accessible name stays the caption's own visible words. An unnamed link is
   WCAG 4.1.2 and this site has already shipped one of those once.

   `:first-of-type` because the band is contracted to bind exactly one intent:
   if a second ever appears it renders as an ordinary text link rather than a
   second full-field overlay fighting the first for the same pixels.

   ⚠️ THAT CONTRACT IS THIS BAND'S ALONE and must not be copied to the closing
   band, which binds two intents on purpose and renders them as two real
   controls. Nothing is stretched there. */
.showcase-reel-link:first-of-type::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 0;
}

/* Focus follows the hit area, not the words — base.css rings the anchor's own
   few characters, which the overlay covers, so the ring would appear to sit
   behind the picture. The colour is inherited from base.css, which rings in
   --accent-text — re-bound to the accent at rest (#FF4F45) inside the void
   scope, 6.46:1 on black. `outline-offset` is NEGATIVE here where the global
   rule is positive: the overlay is flush with the viewport on a full-bleed
   band, so a ring drawn OUTSIDE it is clipped away on all four sides and the
   focus state becomes invisible. */
.showcase-reel-link:first-of-type:focus-visible { outline: none; }
.showcase-reel-link:first-of-type:focus-visible::after {
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
}

/* ── showcase-band ▸ the REEL's PAUSE CONTROL (WCAG 2.2.2, Level A) ──────── */

/* The clip is 7.000 s of `autoplay loop` beside a caption and a link, which is
   precisely what 2.2.2 asks a page-level control for — and nothing catches it:
   axe's autoplay rule keys on an AUDIO track and these files are silent by
   construction, so the page measures clean while failing.
   `prefers-reduced-motion` (below) is a complement, not a substitute: an OS
   opt-in most visitors never set is not a control on the page.

   🔴 `z-index: 1` IS THE WHOLE INTERACTION FIX, and it is one line because of
   what it is fighting. `.showcase-reel-link:first-of-type::after` covers the
   entire figure at `inset: 0` with NO z-index of its own, so it sits at the
   bottom of the positioned stacking order; a positioned sibling that states
   one paints — and hit-tests — above it while covering only its own box. The
   link still takes every other pixel of the field, and because the button is
   appended to the FIGURE rather than inside the anchor, a click on it cannot
   bubble into a navigation; no event-handler workaround is involved.

   ⚠️ After touching this rule OR the ::after above it, re-check both hit
   targets with `document.elementFromPoint`: the button at the button's centre,
   `a.showcase-reel-link` at the field's centre, corners and caption.

   ⚠️ TOP OFFSET CLEARS THE STICKY BAR. The hero is the first band and the
   chrome shell is pinned over its top edge; at a plain `var(--space-2)` the
   control sat underneath the nav. Keyed off --frame-header-h so it follows the
   bar rather than guessing at it. Bottom-right was the alternative and is
   taken — that corner is the hero's own mono plate. */
.showcase-reel-pause {
  position: absolute;
  z-index: 1;
  top: calc(var(--frame-header-h) + var(--band-pad));
  right: var(--band-pad);
  /* 44px, the same tap-target floor .band-cta states. */
  width: 2.75rem;
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  /* Square, not a circle. DESIGN.md allows exactly one round thing on this
     site and it is the record. A 50% radius here was the old direction's. */
  border-radius: 0;
  cursor: pointer;
  /* The control sits on media this theme does not own, and the band under it
     is true black, so it is drawn in band tokens rather than page ones: an
     opaque --band-void fill guarantees the glyph's contrast whatever frame is
     behind it (--ink on --band-void, 16.34:1), and the 1px --ink border
     carries the control's own boundary at the same ratio — well over 1.4.11's
     3:1 — without recolouring a single pixel of the product. --ink is the void
     scope's value here because the button lives inside the hero band and
     custom properties inherit. */
  background: var(--band-void);
  color: var(--ink);
  border: 1px solid var(--ink);
  transition: background var(--motion-duration) var(--motion-ease);
}
.showcase-reel-pause:hover {
  background: var(--ink);
  color: var(--band-void);
}

.showcase-reel-glyph {
  width: 1rem;
  height: 1rem;
  fill: currentColor;
  /* So `document.elementFromPoint` at the button's centre returns the BUTTON,
     not the <svg> inside it. The click worked either way (the svg is a child,
     the event bubbles), but the invariant this file and src/js/reel-pause.js
     both ask you to re-verify is stated in terms of the button, and a check
     that has to be interpreted is a check that eventually gets waved through. */
  pointer-events: none;
}
/* Both glyphs ship and one is hidden, so the toggle is a paint rather than a
   re-parse and the button's box cannot reflow under a finger mid-tap. The
   attribute is written by the script off the VIDEO'S OWN `paused` property,
   never off a cached boolean — autoplay can be refused and OS media keys can
   pause the element behind the page's back. */
.showcase-reel-pause[data-state="playing"] .showcase-reel-glyph--play,
.showcase-reel-pause[data-state="paused"] .showcase-reel-glyph--pause { display: none; }

/* ⭐ showcase.json's `vector.reducedMotion: "static-first-frame"` — AND NOTE
   WHAT THIS BLOCK NO LONGER DOES.
   It used to swap a <video> for its poster <img> (`"static-poster"`). THE
   ASSET HANDLES ITSELF NOW: the generated SVG carries its own
   `@media (prefers-reduced-motion: reduce)` block, which sets `animation: none`
   — holding the disc at `rotate(0deg)`, and that IS frame 0 because no rotation
   is baked into the path data — and takes the flash overlays to `opacity: 0`.
   The result is a clean static first frame with no swap, no second file and no
   handoff to get wrong. DESIGN.md's stated fallback, delivered by the object
   rather than by the page.

   🔴 SO DO NOT ADD A COMPETING RULE HERE. Anything site-side that touches the
   drawing's animation under this media query is fighting the asset for the
   final angle, and the asset is the one that knows which angle is frame 0.
   (base.css's global `*, *::before, *::after` reduced-motion rule is not a
   competitor: it clamps duration and iteration count, and the asset's shorthand
   has already taken `animation-name` to `none`, so nothing runs either way.)

   What is left is the control, and it is not a duplicate of the script's own
   guard — src/js/reel-pause.js asks the disc for its computed `animation-name`
   and builds no button when there is nothing to pause. This rule is the belt to
   that braces: it holds even if the script never runs, and it costs one line.

   🔴 KEPT SEPARATE from the `[data-loop="true"]` rule further down, and they
   must not be merged. "Stop cycling a grid of frames" and "hold a turning
   drawing at its first frame" are two behaviours of two different showcase
   kinds; one selector covering both would couple them, and the frame grid's
   rule has to keep working for any future showcase mounted on this layout. */
@media (prefers-reduced-motion: reduce) {
  .showcase-reel[data-reduced-motion="static-first-frame"] .showcase-reel-pause { display: none; }
}

/* ── showcase-band ▸ the FRAME GRID (kind: animation | screenshots) ───────── */

/* 🔴 NO LIVE USER ON THIS SITE, DELIBERATELY KEPT. The home page's screenshots
   moved to layouts/artifact-band.yml when the bands landed, so nothing
   currently mounts a frame-grid showcase. The branch survives in
   layouts/showcase-band.yml and so does its styling, for one reason that is
   worth the ~15 lines: the `prefers-reduced-motion` rule below is one of the
   two the site is contracted to preserve, and deleting the selectors it needs
   would quietly delete a stated accessibility behaviour rather than retire it.

   Flattened to the band system's rules — no card, no border, no radius — so
   that if it is ever mounted again it lands as a grid of bare frames rather
   than as a grid of panels from the old direction. */
.showcase-frames {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: clamp(16px, 3.4vw, 44px);
  width: 100%;
}

.showcase-frame {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.showcase-frame h3 { margin-bottom: var(--space-2); }

/* Intrinsic width/height attributes are bound onto these (anti-CLS, same as
   the reel). base.css's `img { max-width: 100% }` has no `height: auto` beside
   it, so without this the attributes would hold the rendered height at the
   full pixel value and squash every capture. */
.showcase-shot {
  width: 100%;
  height: auto;
  border: 0;
  border-radius: 0;
}

/* content/showcase.json's `reducedMotion: static-first-frame`, expressed
   declaratively: when the visitor asks for less motion, a looping band shows
   only its first frame instead of cycling. No JS reads the preference.
   🔴 Separate from the reel's rule above — see the note there. */
@media (prefers-reduced-motion: reduce) {
  .showcase-band[data-loop="true"] .showcase-frame:not(:first-child) { display: none; }
}

/* ── the eyebrow (frame grid, feature blocks) ────────────────────────────── */

.eyebrow {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--chrome-size);
  letter-spacing: var(--tracking-mono);
  text-transform: lowercase;
  opacity: var(--plate-alpha, 0.68);
}

/* ── buttons (layouts/issue-report.yml's submit) ─────────────────────────── */

/* The reading pages are not bands, and the one control they carry is the issue
   reporter's submit. Same shape language as .band-cta — flat, square, and now
   DISPLAY-faced — so the site has one button and not two.

   ⚠️ IT MOVED WITH .band-cta ON PURPOSE (2026-09-30). That rule went from mono
   to Cabinet Grotesk because the owner could not read it; leaving this one on
   mono would have given the site exactly the two different buttons this comment
   exists to forbid, and the divergence would only show on /support — the page
   least likely to be looked at again. The reasoning for each value is on
   .band-cta and is not repeated here. */
.btn, .badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;              /* 44px — the tap-target floor */
  padding: 0 var(--space-3);
  border: 1px solid transparent;
  border-radius: 0;
  font-family: var(--font-display);
  font-weight: 800;                 /* the only Cabinet Grotesk face loaded — see .band-cta */
  font-size: calc(var(--chrome-size) + 3px);
  letter-spacing: 0;
  text-decoration: none;
  transition: background var(--motion-duration) var(--motion-ease),
              border-color var(--motion-duration) var(--motion-ease),
              color var(--motion-duration) var(--motion-ease);
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink-on-accent);
}
.btn--primary:hover {
  background: transparent;
  color: var(--ink);
}

.btn--ghost { border-color: currentColor; color: inherit; }
.btn--ghost:hover { border-color: var(--accent); }

.badge { border-color: currentColor; opacity: 0.68; }
/* A store badge with nowhere to point yet. It still carries its label, because
   an unlabelled control fails WCAG 4.1.2 whether or not there is anything to
   click — cta.json's own note makes that call. */
.badge--coming-soon { border-style: dashed; cursor: default; }

/* ── steps ───────────────────────────────────────────────────────────────── */

.steps-list {
  counter-reset: step;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-4);
}

/* 01 / 02 / 03 comes from a CSS counter, not from the markup and not from the
 * build — see layouts/steps.yml. `decimal-leading-zero` is the whole feature.
 * Generated content is not in the accessibility tree, which is right here: the
 * <ol> already announces the position, and a screen reader should not have to
 * hear "zero one" before every step title. */
.step { counter-increment: step; }
/* ⚠️ --accent-text, and this selector is WHY that token exists.
 *
 * 13px of accent-coloured type on the page ground: 3.01:1 on the teenager pole,
 * against AA's 4.5:1 for anything under 24px. It shipped that way because NO
 * TOOL CAN SEE IT — axe and Lighthouse both evaluate elements, and generated
 * content has none; both passed this page while flagging the identical colour
 * on .showcase-reel-link two sections above. Caught 2026-09-25 by reading
 * `getComputedStyle(el, '::before')` directly. Anything drawn on a
 * pseudo-element has to be checked by hand, permanently. */
.step::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--accent-text);
  margin-bottom: var(--space-1);
}
.step-title { margin-bottom: var(--space-1); }
.step-detail { color: var(--text-secondary); margin: 0; }

/* ── feature-grid ────────────────────────────────────────────────────────── */

/* A per-placement band, set by wireframe.yml's `class: band-alt` and passed
 * through the layout's `#{plot@class!}`. The layout has no opinion about it —
 * the same grid on another page may want no band at all.
 *
 * ⚠️ NO USER TODAY. The home page's feature grid was replaced by the band
 * stack. Flattened anyway — radius 0, no elevation ground — so that if it is
 * mounted again it lands in this system rather than reintroducing the rounded
 * panel from the previous direction. */
.band-alt {
  border-block: 1px solid var(--border);
  padding: var(--space-4) var(--space-3);
  border-radius: 0;
}

.feature-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-3);
}

/* 🔴 NOT A CARD ANY MORE. DESIGN.md's "Field" forbids a container with its own
   ground sitting on another ground, and a rounded, bordered, filled box is the
   canonical one. A single top rule per entry separates them instead — the same
   convention .release-entry and .legal-section already use, so the reading
   pages have ONE separator idiom rather than three. */
.feature-card {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
  border-radius: 0;
}
.feature-card:first-child { border-top: 0; padding-top: 0; }
.feature-card h3 { margin-bottom: var(--space-1); font-size: 1rem; }
.feature-card p { margin: 0; color: var(--text-secondary); }

/* ── feature-list ────────────────────────────────────────────────────────── */

.feature-entries { margin: 0; }
.feature-entry {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.feature-entry:first-child { border-top: 0; padding-top: 0; }
.feature-entry dt {
  font-family: var(--font-display);
  font-size: 1.125rem;
  color: var(--text-bright);
  margin-bottom: var(--space-1);
}
.feature-entry dd { margin: 0; color: var(--text-secondary); max-width: var(--measure); }

/* ── release-notes ───────────────────────────────────────────────────────── */

.platform-section { margin-bottom: var(--space-5); }
.platform-title { font-size: 1.25rem; }

.release-entries { list-style: none; margin: 0; padding: 0; }
.release-entry {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}

.release-head {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  font-size: 0.8125rem;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}
.release-head .build-number { font-family: var(--font-mono); }

.highlights { margin: 0 0 var(--space-2); }
.highlights dt { font-weight: 600; color: var(--text-primary); }
.highlights dd { margin: 0 0 var(--space-2); color: var(--text-secondary); max-width: var(--measure); }

/* The sanctioned record of a maintenance re-ship — an empty `highlights` in
 * release-notes.json. Stated in words, not left blank. */
.maintenance { color: var(--text-muted); font-style: italic; margin: 0; }

/* ── contact ─────────────────────────────────────────────────────────────── */

.contact-email {
  font-family: var(--font-mono);
  font-size: 1.125rem;
}

/* ── issue-report (site/worker/issue-reporter) ───────────────────────────── */

.report-intro { color: var(--text-secondary); max-width: var(--measure); }

.report-form fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* 🔴 NO OPACITY ON THE DISABLED FIELDSET, AND THE ABSENCE IS THE FIX.
 *
 * This rule used to read `opacity: 0.55` — "muted rather than hidden, so the
 * shape of the form is still legible before it's usable". The intent was
 * right and the mechanism was wrong, because opacity composites EVERYTHING
 * inside the fieldset against the band, prose included. Measured on
 * --band-paper: 0.55 put the 15px field labels at 4.01:1 and the anonymity
 * hint at 3.5:1, both under AA's 4.5:1, and the audit flagged 13 separate
 * strings. Raising it to 0.8 fixed the labels and left the hint at 4.19 and
 * four <option>s at 4.12 — the classic shape of a fix that is really a
 * negotiation.
 *
 * WCAG 1.4.3 exempts an INACTIVE user interface component, so the controls
 * were arguably fine either way. The LABELS and the HINT are not controls —
 * they are prose that happens to sit inside a fieldset — and there is no
 * reading of the SC that exempts them. So the dimming goes, and the disabled
 * cue is the one the platform already draws: `disabled` on the fieldset, which
 * every user agent renders in its own greyed treatment, plus the status line
 * and the <noscript> text that say in words why nothing can be filed yet.
 * Measured after: every string on /support passes, the worst at 6.58:1.
 *
 * If a visual mute is ever wanted back, it must apply to the CONTROLS ONLY
 * (`fieldset[disabled] :is(input, select, textarea, .report-kind)`) and never
 * to a text node. */

.report-kind-group { display: flex; gap: var(--space-2); }
.report-kind {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border-bright);
  border-radius: 0;
  cursor: pointer;
}
.report-kind:has(input:checked) {
  border-color: var(--accent);
  color: var(--text-bright);
}

.report-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.report-label { font-weight: 600; font-size: 0.9375rem; }

.report-field input[type="text"],
.report-field input[type="email"],
.report-field textarea,
.report-field select {
  font: inherit;
  color: var(--ink);
  background: var(--band-paper);
  border: 1px solid var(--border-bright);
  border-radius: 0;
  padding: var(--space-1) var(--space-2);
  min-height: 2.75rem;              /* the same 44px tap-target floor as .btn */
}
.report-field textarea { min-height: 8rem; resize: vertical; }

.report-optional {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-3);
}

.report-hint { color: var(--text-muted); font-size: 0.875rem; margin: 0; }

/* The hosted Turnstile challenge — sized to the widget, not the reverse. */
.report-challenge iframe {
  width: 300px;
  max-width: 100%;
  height: 65px;
  border: 0;
}

.report-noscript { color: var(--text-secondary); }

.report-status {
  margin: 0;
  font-size: 0.9375rem;
  min-height: 1.5em;
}
.report-status.is-ok { color: var(--text-bright); }
/* ⚠️ Was --accent-bright, measured 2.26:1 at 15px on the bright pole — the
 * worst contrast on the site and, like .step::before, invisible to tooling
 * because the class is only ever applied by script. Kept in the ACCENT family
 * rather than moved to --error on purpose: this site's own rule is that no
 * message blames the reader, and an alarm-red status for "the check is still
 * loading" would do exactly that. --accent-text is the same voice at a
 * readable weight (5.28:1 bright / 6.31:1 dark). */
.report-status.is-error { color: var(--accent-text); }
.report-status a { color: inherit; font-weight: 600; }

/* ── legal-doc (layouts/legal-doc.yml → /privacy, /terms) ─────────────────── */

/* Long-form prose, the one place on this site where reading a wall of text is
 * the actual task. So: measure-capped lines, headings that break the wall into
 * scannable sections, and a table that stays a table rather than becoming a
 * decorated grid. Nothing here is decoration — a legal page that is pleasant to
 * read is a legal page people actually read. */

.legal-updated {
  color: var(--text-muted);
  font-size: 0.8125rem;
  margin: 0 0 var(--space-4);
}

/* The intro paragraphs sit outside any .legal-section, directly under the
 * updated line — slightly larger, as the lede. */
.legal-doc > p:not(.legal-updated) {
  max-width: var(--measure);
  color: var(--text-primary);
  font-size: 1.0625rem;
}

.legal-section {
  margin-top: var(--space-5);
  /* A rule per section, not a box: the border is the only separator, matching
   * .release-entry's top-border convention rather than inventing a card. */
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}

.legal-section h2 {
  font-family: var(--font-display);
  font-size: 1.25rem;
  letter-spacing: var(--tracking-tight);
  color: var(--text-bright);
  margin: 0 0 var(--space-2);
}

.legal-section p,
.legal-section ul {
  max-width: var(--measure);
  color: var(--text-secondary);
}

.legal-section p { margin: 0 0 var(--space-2); }

/* ── use 3: THE LIST MARKER ──────────────────────────────────────────────────

   The third plain rule the arc replaces — here a browser-drawn `disc`, the one
   piece of shape on the reading pages that nothing in this system chose. A dot
   is not neutral: it is the only circle on the site that is not the record,
   and DESIGN.md's "Shape" allows exactly one.

   🔴 `list-style: none` COSTS AN ANNOUNCEMENT IN SAFARI AND `role="list"` IS
   NOT THE ANSWER HERE. VoiceOver/WebKit drops the list semantics from a `<ul>`
   whose marker is removed, which is why the usual advice is to restate
   `role="list"`. This markup comes from layouts/legal-doc.yml rendering
   content/legal.json, and neither is this file's to edit — so the marker is
   removed with `list-style-type: ""` (an EMPTY STRING marker, CSS Lists 3)
   rather than with `none`. It suppresses the glyph identically and keeps the
   element a list in every engine, because the list-item box is still there;
   there is simply nothing in it. Supported in every browser this site targets
   and degrades to a visible default bullet, not to a broken layout, anywhere
   it is not.

   The arc goes on `li::before` rather than on `li::marker`, because `::marker`
   accepts almost no properties — no mask, no background, no position — and a
   mask is the only way to get a round-capped stroke that inherits the band's
   polarity. */
.legal-section ul {
  margin: 0 0 var(--space-2);
  padding-left: 0;
  list-style-type: "";
}
.legal-section li {
  position: relative;
  padding-left: 1.75em;
  margin-bottom: var(--space-1);
}
.legal-section li::before {
  position: absolute;
  left: 0;
  /* Centred on the FIRST line, not on the item: an item that wraps to four
     lines must keep its mark beside the line the sentence starts on. `0.8em`
     is half of the inherited 1.6 line-height, so this tracks the type rather
     than restating a pixel. */
  top: 0.8em;
  transform: translateY(-50%);
  width: var(--arc-w-list);
  /* 🔴 currentColor AT FULL STRENGTH, AND THE ABSENT `opacity` IS THE POINT.
     The mark inherits --text-secondary from `.legal-section ul` — the same
     colour as the sentence it marks, which already clears AA as TEXT and
     therefore clears 1.4.11's 3:1 as a non-text cue with a wide margin. The
     chrome tier's --plate-alpha was the obvious thing to reach for and it is
     the wrong tier: a plate is a label stamped on a band's ground, while this
     sits in a reading column at 13px wide and a 1.18px stroke. Dimming an
     already-thin hairline is how a marker becomes a smudge, and it would be
     dimming a colour that was already dimmed once. */
  background: currentColor;
}

/* ── the at-a-glance table ────────────────────────────────────────────────── */

/* Deliberately NOT measure-capped: this one is a five-column reference and
 * squeezing it to reading width is what makes data tables unreadable. */
.legal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--space-3);
  font-size: 0.9375rem;
}

.legal-table caption {
  text-align: left;
  color: var(--text-muted);
  font-size: 0.8125rem;
  margin-bottom: var(--space-1);
}

.legal-table th,
.legal-table td {
  text-align: left;
  padding: var(--space-1) var(--space-2) var(--space-1) 0;
  border-top: 1px solid var(--border);
  vertical-align: top;
}

.legal-table th {
  color: var(--text-bright);
  font-weight: 600;
}

.legal-table td { color: var(--text-secondary); }

/* Narrow screens: a five-column table cannot shrink, so it scrolls in place
 * rather than reflowing its cells away from their headers.
 *
 * ⚠️ The scroll container must be the DIV WRAPPING THE TABLE, and it must
 * carry `min-width: 0`. An earlier version put `overflow-x: auto` on
 * `.legal-doc` instead, and the whole /privacy page dragged sideways at 390px
 * — body text clipped, nav clipped, page-level horizontal scroll. The cause is
 * two rules away in base.css: `body[data-frame-footer="sticky"] > main` makes
 * `main` a FLEX ITEM, flex items default to `min-width: auto`, and so the
 * table's `min-width: 34rem` propagated all the way up as a min-content floor
 * and pushed `main` to 592px inside a 390px viewport. `min-width: 0` is what
 * stops that propagation; the overflow alone does not. Verified at 390/768/1440. */
.legal-table-scroll {
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  /* Scroll-momentum on touch, and a focusable region so a keyboard user can
   * reach the overflow — a scroll container with no keyboard path is a
   * WCAG 2.1.1 trap. */
  -webkit-overflow-scrolling: touch;
}

@media (max-width: 40rem) {
  .legal-table { min-width: 34rem; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SCENE BAND — layouts/scene-band.yml, added 2026-09-28
   ══════════════════════════════════════════════════════════════════════════

   A band whose ground is a drawn street-art scene with people in it, with the
   app's real output seated into the drawing where the art put a record, and the
   band's words set on the black above them. STORY.md, "The spine", records why
   this replaced the artifact/statement alternation; this file is only how big it
   is and how it arrives.

   ⚠️ IT DOES NOT REUSE `.band`'s CENTRED GRID. `.band` is `display: grid` +
   `place-items: center`, which shrink-wraps its children to their content and
   centres them — right for one floating object, wrong here: it made the words
   544px wide inside a 1440px band, so a full-width scrim drawn behind them was
   a 544px rectangle with two hard vertical edges sitting over the drawing. The
   layout is restated below; the ground, the plates and the whole `.band--void`
   theme hook are inherited unchanged, because this is still a band.

   ── WORDS ABOVE, SCENE BELOW, AND THAT IS A DECISION ──────────────────────
   The words were overlaid on the drawing for one build. They landed across the
   DJ's head — and the fix for that is not a darker scrim, it is not overlapping
   in the first place: EVERY scene here is a single figure with its head near the
   top of the frame, so an overlay collides on all six or on none. Stacking also
   means the type never sits on saturated colour, which removes the site's one
   4.5:1 risk instead of managing it. The band is a poster: line, then picture. */

.band--scene {
  position: relative;
  display: block;
  /* ⛔️ NO `min-height`. It was `100svh`, on the idea that a band should fill a
     screen — and what it actually produced was DEAD BLACK under every drawing:
     37px on the hero, 121px on the listening band, the drummer and the wall,
     measured. A band is now exactly its words plus its picture, so the space
     between two sections is the overlap below and nothing else. The owner's
     note: "there is too much space between the sections."

     ⚠️ The drawings are ~823px tall at 1440 and the words another ~340, so a
     band still runs past a laptop screen on its own. The floor was buying
     nothing that the content was not already providing. */
  padding: 0;
  /* 🔴 NOTHING CLIPS. A band used to be `overflow: hidden` + `isolation:
     isolate`, which made each one a sealed box: the drawing could not reach past
     its own section, so however far the bands were pulled together the pictures
     still met at an invisible wall and the page read as a STACK OF SECTIONS.
     The owner's call — "no containment in sections". With the clip gone the
     drawings genuinely overlap: one figure's air is the next one's.

     ⚠️ `isolation` went with it. It existed to give the grain overlay a blending
     context, and the grain is gone; left in, it would keep re-creating exactly
     the containment this removes. */
}

/* ⛔️ THE BANDS DO NOT OVERLAP. They did, by up to 11rem, so that one drawing's
   air ran into the next and the page read as one surface instead of a stack.

   🔴 IT WAS CLIPPING THE FEET OFF EVERY FIGURE ON THE PAGE. A band paints its own
   black ground (`.band--void`), and a later band in the DOM paints over an
   earlier one — so pulling each band up did not blend two drawings, it covered
   the bottom of the one above. Measured before removal: 113px of every scene's
   artwork, on all six bands, hidden under the next band's background. The image
   box was the right size the whole time, which is why an overflow check never
   caught it — the artwork was occluded, not clipped, and those look identical.

   ⚠️ THE LESSON FOR THE NEXT "make it flow" IDEA: on a page where every section
   paints an opaque ground, overlap IS occlusion. If two drawings should share
   air, the air has to come from inside the drawings, not from negative margin. */

/* ── the words ───────────────────────────────────────────────────────────── */

.scene-words {
  position: relative;
  z-index: 2;
  display: grid;
  justify-items: center;
  /* The top pad clears the fixed header and gives the line the air DESIGN.md
     asks for; the bottom pad is the gap to the drawing's shoulders. */
  /* Trimmed 2026-09-28: the hero's payoff — the hands on the record — sits low
     in its drawing, and every rem spent above the statement pushes it further
     under the fold on a laptop. The top pad still clears the fixed header and
     still gives the line the air DESIGN.md asks for. */
  padding: clamp(3.25rem, 9vh, 6.5rem) var(--band-pad) clamp(0.75rem, 2.5vh, 1.75rem);
  text-align: center;
}

/* A statement over a drawing has to hold its own against a full-height figure,
   so it sets WIDER than the catalogue bands' 6.8ch. Swept the same way against
   the authored strings: "you cannot play a wrong note" breaks as "you cannot
   play / a wrong note" between 9.4ch and 12.1ch, and "send it to one person"
   breaks as "send it to / one person" between 7.9ch and 11.6ch. 10.6ch sits
   inside both. The standing rule is unchanged and still the one that matters:
   LINE TWO MUST BE NO WIDER THAN LINE ONE. */
.band--scene { --say-cap: 10.6ch; }

.band--scene .band-say { font-size: clamp(2.1rem, 6.2vw, 4.4rem); }

/* 🔴 THE CLOSE KEEPS THE PAGE DEFAULT, and this override is a measurement, not a
   preference. "hear one. make one." was swept against the catalogue cap and its
   window is [6.2, 9.2]ch — 10.6ch is outside it, and at 10.6 the line breaks as
   "hear one. make / one." with a four-character orphan. The scene cap exists for
   the longer second-person statements ("you cannot play a wrong note"); the
   close is two parallel imperatives and wants the tighter measure it was written
   for. Both numbers are swept; neither is a guess. */
.band--scene-close { --say-cap: 6.8ch; }

/* ⛔️ THE FOOTER DOES NOT CUT THE CROWD, AND NOTHING ELSE CUTS ANY DRAWING.
   The closing scene ran 88px under the footer for one build so the footer's edge
   would slice through the crowd. The owner's rule, which supersedes it and every
   other crop on this page: "never cut any artwork. The image-height has to be
   respected — never cropped."

   It is the right rule and it is worth stating why: these drawings are composed
   whole. A crop is the site deciding it knows better than the composition, and
   every one of them here was the site solving a LAYOUT problem — fold position,
   figure size on a phone, a tidier seam — at the artwork's expense. The layout
   accommodates the image now, not the other way round. */

/* THE CLOSE ENDS IN A LINE, ON PURPOSE — it butts the footer's lifted black
   (`--band-graphite`) and that edge is the page's floor. It needs no rule now
   that nothing fades, but the intent is recorded because it is the reason the
   crowd's 57%-drawn bottom edge is CORRECT rather than an oversight. The owner:
   the crowd "can be the only hard cut thing towards the footer". */

/* ── the drawing ─────────────────────────────────────────────────────────── */

/* ⛔️ NO MASK ON A SCENE, AND THE ROUND TRIP IS WORTH KEEPING WRITTEN DOWN.
   A bottom fade was added here so the drawings would "flow into one another"
   rather than ending in a straight line. It did that — and it also washed out
   the bottom 13% of every scene, which is exactly and always where a standing
   figure's SHOES are. The owner flagged it twice ("no gradation on the shoes",
   then "all shoes still have gradient overlays"), and the second time is on me:
   the first report was read as the artwork's own `<linearGradient>`s, which were
   real and were flattened, but were never what he was looking at. A CSS fade
   over a shoe and a gradient fill in a shoe are indistinguishable on screen.

   Measured before removing it, bottom row of each drawing, percentage actually
   drawn at the frame edge — i.e. how much a hard cut would sever:

       dj 0%   listen 0%   drummer 2.9%   tag 4.7%   share 2.7%
       wall 49.9%   crowd 57.2%

   Five of the seven end in black and cannot show a seam at all; the crowd is
   MEANT to cut hard into the footer (see below). So the fade was protecting one
   band, the wall, and taxing every pair of shoes on the page to do it. The flow
   now comes from the overlap alone, which costs nothing.

   ⚠️ If the wall's cut ever needs softening, soften THAT SCENE — and not with a
   full-width fade, which will find its shoes too. */
/* 🔴 THE DRAWING SITS IN THE PAGE'S COLUMN — THE BAND STILL DOES NOT (owner,
   2026-09-30). `--content-max` is the same cap `main` gives every reading page,
   so a contained scene and the prose on /features stand in one column and the
   page's centre line does not move as you navigate.

   ⚠️ THE CAP IS HERE, ON THE DRAWING, AND NEVER ON `.band--scene`. The <section>
   must keep painting its ground edge to edge: a contained ground is a centred
   card, which is the single thing DESIGN.md's "Field" section forbids, and the
   section is the more obvious element to hang a max-width on. If a scene ever
   starts reading as a card, the cap has migrated up one level.

   `margin-inline: auto` and not `justify-items`/`place-self`: `.scene` is a
   block in normal flow here, and auto margins are what centre a capped block
   without assuming anything about its parent's display type. The parent is the
   <section>, whose display the band layouts are free to change.

   The three edge-to-edge scenes release it below. */
.scene {
  position: relative;
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  /* No `aspect-ratio` and no `overflow`: the drawing inside sets the height and
     is never trimmed to fit. */
  /* No clip here either — the drawing is allowed out of its own box, which is
     what lets a figure carry past the seam into the band below. The horizontal
     overflow the wide frame produces is handled once, on `main`, with
     `overflow-x: clip` (base of this file) rather than by re-clipping here:
     `clip` stops the sideways scrollbar WITHOUT creating a scroll container, so
     the vertical bleed survives it. `hidden` would kill both. */
}

/* THE EDGE-TO-EDGE SCENES — `bleed: true` in content/artwork.json, emitted as
   this class by layouts/scene-band.yml. Share, wall and crowd: each one is
   composed so that something is MEANT to leave the frame (the ribbons sweeping
   out both sides, the crowd running the full width into the footer, the wall's
   two shelf rails, which the app itself draws past the screen edge on purpose).
   A cap does not shrink those drawings, it truncates them.

   `width: 100%` already stands from `.scene`; only the ceiling is lifted. */
.scene--bleed {
  max-width: none;
}

/* 🔴 THE FRAME IS WHAT MAKES THE MEASURED SEAT COORDINATES SURVIVE A CROP.
 *
 * content/artwork.json's `seat` is in percentages OF THE DRAWING, measured off a
 * grid rendered over it. If the drawing were cropped straight into the band, its
 * painted box would stop matching its element box the moment the band's ratio
 * differed from the art's — and every record would drift off its turntable,
 * silently, at exactly the viewport widths nobody screenshots.
 *
 * So the art and the seat live together in a box that ALWAYS keeps the drawing's
 * ratio, and it is that box the band scales and crops. Percentages inside it are
 * true at every width by construction.
 *
 * 🔴 IT ALSO CARRIES THE `perspective`, and that placement is the result of two
 * bugs rather than a preference. `perspective` belongs on the PARENT of the
 * transformed element, and the seat's parent is this frame — so it goes here.
 * Putting it on `.scene` instead meant the frame sat between the camera and the
 * seat, and because the frame is itself transformed (the centring `translateX`)
 * it flattened the projection; propping that up with `transform-style:
 * preserve-3d` fixed the tilt and then broke the OTHER seats, because a blended
 * element inside a 3-D rendering context is undefined and Chrome dropped the
 * wall capture entirely — it rendered nothing, in a band whose whole argument is
 * three people pointing at it. One property in the right place removes both.
 *
 * Anchored to the BOTTOM: these figures stand on the band's floor, so when a
 * narrow viewport has to crop it is the empty black over their heads that goes. */
.scene-frame {
  position: relative;
  width: 100%;
  /* The 3-D context a seated capture's tilt would resolve in. Nothing seats
     anything today — every band draws its interface instead — but the property
     belongs on the seat's parent, which is here. */
  perspective: 1600px;
}

/* 🔴 THE IMAGE SETS THE HEIGHT. `width: 100%` + `height: auto` and nothing else:
   the box takes the drawing's shape rather than the drawing being fitted into a
   box. This replaced a frame that was sized by HEIGHT with `min-width: 100%`,
   which cropped the sides whenever the band's ratio differed from the art's —
   silently, and worst on a phone. */
.scene-art {
  display: block;
  width: 100%;
  height: auto;
}

/* 📱 NO MOBILE CROP EITHER. The scene used to go square on phones and let the
   frame — sized by height — hang over both edges, so the figure stayed large.
   That is a crop, and it is the one the new rule most clearly forbids: it cut
   the outer air off every drawing on the surface most people will see. A phone
   now gets the whole picture, small. If a figure reads too small there, the
   answer is a drawing composed for that width, not a crop of one that was not. */

/* ── the seated capture ──────────────────────────────────────────────────── */

/* The app's own output, dropped where the drawing drew a record.
 *
 * ⚠️ THE BLACK SQUARE IS NOT A BUG TO CROP OUT. Both the generated record SVG
 * and every app capture carry a true #000000 ground, and so does this band
 * (DESIGN.md, "--band-void is TRUE #000000, and that is measured"). The square
 * dissolves into the scene and only the disc reads, which is why no cut-out
 * asset and no mask exists anywhere in this system. */
.scene-seat {
  position: absolute;
  left: var(--seat-x);
  top: var(--seat-y);
  width: var(--seat-size);
  transform: translate(-50%, -50%) rotateX(var(--seat-tilt, 0deg));
}

/* No rotation, so no 3-D — a flat seat must not pay for a compositing layer it
   does not use. */
.scene-seat--flat { transform: translate(-50%, -50%); }


.scene-seat > svg,
.scene-shot {
  display: block;
  width: 100%;
  height: auto;
}

/* ⛔️ NO `mix-blend-mode` HERE, AND THE ATTEMPT IS WORTH RECORDING because it
   looks like the obvious answer and it is not. `screen` over black is the
   identity, so blending a capture should have been free insurance: its own black
   margin would drop out and a drawn hand reaching into the seat would show
   through. It cannot be made to work in this tree. The seat sits inside
   `.scene-frame`, which carries the `perspective` the hero's tilt needs — and a
   blended element inside a perspective/3-D rendering context is undefined
   enough that Chrome simply DROPS IT: the wall capture rendered as nothing at
   all, in the one band whose entire argument is three people pointing at it.
   Moving the blend from the <img> to the seat and the perspective from `.scene`
   to the frame both changed which thing broke and neither fixed it.

   The real answer was cheaper and is upstream: every capture is already true
   #000000 (measured), and where a seat would have collided with the drawing, the
   CROP was tightened and the seat moved instead. Composition, not compositing. */


/* ── ⛔️ NO GRAIN, AND NO TEXTURE OF ANY KIND ───────────────────────────────

   A spray-grain plate was screened back over every band at 8% to put some
   aerosol grit behind the flat vector — the raster half of the "hybrid" the art
   direction started as. It is gone, on the owner's call: "Flat!"

   What it actually did was lift every flat colour by 2–6/255 and mottle it. On
   a register whose whole claim is FLAT bold vector on true black, a texture that
   makes no colour on the page the colour it is authored as is working against
   the thing it was meant to enrich — and it put a wash on exactly the shoes that
   had just been de-gradated.

   🔴 Which makes the grounds honest again: `--band-void` is TRUE #000000
   (DESIGN.md's measurement), and with nothing screened over it, the page now
   actually renders that value. That measurement is load-bearing for the whole
   composite — a capture's own black only dissolves into a band that IS black. */

/* ── the cue ─────────────────────────────────────────────────────────────── */

/* Three things arrive as the band is reached — the drawing, the seated capture,
   the words — each on its own short delay, and then they stop. Nothing is pinned
   and no timeline is driven by the wheel (STORY.md, "How it moves").
   src/html/_shell.html adds `.is-in` once and never removes it.
   ⚠️ 200ms ease-out, the house motion rule. The STAGGER, not the duration, is
   what makes the arrival read as a sequence rather than as one lump. */
.band--scene [data-cue] {
  transition: opacity 200ms ease-out, transform 200ms ease-out;
}

.has-cues .band--scene:not(.is-in) .scene-art,
.has-cues .band--scene:not(.is-in) .scene-words {
  opacity: 0;
  transform: translateY(14px);
}

/* Opacity only — the seat's transform IS its position and its tilt, and adding a
   cue offset to it would lift the record off the turntable it is seated in. */
.has-cues .band--scene:not(.is-in) .scene-seat { opacity: 0; }

.band--scene .scene-seat { transition-delay: 70ms; }
.band--scene .scene-words { transition-delay: 140ms; }

/* 🔴 THE REST STATE IS THE DEFAULT, AND `.has-cues` IS WHAT MAKES THAT TRUE.
   Every hidden rule above is scoped to that class, which a two-line script in
   <head> adds only once it has checked that IntersectionObserver exists and that
   the reader has not asked for reduced motion. With scripting off, with the
   observer missing, or under a CSP that drops inline script, the class is never
   added and the bands are simply at rest — visible, correct, unanimated.

   This was built the other way round for one build (hide in CSS, reveal in JS)
   and that version shipped six screen-height bands of nothing to anyone without
   JavaScript. A reveal must never be the reason content is missing; it can only
   take something away that it is already able to give back.

   The media query below is then belt-and-braces: the script will not add the
   class under reduced motion, and if it somehow did, these rules undo it. */
@media (prefers-reduced-motion: reduce) {
  .band--scene [data-cue] { transition: none; }
  .has-cues .band--scene:not(.is-in) .scene-art,
  .has-cues .band--scene:not(.is-in) .scene-words {
    opacity: 1;
    transform: none;
  }
  .has-cues .band--scene:not(.is-in) .scene-seat { opacity: 1; }
}
