/* ==========================================================================
   TELL — ui.css · dialogue box, choice menus, HUD, ledger strip, codex drawer
   ==========================================================================
   THE SHIPPED LOOK (design/50-ui-redress.md, election closed 2026-08-31). The
   winning skins — hud-a (the painted desk edge), menu-a (steno pad + working
   page) and db-a (the dialogue vessel detuned in place) — are folded in here as
   the DEFAULT. There is no per-candidate class prefix anywhere in this file and no
   skin needs loading: this sheet IS the game's chrome. The losing candidates
   still live in css/skins/ and are unreferenced.

   Pillar law enforced here, in the stylesheet itself:
   - Option kinds style by LABEL — the `.kind-tag` word above the written line.
     The old shape glyph is gone (the pad and the page have no room for a UI
     bullet), so the tag is not one channel of two, it is THE channel, and it is
     drawn on every row in every state. Never stoplight color.
   - `kind-insufficient` renders as a RUBBER STAMP — its own object, its own
     stamp-blue ink, a stated position — always last unless the engine hoists
     it, never dimmed, never a "coward" style.
   - Sin options have NO selector: the payload carries no sin flag and no
     class exists to hook one. They are indistinguishable at pick time.
   - No scores, no numbers-on-screen: meters render as chips/atmosphere.
   - THE ONE-RED LAW. Two saturated marks exist in the whole sheet and both are
     sanctioned by §3: the Record ribbon (a plate, on the desk edge) and the
     2px rgba(142,47,42,.9) :focus-visible keyline on the HUD's brass objects,
     which exists only under keyboard focus. NOTHING in or near the choice menu
     is red or green — the menu's focus mark is GRAPHITE for exactly that
     reason (see THE FOCUS MARK), and no colour anywhere marks an option as
     better, worse, safer or truer.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Skin tokens — declared on :root, not on a component, and deliberately.
   `--hb-h` and `--hb-frame-top` are read by the PAUSED tag, which is drawn on
   `.stage-frame` inside #panel-layer — nowhere near `.hud-bar` — so a component
   scope would put them out of reach of the one rule that needs them most. The
   --dba-* marks are read on `.db` and on `.lb-*` inside the stage frame, two
   subtrees with no common ancestor below the root. Everything else rides the
   --pp-* paper tokens from css/ui-paper.css. The one per-element token is
   `--hb-drop`, which is a per-CONTROL fact and is set on the buttons.
   -------------------------------------------------------------------------- */
:root {
  /* the desk edge */
  --hb-h: 54px;                 /* strip height at >=720px                    */
  --hb-face: 44px;              /* one sprite cell, rendered                  */
  --hb-edge-y: 6%;              /* where the plate's lit arris breaks         */
  --hb-pad-x: clamp(14px, 2.4vw, 34px);

  /* the ribbon: face width; the graphic is exactly one third of it */
  --hb-ribbon-w: 92px;

  /* brass, for the attention pool on the wood and nothing else */
  --hb-brass: rgba(198, 170, 112, 0.92);
  --hb-brass-dim: rgba(198, 170, 112, 0.55);
  /* bone — a stencil worn into dark wood; ink would be invisible on it */
  --hb-bone: rgba(232, 223, 201, 0.62);

  /* object shadows: drop-shadow, not box-shadow, so they follow the alpha */
  --hb-shadow-rest: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.62));
  --hb-shadow-lift: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.60));

  /* How far `.stage-frame`'s own top sits below the top of the stage. The
     frame is TOP-ANCHORED at the desk edge's lower lip (css/stage.css: the
     letterbox slack goes `min(--hb-h, slack)` on top, remainder to the bottom
     bar — the owner retired the dead band between the wood and the art), so
     this is that same expression, not half the slack. On a 16:9-or-wider
     window it is 0 — which is the whole reason the PAUSED tag's clearance rule
     exists; at 54px and beyond the frame's top IS the strip's lip and the tag
     needs only its base inset. Must move with stage.css's margin. Used only by
     the PAUSED tag. */
  --hb-frame-top: min(var(--hb-h), calc(100vh - min(100vh, 100vw * 0.5625)));

  /* THE PARK AND THE BUDGET — one pair, and they are only correct together.
     `--cm-park` is height the choice menu is NOT allowed to have (THE FIT
     subtracts it from the menu's max-height); `--cap-park-h` is what the
     finished line is allowed to use of it (THE PARKED LINE). The strip runs
     from the desk edge's lip to --cm-park below the top of the stage, so
     --cap-park-h plus the 0.25rem lip inset must stay inside it — at 1280x720
     the pad's highest possible top edge is y 135.6 and the parked line's
     lowest possible bottom edge is y 130. Both are plain lengths, declared
     here and NOT read by any image, so the :root hazard THE RULE LOCK
     documents for --rp-lines does not apply to either. */
  --cm-park: 7.5rem;
  --cap-park-h: 4.5rem;

  /* the dialogue vessel's two marks, both derived from the paper tokens */
  --dba-ground: rgba(11, 18, 32, 0.82);   /* --pp-ink at .82, stated literally */
  /* THE PENCIL MARK ON INK. --pp-graphite #3a3f45 mixed 55/45 toward --pp-card
     #ede6d6 = #8b8a86. Same graphite family, zero chroma, lifted just far
     enough that a pencil stroke registers on a near-black ground. */
  --dba-mark: #8b8a86;
  /* THE PRINTED RULE ON INK. --pp-rule is a ruling built for cream and measures
     1.6:1 over ink — genuinely invisible. This is its ink-side value, same hue
     family as --pp-stamp-blue, tuned to 2.5:1: a hairline you can see and would
     never call a highlight. */
  --dba-rule-lit: rgba(122, 138, 166, 0.55);

  /* ========================================================================
     THE RULE LOCK — writing sits ON the line, at every viewport
     ========================================================================
     Owner ruling 2026-08-31: on every papered text surface the text lines sit
     ON ruled lines, like real writing on ruled paper. The plates' baked ruling
     was therefore ERASED (assets/ui/steno-pad-blank.png,
     ledger-spread-blank.webp — same canvas, same pixels, so every crop written
     against the ruled plates still holds) and the ruling is redrawn at runtime
     as a background gradient the type grid is built on. It has to be a CSS fact
     because the baked pitch could not be one: both plates were photographed in
     slight perspective and their gaps DRIFT (steno 24.8 -> 26.3px, ledger 31.6
     -> 29.5px over one page), so no fixed layout could ever have sat on them.

     ------------------------------------------------------- THE GRID, EXACTLY
     One surface declares one pitch, `--rp`. Then:

       1. every line of type on that surface gets `line-height: var(--rp)`, so
          a line box is exactly one rule tall and a WRAPPED line lands on the
          next rule for free — no rule per element, no per-string tuning;
       2. every container offset between those lines (padding, margin, gap) is
          0 or a whole multiple of --rp, so a block that starts on a rule ends
          on one;
       3. each line is nudged down by THE SHIM so its baseline lands on the
          rule instead of floating above it.

     THE SHIM, derived. In a line box of height R holding type of size F with
     ascent A and descent D (per em), the half-leading is (R − F(A+D))/2 and
     the baseline sits d = R/2 + F(A−D)/2 below the line box's top. So a box
     whose top is on a rule has its baseline d ABOVE the next rule, short by

         shim = R − d = (R − F·k) / 2,      k = (A − D) per em

     which is what `--rp-k` carries. It is applied as `position: relative; top:
     <shim>` — NOT as padding — on purpose: relative offsets move the paint and
     leave the box alone, so a shimmed line still occupies exactly n × --rp and
     cannot drift the grid below it, and there is no margin-collapse hazard to
     reason about at any container edge.

     `1em` inside the shim resolves against THE ELEMENT'S OWN font-size, so one
     declaration serves a clamped, viewport-scaled size without restating the
     clamp.

     THE FACE METRICS ARE MEASURED, NOT GUESSED — read off the shipped font
     files (assets/fonts/*.woff2) with fontTools, using the metric set the
     browser actually uses for each:
       Caveat          upem 1000, typo asc 960 / desc −300, USE_TYPO_METRICS
                       set  ->  k = 0.660
       Courier Prime   upem 2048, typo asc 1600 / desc −700, USE_TYPO_METRICS
                       set  ->  k = 0.440
       Georgia         upem 2048, hhea asc 1878 / desc −449, no
                       USE_TYPO_METRICS  ->  k = 0.698
     A k that is wrong by 0.1 moves a baseline by F/20 — under a pixel at every
     size used on these surfaces — so a fallback face landing instead of the
     self-hosted one costs a hairline, never the line.

     PROOF THE MODEL IS THE SHIPPED ONE: js/feedback.js's ruled writing lines
     (the precedent this was told to reuse) draw their rule at
     `calc(var(--fb-rule) − .31rem)` for Caveat at F 1.22rem, R 1.62rem. The
     formula above gives shim = (1.62 − 1.22 × 0.66)/2 = 0.3097rem. The number
     in the shipped file is 0.31rem.

     THE INK. --pp-rule is ui-paper.css's own "printed ruling" token and is the
     colour the erased rules measured at: a soft blue-grey hairline that darkens
     cream by about a sixth. The weight is 1px — the plates' ruling measured 2
     core rows plus an antialiased skirt at source scale, which is one device
     pixel once the plate is drawn down to a menu. Never darker: a rule that
     competes with the writing is a UI line, not paper.
     ------------------------------------------------------------------------ */
  --rp-w: 1px;                  /* the printed hairline's weight               */
  --rp-drop: 0.1rem;            /* how far under the baseline it is printed    */
  --rp-ink: var(--pp-rule, rgba(43, 58, 85, 0.28));
  --rp-k-hand: 0.66;            /* Caveat        — marginalia, pencil lines    */
  --rp-k-type: 0.44;            /* Courier Prime — labels, tags, typed rows    */
  --rp-k-prose: 0.698;          /* Georgia       — quoted words, prompts       */
  /* AND `--rp-lines` — the ruling image itself — IS DELIBERATELY NOT HERE.
     A custom property's value is var()-substituted at computed-value time ON
     THE ELEMENT THAT DECLARES IT, and the ruling has to name --rp to know its
     pitch. Declared on :root, where no pitch exists, it would resolve to the
     guaranteed-invalid value ONCE and every descendant would inherit that
     corpse — a surface setting --rp later could not revive it, and the
     `background-image` naming it would be dropped whole, taking the PLATE on
     the same declaration down with it. So it is declared beside the pitch it
     reads, on the surfaces themselves: see THE RULED SURFACES below, and
     `--cxb-rules` in css/codex.css, which sidesteps the same trap by carrying
     its pitch in `background-size` instead of in the gradient. */
}

/* --------------------------------------------------------------------------
   Dialogue box — DETUNED IN PLACE. Not dark-slate software: the quietest
   possible ink object. A near-black ground, no radius and no box, one printed
   hairline where the border used to be, the speaker restated as a typed slug
   sitting ON that hairline, and the advance triangle replaced by a drawn
   pencil tick. Geometry (width, padding, margins, min-height) is untouched;
   only the paint changed.

   NOTE, so nobody reviews this as a blank change: js/scene-player.js routes
   every line to one of THREE vessels — an anchored speaker gets `.bubble`, a
   framed shot gets `.lb-caption`, and `.db` is the headless degrade that never
   fires with a stage on screen. The same grammar is carried onto `.lb-caption`
   in the section below, which is the vessel actually seen.
   -------------------------------------------------------------------------- */
#dialogue-box { pointer-events: none; }

.db {
  position: relative;
  margin: 0 auto;
  width: min(920px, 94%);
  padding: 1.05rem 1.5rem 1.25rem;
  margin-bottom: max(2.2vh, 14px);
  background: var(--dba-ground);
  /* ONE hairline, not two. A double rule reads as a 2px edge, which is the box
     coming back. Every other side stays borderless. */
  border-top: 1px solid var(--dba-rule-lit);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 0.46);   /* unsaturated seat, not a glow */
  transition: opacity 200ms ease;
  pointer-events: auto;
  min-height: 4.6rem;
  /* Own stacking context, so nothing drawn inside the vessel can fall behind
     the stage art. */
  isolation: isolate;
}
.db[hidden] { display: none; }
.db.dimmed { opacity: 0.4; }          /* cue pre-beat: the world holds its breath */

/* The speaker plate is deleted as an OBJECT — no ground, no border, no shadow,
   no radius — and restated as a typed slug, nudged just clear of the box's top
   edge so the hairline above runs directly beneath it. One line doing two jobs:
   the box's top rule IS the slug's underline. --pp-manila rather than the
   typewriter ink, because near-black type on a near-black ground is not a
   detune, it is a deletion (9.9:1 on the vessel's ground). */
.speaker-plate {
  position: absolute;
  top: -1.28rem;
  left: 1.2rem;
  padding: 0 0 0.18rem;
  font-family: var(--font-type);
  font-size: 0.68rem;
  letter-spacing: 0.26em;
  text-indent: 0.06em;
  text-transform: uppercase;
  color: var(--pp-manila);
}
.speaker-plate[hidden] { display: none; }
.speaker-plate .speaker-stance {
  margin-left: 0.7em;
  color: rgba(201, 180, 137, 0.58);   /* --pp-manila, stepped back */
  letter-spacing: 0.14em;
  font-style: italic;
  text-transform: lowercase;
}

/* The reading face is untouched (§3: body/reading text stays the current
   face); only the ink tone is re-anchored to the paper palette. */
.line-text {
  margin: 0.35rem 0 0;
  font-size: var(--fs-line);
  line-height: 1.65;
  color: var(--pp-card);
  min-height: 2.6em;
}
/* Narration keeps the italic — the mark that says "no one said this" — and
   loses the rain-blue for a paper-neutral step-back. */
.db.narration .line-text {
  font-style: italic;
  color: rgba(237, 230, 214, 0.72);   /* --pp-card at .72 */
}

/* THE ADVANCE CARET — a DRAWN pencil tick, not a typed glyph and not the old
   CSS triangle. The triangle was a border trick; it is unmade by giving the
   element a real box, and the tick is two tapered strokes in ::before/::after.
   Tapered ends (transparent -> mark) read as pressure fading at lift-off,
   which is what separates a pencil mark from a vector. Position stays. */
.advance-caret {
  position: absolute;
  right: 1.1rem;
  bottom: 0.65rem;
  width: 15px;
  height: 11px;
  opacity: 0;
}
.advance-caret::before,
.advance-caret::after {
  content: "";
  position: absolute;
  display: block;
  height: 1.6px;
  border-radius: 0.8px;
  transform-origin: 0 50%;
}
/* the short down-stroke into the joint */
.advance-caret::before {
  left: 0;
  bottom: 4px;
  width: 6.4px;
  transform: rotate(38deg);
  background: linear-gradient(90deg,
    rgba(139, 138, 134, 0) 0%, var(--dba-mark) 38%, var(--dba-mark) 100%);
}
/* the long up-stroke away from it */
.advance-caret::after {
  left: 4.8px;
  bottom: 7.4px;
  width: 11px;
  transform: rotate(-33deg);
  background: linear-gradient(90deg,
    var(--dba-mark) 0%, var(--dba-mark) 58%, rgba(139, 138, 134, 0) 100%);
}
.advance-caret.show { animation: caret-breathe 1.6s ease-in-out infinite; }

/* The old animation bobbed the triangle. A pencil mark does not bob: it only
   comes up out of the paper. Opacity only. */
@keyframes caret-breathe {
  0%, 100% { opacity: 0.30; }
  50%      { opacity: 0.92; }
}

/* --------------------------------------------------------------------------
   THE LETTERBOX CAPTION — the same grammar, on the vessel actually on screen.

   `.lb-*` geometry, timing and the bands belong to css/stage.css and are NOT
   touched here; this is the ink treatment only — the printed hairline, the
   typed slug, the paper ink, and the pencil tick — carried across so the
   dialogue register is one thing whichever vessel the router picks.
   -------------------------------------------------------------------------- */

/* The same printed hairline the box gets above, laid along the top of the
   caption and INSET to the caption's own text padding (stage.css's clamp), so
   it reads as a rule on a page rather than the edge of a film bar. Absolutely
   positioned: .lb-caption is a flex column and an in-flow pseudo would become a
   flex ITEM and push the text down. */
.stage-frame.letterboxed .lb-caption::before {
  content: "";
  position: absolute;
  top: 0;
  left: clamp(14px, 4.5cqw, 54px);
  right: clamp(14px, 4.5cqw, 54px);
  height: 1px;
  background: var(--dba-rule-lit);
  pointer-events: none;
}

/* The speaker slug: the same typed restatement the `.db` plate gets. This
   replaces the one green in the vessel grammar (stage.css set it in a mono
   stack at #93ad76) with the §3 paper tone — warmer, quieter, 9.9:1 on the
   band black. */
.lb-name {
  font-family: var(--font-type);
  color: var(--pp-manila);
  letter-spacing: 0.22em;
}

/* Reading face untouched; only the ink is re-anchored to --pp-card, within a
   hair of the base cream — the detune here is deliberately almost nothing,
   because the letterbox type already fits.
   SCOPED TO `.lb-caption` FOR CASCADE, not for meaning: stage.css's
   `.stage-frame.spread .lb-text` sets its own colour at two classes, so a bare
   `.lb-text` here would have been silently dropped in comic-spread mode and the
   column would have kept the old cool grey. Two classes, later sheet, wins. */
.lb-caption .lb-text {
  color: var(--pp-card);
}

/* The letterbox caret becomes the same pencil tick. THE METRIC CONTRACT: the
   element keeps stage.css's inline box (.44em x .06em, baseline-aligned) to the
   pixel — the strokes are absolutely positioned children, so they contribute
   nothing to the line box. This matters because `.spread`'s caption column is
   `max-height:74%; overflow-y:auto`, and a taller caret could have put a
   scrollbar on a narration column. `background:none` retires the old dash;
   `opacity` (stage.css's `.show`) still governs both strokes. */
.lb-caret {
  position: relative;
  background: none;
  overflow: visible;
}
.lb-caret::before,
.lb-caret::after {
  content: "";
  position: absolute;
  display: block;
  height: 1.4px;
  border-radius: 0.7px;
  transform-origin: 0 50%;
}
.lb-caret::before {
  left: 0.1em;
  bottom: 0.02em;
  width: 0.34em;
  transform: rotate(38deg);
  background: linear-gradient(90deg,
    rgba(139, 138, 134, 0) 0%, var(--dba-mark) 38%, var(--dba-mark) 100%);
}
.lb-caret::after {
  left: 0.36em;
  bottom: 0.2em;
  width: 0.58em;
  transform: rotate(-33deg);
  background: linear-gradient(90deg,
    var(--dba-mark) 0%, var(--dba-mark) 58%, rgba(139, 138, 134, 0) 100%);
}

/* THE SPREAD EXCEPTION — stated LAST, on purpose.
   `.spread` is the comic-mode board column: no bars, narration set as type on
   the board beside the panel. A rule drawn along its top would be a line across
   the middle of the picture, so it is switched off.
   ORDERING CONTRACT (the stage.css letterbox-dim lesson, which is exactly this
   bug class): `.stage-frame.spread .lb-caption` and
   `.stage-frame.letterboxed .lb-caption` are the SAME specificity, so whichever
   is stated later wins — and a frame can legitimately carry both classes. The
   doubled `.spread.spread` buys a full class of specificity so this cannot be
   lost by anyone reordering the file. */
.stage-frame.spread.spread .lb-caption::before { content: none; }

/* ==========================================================================
   Choice menu — THE STENO PAD AND THE WORKING PAGE
   ==========================================================================
   Two objects, one register each (js/scene-player.js stamps the class):

   · `.cm.menu-say`  — a steno pad dropped onto the lower stage. Spiral at the
     top, the page running off the bottom of the frame (#app clips it). Options
     are pencil-written lines: Caveat, graphite, no boxes, no rules, no glyphs.
     Hover is a graphite underline under the row's own words.
   · `.cm.menu-read` — the commonplace book's working page. The ground is a crop
     of the ledger spread's LEFT page (its red margin rule lands at ~7% of the
     menu's width, so the ink lines are written to the right of it, as they
     would be in the book). Options are ink-written; INSUFFICIENT EVIDENCE is a
     rubber stamp; Attention prices are ink tally strokes.

   A menu of any other kind carries neither class and keeps the plain width
   below with no plate — the written line is still the row.

   NO REORDERING ANYWHERE IN THIS SECTION. There is no `order` and no
   `*-reverse`: the engine's payload order is the DOM order is the keyboard
   order is the screen order.
   -------------------------------------------------------------------------- */
#choice-menu { pointer-events: none; }

/* THE BOX MODEL, stated because getting it wrong is what broke the first cut.
   There is NO global `box-sizing` reset in this project (every use in css/ and
   index.html is per-element), so `.cm` was content-box: its width was the
   CONTENT width and any padding was added OUTSIDE it. Worse, PERCENTAGE PADDING
   RESOLVES AGAINST THE CONTAINING BLOCK — `#choice-menu`, the full stage — not
   against the menu. Padding in percentages rendered a nominally 880px page at
   1053px with a 115px left inset, which left the Attention tallies hanging off
   the paper. Every measurement below is therefore in rem, and border-box makes
   `width` mean the width.
   `isolation` is load-bearing too: the stamp multiplies onto the page, and a
   blend needs an isolated group to land in. */
.cm {
  box-sizing: border-box;
  position: relative;
  isolation: isolate;
  margin: 0 auto;
  width: min(880px, 94%);
  margin-bottom: max(2.2vh, 14px);
  pointer-events: auto;
}
.cm::before,
.cm::after,
.cm *,
.cm *::before,
.cm *::after { box-sizing: border-box; }
.cm[hidden] { display: none; }

/* The paper stack — the ground is a pseudo at z-index 0 and the three content
   blocks ride above it. #choice-menu already isolates the lot from the panel
   layer, so no z-index here escapes the menu. */
.cm-head,
.cm-focus,
.cm-options {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   THE RULED SURFACES. The pad and the page are the two menus made of paper, so
   they are the two that carry a pitch and a ruling; a menu of any other kind
   declares neither and every rule in THE RULED GRID (further down) passes it
   by, leaving today's metrics untouched.

   BOTH DECLARATIONS LIVE HERE, TOGETHER, AND THAT IS LOAD-BEARING: `--rp-lines`
   names `--rp`, and a custom property is substituted on the element that
   DECLARES it — so the image and the pitch it reads have to be declared on the
   same element or the image resolves to nothing. See THE RULE LOCK on :root.

   `--rp-k` is the surface's default face metric: both menus are written in the
   hand, and the typed and prose lines override it where they are stated.
   -------------------------------------------------------------------------- */
.cm.menu-say,
.cm.menu-read {
  --rp: 1.6rem;
  --rp-k: var(--rp-k-hand);
  --rp-lines: repeating-linear-gradient(to bottom,
    var(--rp-ink) 0, var(--rp-ink) var(--rp-w),
    rgba(0, 0, 0, 0) var(--rp-w), rgba(0, 0, 0, 0) var(--rp));
}

/* --------------------------------------------------------------------------
   THE FIT — the pad is an object ON the desk, not a page sliced by the screen
   ==========================================================================
   Three defects, one geometry, and they are the same geometry:

     1  the pad had NO height of its own. `.cm` grew with its rows and the
        plate was drawn at `128%` of the menu's WIDTH with the pad's own
        aspect ratio, so the plate box was 756.6px tall whatever the menu held.
        Bottom-anchored, that put the plate's lower edge 438px BELOW a 720px
        viewport on a two-option menu — the paper was cut by the screen edge
        instead of ending, and the last written row sat 35px off it with no
        margin of paper under it, which is what reads as a cut row.
     2  the ch02 interlude's nine rows made the menu 605px tall, so the pad
        climbed to y 99 and covered the panel it had just changed.
     3  the finished line — the paragraph the choice ANSWERS — was under all
        of it, at opacity .28 and behind an opaque plate. See THE PARKED LINE.

   THE THREE NUMBERS, and why they are these:

     --cm-foot   TWO RULES of paper below the writing (the grid's law: an
                 offset is 0 or a whole multiple of --rp, and this one is
                 spent, not tidied). It is the menu's own `margin-bottom` AND
                 the plate's overhang below it, which is what makes the plate's
                 lower edge land at the same place for every option count:
                 plate bottom = cm.bottom + foot = (100vh − foot) + foot.
                 Minus 2px, so "inside the viewport" is a strict inequality.
     --cm-park   the strip under the desk edge the finished line parks on,
                 declared on :root beside --cap-park-h. It is RESERVED here —
                 subtracting it from the menu's height is what guarantees the
                 pad's top can never reach the parked line, with no runtime
                 measurement and nothing for JS to keep in sync.
     max-height  what is left: 100vh − the desk edge − the park − the foot.
                 At 1280x720 that is 494.8px and the nine-row menu scrolls; at
                 1600x900 it is 674.8px and the same menu fits whole.

   THE SCROLL KEEPS THE LOCK. `.cm-options` is the only flex item that may
   shrink (the head, the strip and the pool are `flex: none`), so the rows —
   and only the rows — scroll. The ruling is printed on the plate BEHIND them
   and does not scroll, so a scroll that stopped anywhere would put every row
   in the gap; `scroll-snap-type: y mandatory` with every `li` a snap START
   fixes that for free, because every row is a whole number of rules tall and
   the options box's own top is on a rule — so every rest position is a whole
   number of rules and the writing is back on the printing. The scrollbar is
   not drawn: paper has no gutter. js/scene-player.js's arrow traversal keeps
   `preventScroll: true` (it is there to stop the PAGE moving) and asks the
   options box for the row instead — order, wrap and focus are untouched.
   -------------------------------------------------------------------------- */
.cm:is(.menu-say, .menu-read) {
  --cm-foot: calc(var(--rp) * 2);
  display: flex;
  flex-direction: column;
  margin-bottom: var(--cm-foot);
  max-height: calc(100vh - var(--hb-h) - var(--cm-park) - var(--cm-foot) - 2px);
}
/* `.cm` has exactly three children (js/scene-player.js: `cm.append(cmHead,
   cmFocus, cmOptions)`), and only the third may give ground. */
.cm:is(.menu-say, .menu-read) .cm-head,
.cm:is(.menu-say, .menu-read) .cm-focus { flex: none; }
/* THE KEYLINE HAS TO FIT IN THE SCROLLER. A scroll container clips at its
   PADDING box on both axes — `overflow-y: auto` alone computes overflow-x to
   auto as well — and THE FOCUS MARK is a 2px graphite outline at
   `outline-offset: 4px`, so the first cut of this ate the whole ring off every
   focused row and left only the graphite underline. Six pixels of room are
   given back on all four sides and taken away again as negative margin, so the
   ROW's box has not moved a pixel and the grid above and below it is exactly
   where it was; `scroll-padding-block` restates the same six so a snapped rest
   position is still a whole number of rules (li offsets are 6px + n rules, and
   the snapport starts 6px in, so the snapped scrollTop is n rules).

   AND ONE RULE OF CLEARANCE AT THE FOOT, which is two things at once. The
   measured fault it fixes: `scrollIntoView({block:'nearest'})` asks for the
   SMALLEST scroll that shows the row, and mandatory snapping then rounds that
   ask to the NEAREST snap position — which, for a row hanging 3.5px below the
   fold, is the one you came from. ArrowDown then "reached" rows 6 and 7 of the
   nine-row menu with 3.5px of the row still cut off (measured: clipBot +3.5,
   +3.7 at 1280x720). Asking for one rule MORE than the row needs moves the
   nearest snap position to the next one down, so the row lands whole with a
   rule of paper under it. That same rule of paper is what keeps a focused row
   out of THE FOOT FADE below. */
.cm:is(.menu-say, .menu-read) .cm-options {
  flex: 0 1 auto;
  min-height: 0;
  margin: -6px -8px;
  padding: 6px 8px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-snap-type: y mandatory;
  scroll-padding-block: 6px calc(6px + var(--rp));
  scrollbar-width: none;
}
.cm:is(.menu-say, .menu-read) .cm-options::-webkit-scrollbar { width: 0; height: 0; }
.cm:is(.menu-say, .menu-read) .cm-options > li { scroll-snap-align: start; }

/* THE FOOT FADE — writing runs off the visible page, it is not guillotined.
   The scrollbar is not drawn (paper has no gutter), so a nine-row menu at
   1280x720 has to say "there is more here" in the paper's own idiom. Left to
   itself the scrollport's lower edge fell 3.5px above the last visible row's
   baseline and cut the glyphs of "What are you?" through their middles on a
   dead-straight invisible line, with a clear rule of blank paper under it: that
   reads as an erasure, not as a page that continues. Fading the last rule of
   the scrollport turns the same cut into ink going under the fold, which is
   what a page of writing looks like when you cannot see all of it.

   IT IS ONLY EVER ON WHEN IT IS TRUE. js/scene-player.js marks the box
   `cm-scrolls` when its content is actually taller than its window, and
   `at-top`/`at-end` when there is nothing above or below — so a menu that fits
   whole (every 2-, 3- and 4-row menu, and all nine rows at 1600x900) is
   untouched, and the LAST row of a scrolled menu is crisp because the foot
   fade is switched off at the end of the run. Both edges fade in the middle of
   the run, which is also how the eye is told the list has a top it can go back
   to. `--cm-fade-t/b` carry the two edges so the four states are three short
   overrides instead of four whole gradients. */
.cm:is(.menu-say, .menu-read) .cm-options.cm-scrolls {
  --cm-fade-t: var(--rp);
  --cm-fade-b: var(--rp);
  -webkit-mask-image: linear-gradient(to bottom,
    rgba(0, 0, 0, 0) 0, #000 var(--cm-fade-t),
    #000 calc(100% - var(--cm-fade-b)), rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(to bottom,
    rgba(0, 0, 0, 0) 0, #000 var(--cm-fade-t),
    #000 calc(100% - var(--cm-fade-b)), rgba(0, 0, 0, 0) 100%);
}
.cm:is(.menu-say, .menu-read) .cm-options.cm-scrolls.at-top { --cm-fade-t: 0px; }
.cm:is(.menu-say, .menu-read) .cm-options.cm-scrolls.at-end { --cm-fade-b: 0px; }

/* --- .menu-say: THE STENO PAD --------------------------------------------
   Plate geometry, measured off assets/ui/steno-pad.png (900x700) and still
   exact for the blank twin now drawn in its place — the erasure was done in
   place on the same canvas, so every number below survived it:
     page interior  x 14.8% -> 87.8%   y 3.6% -> 94.3%
     spiral binding y  3.6% -> 10.7%   (the rings, across the full page width)
   The pad is drawn at 128% of the menu's width and centred, which puts the
   page's own interior at 4.9% -> 98.4% of the menu box — so the content inset
   sits comfortably inside the ruled area at every viewport width (the whole
   construction is proportional to the menu width, so it does not drift).
   At 1280 the pad is 760 wide and centred (260..1020); the ::before draws it at
   972.8 centred, so the pad spans 153.6..1126.4 and its PAGE interior lands at
   297.6..1007.7. The content box (317.6..972) sits inside that with ~20px of
   ruled paper to the left and ~36px to the right. Padding-top clears the
   spiral: the pad is 756.7 tall, its rings occupy 27..81 from the pad's top,
   the pad is lifted 2.4rem above the menu, so the first line must start below
   42.6px — 3rem does.

   The plate is OPAQUE and carries its own painted dark desk around the pad.
   assets/ui/MANIFEST.json's own warning applies — an opaque plate laid over the
   stage shows the seam where its ground meets the art — so the ground is masked
   to an ellipse that is solid across the whole page and fades out through the
   painted surround. No hard rectangle meets the panel. The pad then runs off
   the bottom of the frame (#app is overflow:hidden) the way a pad on a desk
   continues past the edge of what you are looking at. */
/* THE PAD IS NOW BLANK AND THE RULING IS DRAWN HERE (see THE RULE LOCK on
   :root). steno-pad-blank.png is the same 900x700 canvas with the printed rules
   and the page divider erased and nothing else touched — the spiral, the corner
   curl and the worn edges all survive — so every crop number above is still
   true and the swap is one url.

   THE RULING RIDES THE PLATE'S OWN PSEUDO, AND IT HAS TO. `.cm` is an isolated
   stacking context and `.cm::before` is a positioned child at z-index 0, so the
   element's OWN background paints first and the opaque plate paints over it —
   a ruling on `.cm` would be drawn and then buried. Stated as a second
   background layer on the pseudo it is simply printed on the paper: a first
   layer is the top one, so the rules sit ON the pad and under the writing,
   which is where ink on paper goes. It also means the ruling inherits the same
   radial mask as the plate and fades out with it at the pad's edges — one
   object, one silhouette, no rectangle of rules escaping the paper.

   THE CROP OF THE RULING, in the pseudo's own frame (the plate is drawn at
   `100% auto` over an `aspect-ratio: 900/700` box, so plate % IS pseudo %):
     background-size-x  73%  ->  the page interior is x 14.8% -> 87.8%.
     background-position-x  54.81%  ->  a tile narrower than its area is placed
       by (area − tile) × p, so p = 14.8 / (100 − 73) = 54.81% puts the ruling's
       left edge on the page's left edge. A percentage, so it holds at every
       viewport exactly as the plate's own crop does.
     background-position-y  --rp-lift + --rp-pad-t + --rp-drop  ->  the pseudo's
       top is --rp-lift ABOVE the menu box and the menu's first line is
       --rp-pad-t below that, so this is the menu's own CONTENT top: the band's
       first rule lands one --rp-drop under it and every rule after it is a
       whole --rp below, which is where the type grid puts its baselines.
     background-size-y  94.3% − the same two lengths  ->  the band stops at the
       page interior's bottom edge and never reaches the pad's lower margin.
   Nothing is ruled above the first line: --rp-pad-t is the padding that clears
   the spiral (42.6px of rings against 48px of padding at the widest the pad is
   ever drawn), so the ruling starts below the binding by construction. */
.cm.menu-say {
  /* --rp and --rp-lines are declared on THE RULED SURFACES above, together */
  --rp-lift: 2.4rem;                /* how far the pad rises above the menu    */
  --rp-pad-t: 3rem;                 /* clears the spiral; also the grid's top  */
  width: min(760px, 92vw);
  padding: var(--rp-pad-t) 3rem 1.2rem 3.6rem;
}
.cm.menu-say::before {
  content: '';
  position: absolute;
  z-index: 0;
  pointer-events: none;
  left: 50%;
  top: calc(var(--rp-lift) * -1);
  width: 128%;
  transform: translateX(-50%);
  /* THE BOX IS A WINDOW ON THE PAD, NOT THE PAD. `aspect-ratio: 900/700` used
     to be here and it is what put the plate's lower edge 438px below a 720px
     viewport: it sized the box off the menu's WIDTH, so the plate was 756.6px
     tall whether the menu held two rows or nine. The box now ends one --cm-foot
     below the menu — which is where the menu's own margin ends, so the plate's
     lower edge lands at 100vh − 2px for EVERY option count — and the image
     keeps its own proportion the only way that matters, in `background-size:
     100% auto`. The pad is never squashed; the window is simply shorter than
     the paper, exactly as a desk is shorter than a pad you are writing on. */
  height: calc(100% + var(--rp-lift) + var(--cm-foot) - 2px);
  background-image: var(--rp-lines),
                    var(--plate-steno-blank, url('../assets/ui/steno-pad-blank.png'));
  background-repeat: no-repeat, no-repeat;
  background-position:
    54.81% calc(var(--rp-lift) + var(--rp-pad-t) + var(--rp-drop)),
    top center;
  /* The band's stop is now measured from the BOX, not from the plate's 94.3%:
     the box no longer is the plate. `100% − lift − pad-t − foot` ends the
     ruling 2px above the menu's own bottom edge — under every written row, and
     clear of the foot the paper fades out in. */
  background-size:
    73% calc(100% - var(--rp-lift) - var(--rp-pad-t) - var(--cm-foot)),
    100% auto;
  /* TWO MASKS, INTERSECTED. The radial is the shipped one, unchanged — it is
     what softens the plate's painted desk into the art at the lower corners.
     The linear is new and is the whole of the bottom fix: the paper FADES OUT
     over the last rule of the box instead of being sliced by the screen edge.
     `mask-composite: intersect` (and the -webkit- spelling before it, for the
     older engine) is load-bearing: mask layers composite with `add` by default,
     and a union of these two is solid everywhere, which is the hard rectangle
     both of them exist to prevent. */
  -webkit-mask-image: radial-gradient(86% 84% at 50% 46%,
                        #000 0 84%, rgba(0, 0, 0, 0) 100%),
                      linear-gradient(to bottom,
                        #000 0 calc(100% - var(--rp)), rgba(0, 0, 0, 0) 100%);
  mask-image: radial-gradient(86% 84% at 50% 46%,
                #000 0 84%, rgba(0, 0, 0, 0) 100%),
              linear-gradient(to bottom,
                #000 0 calc(100% - var(--rp)), rgba(0, 0, 0, 0) 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* --- .menu-read: THE WORKING PAGE ----------------------------------------
   A crop of the ledger spread (1600x1000; the blank twin now, same canvas and
   same pixels, so the crop below is unchanged). The LEFT page is used,
   because its red margin rule is a LEFT margin (image x = 10.2%) and the ink
   lines are written to the right of it. The crop is expressed as
   background-size/position percentages so it holds at every menu width:
     background-size: 255% auto   ->  image is 2.55x the menu width
     background-position-x: 12.3% ->  puts image x=7.5% at the menu's left edge
   which lands the red margin rule at ~6.9% of the menu's width and keeps the
   page's gutter safely off the right-hand edge. The vertical position rides in
   the page's upper-middle for any menu height (the paper is homogeneous there).
   At 1280 the page is 880 wide and centred: 200..1080, whole thing on screen.
   The content box is 270.4..1025.6, which leaves ~55px of ruled paper to the
   right of the widest thing in the menu — the Attention tallies — so they read
   as written IN the page's margin instead of hanging off its edge. */

/* THE PAGE IS NOW BLANK AND THE RULING IS DRAWN HERE (see THE RULE LOCK on
   :root). ledger-spread-blank.webp is the same 1600x1000 canvas with the
   printed rules erased and BOTH dried-red margin rules kept — they are page
   furniture, not ruling — so the crop above still lands the red rule at ~6.9%
   of the menu's width and the swap is one url.

   Two layers now, the ruling FIRST because a first layer is the top one and
   rules are printed ON the paper. The whole menu box is page here (unlike the
   pad, where the plate carries its own painted desk), so the ruling spans the
   full width and only its PHASE has to be stated: it starts one --rp-drop below
   the element's own content top, which is where the type grid starts. Both
   layers keep their own positioning area — the plate's crop is written against
   the padding box and is not disturbed. */
.cm.menu-read {
  /* --rp and --rp-lines are declared on THE RULED SURFACES above, together */
  --rp-pad-t: 1.1rem;
  width: min(880px, calc(100vw - 3rem));
  padding: var(--rp-pad-t) 3.4rem 1.25rem 4.4rem;
  background-image: var(--rp-lines),
                    var(--plate-ledger-blank, url('../assets/ui/ledger-spread-blank.webp'));
  background-repeat: no-repeat, no-repeat;
  background-size: 100% calc(100% - var(--rp-pad-t)), 255% auto;
  background-position: 0 calc(var(--rp-pad-t) + var(--rp-drop)), 12.3% 20%;
  background-color: var(--pp-ledger);   /* the floor under a slow-loading plate */
  box-shadow: var(--pp-shadow-pinned);
}

.cm-head {
  display: flex;
  align-items: flex-end;
  gap: 1.2rem;
  margin-bottom: 0.7rem;
  padding: 0 0.4rem;
}

/* A sentence written on the page, not dark-UI secondary text: no text-shadow (a
   drop shadow under ink is a double exposure), prose italic, ink at the dim
   level so the options are the darker thing on the sheet. */
.cm-prompt {
  margin: 0;
  flex: 1;
  font-family: var(--font-prose);
  font-style: italic;
  color: var(--pp-type-dim);
  font-size: 1rem;
  line-height: 1.55;
}
.cm-prompt[hidden] { display: none; }

/* Beat-counted window ring — never wall-clock, no timer pressure */
.cm-window {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
}
.cm-window[hidden] { display: none; }
.cm-window svg { width: 44px; height: 44px; }
/* DORMANT in the shipped engine (js/scene-player.js — "called by nothing"; no
   scene authors `window=`), restated in the paper register anyway so the sheet
   does not have a hole in it if the renderer is ever re-armed. */
.cm-window .win-seg {
  stroke: var(--pp-type-dim);
  stroke-width: 3;
  stroke-linecap: round;
  opacity: 0.85;
}
.cm-window .win-count {
  fill: var(--pp-type);
  font-family: var(--font-type);
  font-size: 15px;
}
.cm-window .win-label {
  font-family: var(--font-type);
  font-size: 0.58rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--pp-type-faint);
}

/* CONSULT THE FILE — the shortcut to the case file on the body being read, in
   the paper-underline register: no box, no fill, one hairline that only appears
   under the pointer. It is META, so it carries no invitation of any kind — no
   pulse, no tooltip, no warm or cool valence, and nothing red or green near it.
   50-ui-redress W0: transcribed off the inline block js/scene-player.js used to
   stamp on this button; `:focus` (not `:focus-visible`) because the old handler
   lit on ANY focus, and that is kept. Its `display` is still written from JS —
   that is state, not style. On paper it is the quietest thing in the head:
   typed, tracked, faint, one graphite hairline under the pointer. */
.cm-file {
  font: inherit;
  font-family: var(--font-type);
  font-size: 0.58rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  background: none;
  border: none;
  border-bottom: 1px solid transparent;
  padding: 0 0 2px;
  color: var(--pp-type-faint);
  cursor: pointer;
  align-self: flex-end;
  transition: color var(--pp-t-lift) var(--pp-ease-paper),
              border-bottom-color var(--pp-t-lift) var(--pp-ease-paper);
}
.cm-file:hover,
.cm-file:focus {
  color: var(--pp-type-dim);
  border-bottom-color: var(--pp-graphite);
}

.cm-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* THE WRITTEN LINE. Everything the dark-UI card was made of has come off: the
   surface fill, the 1px box, the 3px left bar, the radius, and the glyph gutter
   with its shape bullet. What is left is a line of writing with room to
   breathe. `border: 0` is not a neutraliser of anything in this sheet — it is
   there because a <button> carries a UA border of its own. */
.cm-options button {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--font-hand);
  font-size: clamp(1.12rem, 1.7vw, 1.3rem);   /* Caveat runs small for its size */
  line-height: 1.28;
  color: var(--pp-graphite);
  background: none;
  border: 0;
  padding: 0.34rem 0.4rem 0.34rem 0;
  cursor: pointer;
  transition: color var(--pp-t-lift) var(--pp-ease-paper);
}
/* Only the SAY menu ever carries a stated price on a row (`opt.hold`'s +1 and
   `opt.lead`'s tally are dialogue-menu modifiers), and both are absolutely
   positioned at right: .9rem — so the right-hand gutter that keeps the writing
   out from under them is opened THERE and nowhere else. */
.cm.menu-say .cm-options button { padding-right: 3.6rem; }

/* THE GRAPHITE UNDERLINE — .paper-underline's technique (ui-paper.css), moved
   onto the option's OWN WORDS so a row carrying a report preview is underlined
   at its sentence and not at the bottom of the whole block. */
.cm-options .opt-text {
  display: block;
  background-image: linear-gradient(var(--pp-graphite), var(--pp-graphite));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--pp-t-lift) var(--pp-ease-paper);
}
.cm-options button:hover:not(:disabled) .opt-text,
.cm-options button:focus-visible .opt-text {
  background-size: 100% 1.5px;
}
/* …and the ink darkens a shade. That is the whole of the hover: no lift on an
   ordinary row (a written line does not rise off the page), no translate, no
   fill, no colour. */
.cm-options button:hover:not(:disabled),
.cm-options button:focus-visible {
  background: none;
  color: var(--pp-type);
  transform: none;
}

/* On the working page the hand is INK, not pencil — the READ menu is Sam
   writing a conclusion into the book, the SAY menu is Sam's notes to himself. */
.cm.menu-read .cm-options button { color: var(--pp-type-dim); }
.cm.menu-read .cm-options button:hover:not(:disabled),
.cm.menu-read .cm-options button:focus-visible { color: var(--pp-type); }
.cm.menu-read .cm-options .opt-text {
  background-image: linear-gradient(var(--pp-type), var(--pp-type));
}

/* Disabled is said in ink, never in alpha — the row stays legible.
   KNOWN CASCADE FACT, recorded not fixed (it is the elected skin's own, folded
   unchanged): the READ page's resting ink two lines above is THREE classes and
   this is two, so on `.menu-read` a disabled row — a whisper, an overdrawn
   question — renders at --pp-type-dim like a live one, and the pale-ink channel
   does not fire there. Whisper still carries its dotted underline and its
   italic, which is the structural channel A06 actually requires; overdrawn on a
   READ menu would carry none. The one-line repair, if it is ever wanted, is to
   scope the state rules `.cm .cm-options button:disabled` — do not "fix" it by
   dimming, which is the thing all three of these rules exist to avoid. */
.cm-options button:disabled {
  cursor: default;
  color: var(--pp-type-faint);
}
.cm-options button:disabled .opt-text { background-size: 0% 1.5px; }

/* Kind tag — with the shape bullet gone this is not the label HALF of
   shape+label, it is the whole channel, so it is drawn on every row in every
   state. Typed small, tracked, faint: a clerk's annotation above a written
   line. */
.cm-options .kind-tag {
  display: block;
  font-family: var(--font-type);
  font-size: 0.56rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--pp-type-faint);
  margin-bottom: 0.1rem;
}

/* --------------------------------------------------------------------------
   THE FOCUS MARK.
   The paper idiom's precedent is the 2px rgba(142,47,42,.9) keyline the pinned
   index card uses, and it is NOT used on anything inside the choice menu,
   deliberately: the one-red law forbids red anywhere near a choice, and
   js/scene-player.js says the same thing again about `.cm-file` specifically
   ("no red and no green anywhere near it"). Every interactive thing on this
   surface is either a choice or sits inside the choice menu, so the keyline is
   drawn in GRAPHITE — same 2px, same offset, same visibility, no valence.
   -------------------------------------------------------------------------- */
.cm-options button:focus-visible,
.cm-focus .cf-row:focus-visible,
.cm-file:focus-visible {
  outline: 2px solid var(--pp-graphite);
  outline-offset: 4px;
  border-radius: 1px;
}

/* --------------------------------------------------------------------------
   THE RUBBER STAMP — `kind-insufficient`, first-class and never an abstention.
   §1: "'INSUFFICIENT EVIDENCE' is a rubber stamp, and in ch13 that stamp sits
   at the TOP." The row gets the stamp ring behind it, multiplied onto the page
   (multiply is what ink on paper actually does, and it means the plate's opaque
   cream ground disappears into the ledger's cream instead of showing as a
   pasted rectangle), and the words in typed stamp blue-black. It is set apart
   by SPACE and by being its own object, never by dimming.

   THE TILT IS ON THE IMPRESSION, NOT ON THE ROW, and that is load-bearing, not
   taste. `mix-blend-mode` blends against the nearest ISOLATED GROUP, and any
   element that creates a stacking context is one — so a `transform` on the
   button would trap the multiply inside the button's own (empty) backdrop and
   the plate would land as an opaque cream rectangle pasted on the page instead
   of as ink pressed into it. The row therefore stays square in EVERY state, the
   ring is rotated inside it, and the model is the truer one anyway: the words
   are the page's own type and the stamp is pressed over them at an angle.
   -------------------------------------------------------------------------- */
.cm-options .kind-insufficient {
  margin-top: 0.95rem;
  padding: 0.8rem 1.5rem;
  color: var(--pp-stamp-blue);
}
.cm-options .kind-insufficient::before {
  content: '';
  position: absolute;
  inset: -4px -6px;
  transform: rotate(-1.1deg);
  pointer-events: none;
  z-index: 0;
  background-image: url('../assets/ui/stamp-ring.png');
  background-repeat: no-repeat;
  background-size: 100% 100%;
  mix-blend-mode: multiply;
  opacity: 0.9;
}
.cm-options .kind-insufficient .kind-tag,
.cm-options .kind-insufficient .opt-text {
  position: relative;
  z-index: 1;
}
.cm-options .kind-insufficient .kind-tag {
  font-weight: 700;
  letter-spacing: var(--track-stamp);
  color: var(--pp-stamp-blue);
  opacity: 0.85;
}
.cm-options .kind-insufficient .opt-text {
  font-family: var(--font-type);
  font-size: 0.86rem;
  letter-spacing: var(--track-type);
  line-height: 1.5;
  color: var(--pp-stamp-blue);
  background-image: linear-gradient(var(--pp-stamp-blue), var(--pp-stamp-blue));
}
.cm-options .kind-insufficient:hover,
.cm-options .kind-insufficient:focus-visible {
  background: none;
  color: var(--pp-stamp-blue);
  transform: none;
}

/* --------------------------------------------------------------------------
   OBSERVE strip (v1.4) — Region Focus offers, above the READ options.
   Deliberately NOT in the choice register: these are not things Sam says or
   concludes, they are things Sam looks harder at. So no written hand and no
   underline of the choice kind — they are MARGIN NOTES on the same working
   page: a ruled head, typed entries, prices as ink tallies. (Engine fact: the
   strip is built on READ menus only, so it only ever appears on the page.)
   -------------------------------------------------------------------------- */
.cm-focus { margin-bottom: 0.7rem; }
.cm-focus[hidden] { display: none; }

/* The Attention pool rides `margin-left: auto` at the head's right end, so it
   is the rightmost thing in the whole menu and the first thing to fall off the
   paper. The right inset is the guarantee that it does not: whatever the page's
   crop does, the tallies stop 1.4rem short of the content box, which is itself
   3.4rem short of the page's edge. */
.cf-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding-bottom: 0.3rem;
  padding-right: 1.4rem;
  margin-bottom: 0.35rem;
  border-bottom: 1px solid var(--pp-rule);
}
.cf-title {
  font-family: var(--font-type);
  font-size: 0.57rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--pp-type-dim);
}
.cf-pool { display: flex; gap: 0.3rem; align-items: center; margin-left: auto; }

.cf-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.06rem; }

/* No card, no box, no border: an entry written in the margin. */
.cf-row {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  width: 100%;
  text-align: left;
  font-family: var(--font-type);
  font-size: 0.72rem;
  letter-spacing: var(--track-type);
  color: var(--pp-type-dim);
  background: none;
  border: 0;
  padding: 0.3rem 0.2rem 0.3rem 0;
  cursor: pointer;
  transition: color var(--pp-t-lift) var(--pp-ease-paper);
}
.cf-row:hover:not(:disabled),
.cf-row:focus-visible { color: var(--pp-type); }
/* the same graphite underline as an option row, on the entry's own words */
.cf-row .cf-what {
  background-image: linear-gradient(var(--pp-type), var(--pp-type));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--pp-t-lift) var(--pp-ease-paper);
}
.cf-row:hover:not(:disabled) .cf-what,
.cf-row:focus-visible .cf-what { background-size: 100% 1px; }

.cf-subject { color: var(--pp-type); }
.cf-where {
  margin-left: 0.4rem;
  color: var(--pp-type-dim);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-size: 0.62rem;
}
/* the quoted line stays prose — it is somebody's actual words on the page */
.cf-when {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.74rem;
  color: var(--pp-type-faint);
}
.cf-cost { display: flex; gap: 0.22rem; align-items: center; flex: none; }

/* Unaffordable rows are SHOWN and disabled — never hidden. Seeing the thing
   you cannot buy is the lesson the economy exists to teach. Two channels, as
   the law insists: the pale ink AND the struck price — never dimming alone. */
.cf-row.unaffordable { cursor: default; color: var(--pp-type-faint); }
.cf-row.unaffordable .cf-subject,
.cf-row.unaffordable .cf-when { color: var(--pp-type-faint); }
.cf-row.unaffordable .cf-cost { position: relative; }
.cf-row.unaffordable .cf-cost::after {
  content: '';
  position: absolute;
  left: -2px; right: -2px; top: 50%;
  height: 1.5px;
  background: var(--pp-graphite);
}

/* --------------------------------------------------------------------------
   A06 — the whisper (single-cue lockout). Full text, full tag, no colour of
   refusal: the option is legible and unavailable, and the ellipsis is the only
   thing added. ONE STRUCTURAL CHANNEL, and it is not dimming: what was a dotted
   left rule on the old card is now a DOTTED UNDERLINE already drawn under the
   words — provisional, not committed — plus the italic hand. No refusal colour,
   no shake, no red.
   -------------------------------------------------------------------------- */
.cm-options button.whisper {
  cursor: default;
  color: var(--pp-type-faint);
}
.cm-options button.whisper .opt-text {
  background-image: none;
  border-bottom: 1.5px dotted var(--pp-type-faint);
  padding-bottom: 1px;
  font-style: italic;
}
.cm-options button.whisper:hover .opt-text { background-size: 0% 1.5px; }
.cm-options .whisper .kind-tag { color: var(--pp-type-faint); letter-spacing: 0.2em; }
.opt-ellipsis {
  color: var(--pp-type-faint);
  font-family: var(--font-hand);
  margin-left: 0.3em;
  letter-spacing: 0.25em;
}

/* --------------------------------------------------------------------------
   v1.6 — @insufficient-first. The stamped row is hoisted to position 1 by the
   engine (payload order IS render order). Its own styling is untouched: same
   impression, same stamp blue-black, same tag, same weight, same tilt. The ONLY
   thing suppressed is the gap that sets it apart from the options ABOVE it,
   because in position 1 there is nothing above it and a stray inset would read
   as an annotation. Nothing here marks the move, and nothing may be added that
   does. (The impression's rotation throws ~5px at the row's ends, which is
   inside the head's own 0.7rem bottom margin, so a hoisted stamp still touches
   nothing above it.)
   -------------------------------------------------------------------------- */
.cm-options li:first-child .kind-insufficient { margin-top: 0; }

/* --------------------------------------------------------------------------
   v1.6 — overdrawn (the question economy). The row is spent, not gone:
   rendered, legible, unavailable. Same treatment as an unaffordable focus row
   — pale ink, cursor default, no hover — and NO added glyph, word or colour.
   The writing simply goes pale, as pencil does when the hand did not press; the
   pool beside the prompt is the only statement of why.
   -------------------------------------------------------------------------- */
.cm-options button.overdrawn {
  cursor: default;
  color: var(--pp-type-faint);
}
.cm-options button.overdrawn:hover .opt-text { background-size: 0% 1.5px; }

/* The interlude's pool: one mark per question, hollow once spent. Same
   arithmetic-in-marks grammar as the attention row, in its own shape (a
   question is not an aperture) so the two are never confused at a glance. */
.cm-questions {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  margin: 0.15rem 0 0.5rem;
}
.cm-questions[hidden] { display: none; }
/* Inked in, hollow once spent — and it KEEPS ITS OWN SHAPE, so it stays a small
   square while the Attention glyph on the same page becomes a tally stroke.
   Drawn nowhere but inside the menu, so it is stated here once and needs no
   `.cm` scope. */
.q-chip {
  width: 10px;
  height: 10px;
  flex: none;
  border: 1.5px solid var(--pp-type-dim);
  position: relative;
  opacity: 0.95;
}
.q-chip::after {
  content: '';
  position: absolute;
  inset: 2.5px;
  border-radius: 1px;
  background: var(--pp-type-dim);
}
.q-chip.spent { opacity: 0.5; }
.q-chip.spent::after { background: transparent; }

/* The one-time house confirm. It is INFORMATION, not advice: it reuses the
   choice menu's own shell and type, and adds no icon, no colour, no weight
   that any other choice does not have. Only the register changes: full ink,
   upright. */
.cm.confirming .cm-prompt {
  font-style: normal;
  color: var(--pp-type);
}

/* --------------------------------------------------------------------------
   v1.8 A09 — THE ACTIVE FRAME (the lead verb's slip).

   21-progression §9: "the UI presents these in a visually distinct 'active'
   frame, because the line between reading and steering is crossed here, on
   purpose, with the game watching." 50-ui-redress §3 says what that frame is
   made of: the SAY menu is a steno pad with pencil-written options, so a lead
   row is A SEPARATE SLIP LAID OVER THE PAD — same hand, same graphite,
   different sheet, a paper-lift shadow of 2-3px and nothing else.

   AND THE LAW IT MUST NOT BREAK (§20's menu-grammar law, restated because this
   is the addition most likely to break it): none of this may mark an option as
   better, worse, safer or truer. The slip says THIS IS A DIFFERENT KIND OF ACT.
   It must never say THIS IS THE GOOD ONE. No glow, no colour, no icon, no
   confirm dialog, no "recommended" affordance, and NO RED AND NO GREEN
   ANYWHERE NEAR IT. The screen-reader kind tag lives in the same slot
   `insufficient` and `withdraw` already occupy, and says no more than
   "somatic option, one attention".
   -------------------------------------------------------------------------- */
.cm-options button.lead {
  position: relative;
  /* the lift: a different sheet resting on the pad, not a highlight — card
     stock, a torn top hairline, and the 1px->3px paper lift, which is the one
     motion the paper idiom allows */
  background: rgba(237, 230, 214, 0.78);
  border-radius: 1px;
  box-shadow: var(--pp-shadow-pinned);
  padding: 0.6rem 3.6rem 0.6rem 0.85rem;
  transform: translateY(-1px);
  transition: transform var(--pp-t-lift) var(--pp-ease-paper),
              box-shadow var(--pp-t-lift) var(--pp-ease-paper);
}
.cm-options button.lead:hover:not(:disabled),
.cm-options button.lead:focus-visible {
  background: rgba(237, 230, 214, 0.86);
  box-shadow: var(--pp-shadow-lift);
  transform: translateY(-3px);
}
/* The torn top edge of the slip — a hairline in ink, not a border treatment. It
   is the only thing that says "different sheet", and it carries no colour of
   its own. */
.cm-options button.lead::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--pp-type-faint);
  pointer-events: none;
}
/* --------------------------------------------------------------------------
   v1.10 §24.3 A13 — THE PERFORMANCE POLARITY, the INTERPRET menu's fourth claim.

   Comfort, Discomfort and Load have been claimable since Chapter 2. Performance
   is the claim that a body PRODUCED this on purpose, and it does not exist until
   #13 unlocks. The ENGINE drops the row while A13 is unheld, so there is no
   disabled state to style here and there must not be one: a greyed Performance
   row would advertise the faculty chapters early, in the one place the player is
   asked to decide.

   THE TRIPLE CHANNEL IS 20-mechanics': GILT FRAME + DOUBLE LINE + the word. It
   is the SAME Leaf Brass double rule A10's `shown` mounts, because it is the
   same claim about the same thing — this one made by the player instead of read
   off a face. The double line is the PATTERN channel and it is not optional;
   colour alone fails at this size and fails for a third of players.

   AND IT IS NOT THE GOOD ROW. No glow, no colour wash, no icon, no confirm, no
   "recommended", NO RED AND NO GREEN. Chapter 19's board beat exists to punish
   calling performance with no timeline evidence — "the player who nulls every
   calm witness will accuse a widow of acting" — and a row that looked like the
   strong one would be the chrome committing that misread on the player's behalf.
   -------------------------------------------------------------------------- */
.cm-options button.polarity-performance {
  position: relative;
}
/* On paper the triple channel is re-expressed as TWO CONCENTRIC BRASS
   HAIRLINES — a border on the pseudo plus an outline offset from it — which is
   a frame AND a double line in one object, in Leaf Brass, inside the
   paper/ink/graphite/brass range. A border, never a glow: 50-ui-redress makes
   the chrome a commonplace book and a specular sheen would be the one glossy
   object in a game made of pencil and card. It reads as "this one has been
   mounted". The frame hangs a touch left of the writing so the rows stay
   left-aligned: nothing about a Performance row's POSITION may differ from its
   neighbours. */
.cm-options button.polarity-performance::after {
  content: '';
  position: absolute;
  inset: -1px 0.25rem -1px -0.5rem;
  pointer-events: none;
  border: 1px solid rgba(142, 122, 74, 0.62);
  outline: 1px solid rgba(142, 122, 74, 0.34);
  outline-offset: 2px;
}
.cm-options button.polarity-performance .kind-tag { color: rgba(142, 122, 74, 0.95); }

/* --------------------------------------------------------------------------
   A12 THE HONEST SCALE — THE REPORT PREVIEW (grammar v1.13 §26)

   NOT A METER, NOT A BAR, NOT A DIAL, NOT A RING, NOT A PERCENTAGE. The line
   this row would put in somebody else's report, set in the same `print` medium
   the diegetic screen-lines layer uses on paper: Courier Prime, ink #22242B, NO
   GLOW (ink-coloured type plus a glow reads as a double exposure — the argument
   that minted `print` in the first place). It is the one piece of clear signage
   the game offers, at its one permanent door.

   AND IT IS NOT A MARK. It draws on EVERY row of a graded menu or on none — a
   preview over only the Probable and Certain rows would be a marker on exactly
   the two rows that must never be marked — so the grade word takes NO colour of
   its own, no weight of its own and no ordering of its own, and the four grades
   are typographically identical to one another. NO RED AND NO GREEN anywhere
   near it, in any palette. What differs between rows is what the line SAYS.
   -------------------------------------------------------------------------- */
.opt-file {
  display: block;
  margin-top: 0.42rem;
  padding-top: 0.34rem;
  border-top: 1px solid var(--pp-rule);   /* the page's own printed ruling */
  font-family: 'Courier Prime', ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.66rem;
  line-height: 1.42;
  letter-spacing: 0.06em;
  color: #22242B;
  opacity: 0.82;
  text-shadow: none;
  white-space: normal;
}
/* The grade is a WORD and it is set like the rest of the line — same face, same
   ink, same size. Its only distinction is that a typist would have capitalised
   it, which is a fact about report forms and not about this row's quality. */
.opt-file .of-grade { font-weight: 700; letter-spacing: 0.1em; }
.opt-file .of-rule { margin: 0 0.34em; opacity: 0.7; }
/* A disabled row's preview dims with the row and states nothing extra: the
   whisper and the unaffordable row both already say why they cannot be taken. */
.cm-options button:disabled .opt-file { opacity: 0.5; }

/* The price, in the SAME small ink tally the READ menu uses for Region Focus.
   Stated openly — a price is a rule of the house, and this one is the whole
   exam ("lead twice and you cannot afford to see what leading did"). */
.opt-lead-cost {
  position: absolute;
  right: 0.9rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 0.28rem;
}
.opt-lead-cost .att-chip { width: 9px; height: 9px; }
.opt-lead-cost .att-chip::after { inset: 2px; }
/* Priced out of reach: shown and disabled, never hidden — the same law as the
   unaffordable focus row and the overdrawn question. The struck price is the
   second channel, drawn as a graphite pen-stroke; pale ink alone is never the
   statement. */
.cm-options button.unaffordable {
  cursor: default;
  color: var(--pp-type-faint);
  transform: none;
  box-shadow: none;
}
.cm-options button.unaffordable:hover { transform: none; box-shadow: none; }
.cm-options button.unaffordable:hover .opt-text { background-size: 0% 1.5px; }
.cm-options button.unaffordable .opt-lead-cost { position: absolute; }
.cm-options button.unaffordable .opt-lead-cost::after {
  content: '';
  position: absolute;
  left: -2px; right: -2px; top: 50%;
  height: 1.5px;
  background: var(--pp-graphite);
}

/* The hold's stated +1 — the one consequence an option may advertise. */
.opt-hold {
  position: absolute;
  right: 0.9rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 0.28rem;
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  color: var(--pp-type-dim);
}

/* ==========================================================================
   THE RULED GRID — the pad and the page, quantized
   ==========================================================================
   Everything above states what the menu is MADE of. This states where its
   lines LAND. The mechanism and the shim's derivation are on :root under THE
   RULE LOCK; this block is only the application of it to these two surfaces.

   SCOPE, AND WHY IT IS TIGHT. Every selector below carries
   `.cm:is(.menu-say, .menu-read)`, so a menu of any other kind — which carries
   neither class and gets no plate and no ruling — keeps today's metrics
   exactly. `:is()` takes the highest specificity of its arguments, so
   `.cm:is(.menu-say, .menu-read) X` is (0,3,0), identical to writing the pair
   out longhand, and outranks every base rule it quantizes.

   THE PITCH is 1.6rem on both, and it is not a free number: at 16px root that
   is 25.6px, and the SAY row it has to hold is a 0.56rem typed tag over a
   clamp(1.12rem, 1.7vw, 1.3rem) written line. Two rules is 3.2rem; the row it
   replaces measured 0.672 + 0.1 + 1.6 + 0.68 padding + 0.15 gap = 3.202rem. So
   the grid is the rhythm the menu ALREADY had, to within half a pixel, and
   neither menu grows. Caveat's own content box at the clamp's top end is
   1.26 x 1.3 = 1.638rem, which fits 1.6rem the way handwriting fits a pad —
   the metric box overhangs by two thirds of a pixel and the ink never does.

   ONE RULE OF THE HOUSE: an offset between two written lines is 0 or a whole
   multiple of --rp, and nothing else. Every `0.34rem`, `0.15rem`, `0.7rem` and
   `0.95rem` below is not being tidied away — it is being spent, because a
   fraction of a rule anywhere puts every line under it in the gap.
   ========================================================================== */

/* The pitch, the ruling image and the surface's default face metric are all
   declared once on THE RULED SURFACES, above `.cm.menu-say`. A line that
   forgets to state its face therefore falls back to the hand's metric, which
   costs at most (0.698 − 0.44)/2 = 0.13em of drift — a hairline, never a line.

   --- the line box + the shim, for every quantized line ------------------- */
.cm:is(.menu-say, .menu-read) .cm-prompt,
.cm:is(.menu-say, .menu-read) .cm-file,
.cm:is(.menu-say, .menu-read) .cf-title,
.cm:is(.menu-say, .menu-read) .cf-what,
.cm:is(.menu-say, .menu-read) .cf-when,
.cm:is(.menu-say, .menu-read) .kind-tag,
.cm:is(.menu-say, .menu-read) .opt-text,
.cm:is(.menu-say, .menu-read) .opt-file,
.cm:is(.menu-say, .menu-read) .opt-ellipsis {
  position: relative;
  top: calc((var(--rp) - var(--rp-k) * 1em) / 2);
}

/* --- the face each of those lines is set in ------------------------------ */
.cm:is(.menu-say, .menu-read) .cm-file,
.cm:is(.menu-say, .menu-read) .cf-title,
.cm:is(.menu-say, .menu-read) .cf-what,
.cm:is(.menu-say, .menu-read) .kind-tag,
.cm:is(.menu-say, .menu-read) .opt-file,
.cm:is(.menu-say, .menu-read) .opt-hold,
.cm:is(.menu-say, .menu-read) .kind-insufficient .opt-text {
  --rp-k: var(--rp-k-type);
}
.cm:is(.menu-say, .menu-read) .cm-prompt,
.cm:is(.menu-say, .menu-read) .cf-when {
  --rp-k: var(--rp-k-prose);
}
/* The stamp's opt-text carries its own `line-height: 1.5` (THE RUBBER STAMP
   block, (0,3,0)) stated ON the element, so the button's inherited rule never
   reaches it — restated here at (0,4,0) so the stamped words take the page's
   line box too. This matters most in ch13, where the hoisted stamp's text
   WRAPS: each wrapped line must be a whole rule, or every row under the stamp
   arrives ten pixels off the printing. */
.cm:is(.menu-say, .menu-read) .kind-insufficient .opt-text {
  line-height: var(--rp);
}

/* --- the head: the prompt, the file shortcut, the question pool ---------- */
/* `align-items: flex-end` is the head's own resting rule and is what makes the
   pool and the shortcut hang from the prompt's LAST line — which is a rule.
   The head's own height is therefore a whole number of rules whatever it
   holds, so the options below it start on one. */
.cm:is(.menu-say, .menu-read) .cm-head { margin-bottom: var(--rp); }
.cm:is(.menu-say, .menu-read) .cm-prompt { line-height: var(--rp); }
/* `height` as well as `line-height`, because the shortcut carries a 1px
   `border-bottom` that is transparent at rest: everything in `.cm` is
   border-box, so a stated height makes the border eat into the rule instead of
   adding a pixel to it — which matters on the one layout where the shortcut is
   the tallest thing in the head (a menu with no prompt). */
.cm:is(.menu-say, .menu-read) .cm-file {
  line-height: var(--rp);
  height: var(--rp);
  padding-bottom: 0;
}
/* The marks hang from the rule instead of straddling it: one rule tall, chips
   sitting ON the line the way a tally is drawn. */
.cm:is(.menu-say, .menu-read) .cm-questions {
  margin: 0;
  height: var(--rp);
  align-items: flex-end;
}

/* --- the OBSERVE strip (READ only, in practice; scoped to both anyway) --- */
/* The head's printed rule was a `border-bottom` standing in for a page that
   had none. The page has one now, so the border goes: two lines a pixel apart
   is a mistake, not a flourish. */
.cm:is(.menu-say, .menu-read) .cm-focus { margin-bottom: var(--rp); }
.cm:is(.menu-say, .menu-read) .cf-head {
  align-items: flex-end;
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: 0;
}
.cm:is(.menu-say, .menu-read) .cf-title { line-height: var(--rp); }
.cm:is(.menu-say, .menu-read) .cf-pool { align-self: flex-end; }
.cm:is(.menu-say, .menu-read) .cf-rows { gap: 0; }
/* THE ROW'S ALIGNMENT MOVES FROM `baseline` TO `flex-start`, and the baselines
   still line up — they line up BETTER. Flex baseline alignment makes the row
   as tall as its tallest ascent plus its deepest drop, so a row holding a
   0.72rem typed entry beside a 0.74rem prose quote came out ~0.6px taller than
   a rule and every row after it drifted. With `flex-start` each child is
   exactly one rule tall and each is shimmed onto the same rule by its own
   face's metric, so the row is exactly one rule and the ink agrees. */
.cm:is(.menu-say, .menu-read) .cf-row {
  align-items: flex-start;
  line-height: var(--rp);
  padding: 0 0.2rem 0 0;
}
/* the price keeps its own height (its struck-through rule is drawn at 50% of
   it) and hangs from the row's baseline */
.cm:is(.menu-say, .menu-read) .cf-cost { align-self: flex-end; }

/* --- the options ---------------------------------------------------------
   The button's own padding is spent: the two rules ARE the row. `line-height`
   is set here rather than on each child so a stray inline node inside a row
   still occupies exactly one rule. */
.cm:is(.menu-say, .menu-read) .cm-options { gap: 0; }
.cm:is(.menu-say, .menu-read) .cm-options button { line-height: var(--rp); }
.cm.menu-say .cm-options button { padding: 0 3.6rem 0 0; }
.cm.menu-read .cm-options button { padding: 0 0.4rem 0 0; }
.cm:is(.menu-say, .menu-read) .kind-tag { margin-bottom: 0; }
/* The report preview's `border-top` was the page's printed ruling, imitated.
   The page prints its own now, one --rp-drop under this row's own baseline. */
.cm:is(.menu-say, .menu-read) .opt-file {
  border-top: 0;
  margin-top: 0;
  padding-top: 0;
  line-height: var(--rp);
}
/* `.opt-text` is `display:block`, so the ellipsis after it already fell to its
   own line; blockifying it is what lets that line be shimmed onto the rule. */
.cm:is(.menu-say, .menu-read) .opt-ellipsis { display: block; margin-left: 0; }

/* --- A06's whisper: the SAME dotted underline, drawn without a border ------
   THE CHANNEL IS NOT NEGOTIABLE and it does not change: an uncorroborated row
   still carries the dotted underline and the italic, and still says nothing in
   colour. What changes is how the dots are DRAWN. `border-bottom: 1.5px dotted`
   plus `padding-bottom: 1px` added 2.5px to a block box that has to be exactly
   one rule tall — so a single whisper anywhere in a menu pushed every line
   under it 2.5px into the gap, and two pushed it five. Redrawn as a background
   the dots cost nothing in layout: 1.5px on, 1.5px off, 1.5px thick, in the
   same faint ink, at the same place under the same words.

   THE THREE `background-size: 0%` RULES IT HAS TO OUTRANK, all (0,3,0):
   `button:disabled .opt-text` (a whisper IS disabled, so this fires at REST),
   `button.whisper:hover .opt-text`, and `button.unaffordable:hover .opt-text`.
   Stated at (0,5,0) and (0,6,0), which is why the hover selector is repeated
   here rather than left to the resting rule. */
.cm:is(.menu-say, .menu-read) .cm-options button.whisper .opt-text,
.cm:is(.menu-say, .menu-read) .cm-options button.whisper:hover .opt-text {
  border-bottom: 0;
  padding-bottom: 0;
  background-image: repeating-linear-gradient(to right,
    var(--pp-type-faint) 0, var(--pp-type-faint) 1.5px,
    rgba(0, 0, 0, 0) 1.5px, rgba(0, 0, 0, 0) 3px);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1.5px;
}

/* --- the stamp, the slip -------------------------------------------------
   Both keep every channel they had. The stamp still gets set apart by SPACE
   and the slip still lifts off the pad — the space is just counted in rules
   now, which is what lets the writing under them stay on the page's lines.
   THE ORDER CONTRACT: `.cm-options li:first-child .kind-insufficient` (0,3,1)
   is what suppresses the gap on a hoisted stamp, and the rule above it here is
   (0,4,0) — MORE specific — so the hoist's zero has to be restated at (0,5,1)
   or v1.6's @insufficient-first quietly grows an inset again. */
.cm:is(.menu-say, .menu-read) .cm-options .kind-insufficient {
  margin-top: var(--rp);
  padding: var(--rp) 1.5rem;
}
.cm:is(.menu-say, .menu-read) .cm-options li:first-child .kind-insufficient {
  margin-top: 0;
}
.cm:is(.menu-say, .menu-read) .cm-options button.lead {
  padding: var(--rp) 3.6rem var(--rp) 0.85rem;
}

/* --- the two priced marks on a row ---------------------------------------
   Both hung from the row's vertical centre, which on a two-rule row is the
   first rule — but centred ON it, so the marks straddled the line. The tally
   is a drawn mark and hangs from the rule; the typed `+1` is a line of type
   and takes the same shim every other line does. Neither moves horizontally,
   and neither gains or loses a channel.

   `--rp-row-top` is where the row's OWN first rule is, measured from the
   button's padding-box top — which is what an absolutely positioned mark is
   offset from. It is zero on an ordinary row and one rule on the two rows that
   carry a rule of padding (the slip and the stamp), so a mark cannot be a rule
   out on the one row that pads itself. */
.cm:is(.menu-say, .menu-read) .cm-options button { --rp-row-top: 0px; }
.cm:is(.menu-say, .menu-read) .cm-options button.lead,
.cm:is(.menu-say, .menu-read) .cm-options .kind-insufficient { --rp-row-top: var(--rp); }
.cm:is(.menu-say, .menu-read) .opt-lead-cost {
  top: calc(var(--rp-row-top) + var(--rp));
  transform: translateY(-100%);
  align-items: flex-end;
}
/* NB `.opt-hold` and `.opt-lead-cost` are ABSOLUTELY positioned, so they take
   the shim through their own `top` and must never be given the shim group's
   `position: relative` — that would drop them back into the flow at the end of
   the row. Same arithmetic, different property: an absolute box's `top` is
   measured from the button's padding-box top, and the button's padding is now
   zero, so the offset that lands the baseline on rule 1 is the shim itself. */
.cm:is(.menu-say, .menu-read) .opt-hold {
  top: calc(var(--rp-row-top) + (var(--rp) - var(--rp-k) * 1em) / 2);
  transform: none;
  align-items: baseline;
  line-height: var(--rp);
}

/* ==========================================================================
   HUD — THE DESK EDGE
   ==========================================================================
   50-ui-redress §1: "a narrow painted desk-edge along the top: the strip is
   worn desk wood in shadow; controls are small brass/enamel objects on it." The
   desk edge is ALWAYS there, the way a desk is always there. No numbers, no
   scores: attention is aperture glyphs in brass, tension is weather.

   --------------------------------------------------- the plates, measured
   assets/ui/desk-edge.png — 1600x140, opaque, worn dark wood. Row-luminance
   profile measured off the file: y 0.00-0.43 = the desk SURFACE receding into
   shadow (mean L 33-46); y 0.43-0.65 = the lit arris, the front edge catching
   the lamp (mean L 62-83, peak at 0.57); y 0.68-1.00 = the front face in deep
   shadow (mean L 19-23). The strip therefore shows the SURFACE with the arris
   just breaking at its lower cut — objects stand on wood, and the light runs
   off the bottom of the strip. `--hb-edge-y` is the one knob for that.

   assets/ui/desk-objects-cut.webp — 900x600, TRUE ALPHA, 3x2 grid of 300x300
   cells. Per-cell alpha bboxes measured off the file; every object's centroid
   is exactly (150,150) in its own cell, so cell registration is perfect and
   `background-size: 300% 200%` + a corner background-position is all the crop
   any object needs:

     cell            control / state              bg-position   bbox in cell        h/300
     r1c1 watch OPEN   pause, playing               0%   0%      x75-225  y18-282    0.880
     r1c2 watch CLOSED pause, [aria-pressed=true]   50%  0%      x70-230  y48-252    0.680
     r1c3 bell         sound, unmuted               100% 0%      x50-251  y62-239    0.590
     r2c1 bell + cloth sound, [aria-pressed=true]   0%   100%    x30-269  y64-235    0.570
     r2c2 drawer pull  settings                     50%  100%    x43-257  y76-225    0.497
     r2c3 EMPTY        (unused)                     100% 100%    —                   —

   THE BASELINE. Because every object is CENTRED in its cell, equal face boxes
   would hang the flat objects (bell, pull) in mid-air on the same centre line
   as the tall one (the propped-open watch). Objects on a desk share a BOTTOM,
   not a centre. Each object's bottom sits at (150 + h/2)/300 of its face box;
   `--hb-drop` shifts the whole control down by the difference from the tallest
   (watch-open, 0.940) so every object's base lands on one line 0.940 of a face
   box below the flex line's top. Values, as fractions of --hb-face:
     watch-open   0.940 - 0.940 = 0.0000
     watch-closed 0.940 - 0.840 = 0.1000   <- the lid drops, the base does not
     bell         0.940 - 0.797 = 0.1433
     bell + cloth 0.940 - 0.783 = 0.1567
     drawer pull  0.940 - 0.750 = 0.1900
   The drop is on the BUTTON, not the face, so the hit area and the focus
   keyline follow the object down instead of floating above it.

   assets/ui/ribbon-bookmark-cut.webp — 240x420, TRUE ALPHA. Silhouette bbox
   x79-159 y14-404: the ribbon is only 80px of a 240px canvas (33.3% wide) and
   390px of 420 tall. Sizing is therefore generous and the transparent side
   margins hang: a 92px-wide face draws a 30.7px ribbon, 150px long, and the
   button box is clipped to the ribbon's own 34px column so the hanging tail
   never swallows a click meant for the panel behind it.

   ------------------------------------------------------------------- laws
   1  Every word on the strip is runtime type over BLANK plates: the labels are
      the engine's own `.hb-label` text, the captions are `attr(aria-label)`,
      the Record count is the engine's `.ledger-count` text node. No lettering
      is baked into any asset used here.
   2  ONE saturated element: the ribbon. Everything else on the wood is brass,
      bone or paper. The only other red is the mandated :focus-visible keyline,
      which exists only under keyboard focus.
   3  Hover is a paper lift — translateY(var(--pp-lift-y)) plus a DEEPER
      drop-shadow on the object's own alpha. No glow anywhere.
   4  No `order` and no `*-reverse`. DOM order is untouched, so screen order ==
      tab sequence — loupe · attention · faculties | watch · ribbon · bell · pull.
      (The ribbon reads better at the extreme right, but the engine renders it
      between pause and sound and moving it visually would desync the tab
      order — flagged, not improvised.)
   5  Lettering is hidden ONLY by the clip-path idiom `.quiet-live` uses. No
      `display:none` on a label span anywhere.
   8  `body.bare .hud-bar { display:none !important }` (css/stage.css) is never
      fought. The strip's own `display: flex` is stated below and always has
      been; it loses to that rule on !important alone, whatever the specificity
      or the file order. NOTHING in this sheet is !important, so the room with
      no instruments stays empty. (stage.css's comment reads as though the
      !important were guarding against a `display` declaration; it is really
      guarding against the specificity a prefixed skin selector would have had.
      With the skins folded away there is no such selector left to guard.)
   9  `.att-chip` is shared across four surfaces; it is restyled for brass ONLY
      under `.hud-bar .attention` — the pool on the desk edge.
   ========================================================================== */
#hud { pointer-events: none; font-family: var(--font-ui); }

/* THE STRIP. Worn wood, full bleed, the objects' flex line sitting on the
   surface with the lit edge breaking at the bottom cut. `background-origin:
   border-box` so the 100%/200% sizing measures the whole strip and not the
   padding box. The `display: flex` is the row's own and predates the paint —
   see law 8 for why `body.bare` still wins over it. */
.hud-bar {
  position: relative;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  height: var(--hb-h);
  gap: clamp(10px, 1.6vw, 22px);
  padding: 5px var(--hb-pad-x);
  pointer-events: auto;
  background-color: #241a12;                             /* --pp-wood-deep */
  background-image: url('../assets/ui/desk-edge.png');
  background-repeat: no-repeat;
  background-origin: border-box;
  background-clip: border-box;
  background-size: 100% 200%;
  background-position: center var(--hb-edge-y);
  /* the strip is an object lying over the stage: a contact line and a soft
     falloff onto the panel, never a border and never a glow */
  box-shadow:
    inset 0 -1px 0 rgba(0, 0, 0, 0.55),
    0 7px 16px -6px rgba(0, 0, 0, 0.72);
}

/* Attention — small aperture glyphs (filled = available, hollow = spent) */
.attention {
  display: flex;
  gap: 0.45rem;
  align-items: center;
}
/* The aperture glyph itself is shared: the HUD row, the OBSERVE strip's pool
   readout, a focus row's price, a hold's stated +1. One shape, one meaning,
   wherever attention is being counted. */
.att-chip {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid var(--hn-silver);
  position: relative;
  flex: none;
  opacity: 0.9;
  transition: opacity var(--t-ui) ease;
}
.att-chip::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--hn-silver);
  transition: transform var(--t-ui) ease, opacity var(--t-ui) ease;
}
.att-chip.spent { opacity: 0.4; }
.att-chip.spent::after { transform: scale(0); opacity: 0; }
.cf-cost .att-chip, .opt-hold .att-chip { width: 9px; height: 9px; }
.cf-cost .att-chip::after, .opt-hold .att-chip::after { inset: 2px; }
.cf-pool .att-chip { width: 11px; height: 11px; }
.cf-pool .att-chip::after { inset: 2.5px; }

/* THE INK TALLY — `.cm.menu-read` SCOPE ONLY.
   On the working page the aperture becomes a TALLY STROKE: one short pen mark
   per unit of Attention, count preserved one-element-for-one-element, leaning
   the way a hand leans. A spent unit is a ghost of the stroke — dotted, not
   absent — so the arithmetic still reads at a glance without colour doing the
   work. Everything outside the page keeps the aperture.
   KNOWN CONSEQUENCE, recorded rather than improvised around: the two att-chip
   sites that live on the SAY menu (`.opt-lead-cost`, `.opt-hold`) fall outside
   this scope by the rule above, so a lead price stays the silver aperture on
   the pencil pad. */
.cm.menu-read .att-chip {
  width: 3px;
  height: 13px;
  flex: none;
  border: 0;
  border-radius: 0;
  opacity: 0.9;
  background: var(--pp-type);
  transform: rotate(-7deg);
}
.cm.menu-read .att-chip::after { content: none; }
.cm.menu-read .att-chip.spent {
  opacity: 0.55;
  background: none;
  border-left: 1.5px dotted var(--pp-type-faint);
}
.cm.menu-read .cf-cost { gap: 0.28rem; }
.cm.menu-read .cf-pool { gap: 0.3rem; }
.cm.menu-read .cf-cost .att-chip { height: 11px; }

/* THE POOL ON THE WOOD — `.hud-bar` scope only (law 9). Same aperture shape,
   same spent/just-spent semantics, same keyframes: only the metal changes. Cold
   silver on warm wood read as software; brass is the strip's own material, and
   nothing here is saturated.
   KNOWN CASCADE FACT, folded unchanged from the elected skin: this rule is three
   classes and `.att-chip.just-spent` below is two, so ON THE DESK EDGE the
   spend/gain beat keeps its animation but not its gold border — the ring stays
   brass through the flash. It is a near-identical hue, and the beat is carried
   by the scale keyframe either way; recorded so it is a decision and not a
   surprise. */
.hud-bar .attention { gap: 0.4rem; }
.hud-bar .attention .att-chip {
  width: 11px;
  height: 11px;
  border-color: var(--hb-brass);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6));
}
.hud-bar .attention .att-chip::after {
  inset: 2.5px;
  background: var(--hb-brass);
}
.hud-bar .attention .att-chip.spent { border-color: var(--hb-brass-dim); }

/* The aperture closing (a spend) and re-opening (a hold). Both are one beat on
   ONE glyph — the pool is never animated as a whole, because the point is
   which single unit of attention just moved. */
@keyframes att-close {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(185, 166, 124, 0.55); }
  40%  { transform: scale(1.28); box-shadow: 0 0 0 4px rgba(185, 166, 124, 0); }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(185, 166, 124, 0); }
}
@keyframes att-open {
  0%   { transform: scale(0.55); opacity: 0.2; }
  60%  { transform: scale(1.2);  opacity: 1; }
  100% { transform: scale(1);    opacity: 0.9; }
}
.att-chip.just-spent { border-color: var(--hn-gold); animation: att-close 520ms ease-out; }
.att-chip.just-gained { border-color: var(--hn-gold); animation: att-open 520ms ease-out; }

/* Holding past the silence budget. The refusal has to be legible or the rule
   stays invisible — but it is a settling, not a buzz: no fail-state language,
   no red, nothing that reads as a penalty. */
@keyframes att-denied {
  0%, 100% { opacity: 1; }
  35%      { opacity: 0.35; }
}
.attention.denied { animation: att-denied 640ms ease-in-out; }

@media (prefers-reduced-motion: reduce) {
  .att-chip.just-spent, .att-chip.just-gained, .attention.denied { animation: none; }
  .att-chip.just-spent, .att-chip.just-gained { border-color: var(--hn-gold); }
}

/* Faculties — held abilities, struck into the wood as typed marks (hover
   carries the name). The grant beat is authored in-script; this is only the
   persistent reflection so a mid-chapter grant shows up the beat it lands.
   The box is gone and the code stays. NOTE a real contradiction in the brief,
   resolved rather than improvised: "typed ink marks on the wood" cannot be
   INK — --pp-type #22242b on wood at L~35 is illegible. The mark is therefore
   struck in bone (ledger cream at 62%) with a dark relief shadow, so it reads
   as a stencil worn into the wood rather than a UI chip. Contrast against the
   strip's mid-tone is ~6:1. */
.faculties { display: flex; gap: 0.42rem; align-items: center; }
.faculties[hidden] { display: none; }
.fac-chip {
  font-family: var(--font-type);
  font-size: 0.56rem;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  color: var(--hb-bone);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.7);
}

.hud-spacer { flex: 1; }

/* --------------------------------------------------------------------------
   THE OBJECTS. Each `data-ctl` control becomes its cell of the sprite sheet.
   The button keeps its box (a generous 44px target) and carries the baseline
   drop; the face carries the crop and the lift. There are no pills on this
   strip and no lettering: the words live on in `.hb-label` (clip-hidden, still
   the accessible text) and on the paper caption below the object.
   -------------------------------------------------------------------------- */
.hud-bar button[data-ctl] {
  position: relative;
  top: var(--hb-drop, 0px);
  flex: none;
  width: var(--hb-face);
  height: var(--hb-face);
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  transition: transform var(--pp-t-lift) var(--pp-ease-paper);
}

/* 50-ui-redress W0 hook — every desk-edge control carries an empty `.hb-face`
   ahead of its `.hb-label`. The face IS the painted brass object; it is
   load-bearing now, not a placeholder. */
.hud-bar button[data-ctl] .hb-face {
  display: block;
  width: 100%;
  height: 100%;
  background-image: url('../assets/ui/desk-objects-cut.webp');
  background-repeat: no-repeat;
  background-size: 300% 200%;   /* 3x2 grid of 300x300 cells */
  filter: var(--hb-shadow-rest);
  transition: filter var(--pp-t-lift) var(--pp-ease-paper),
              transform var(--pp-t-lift) var(--pp-ease-paper);
}

/* --- the crops, one line each ------------------------------------------- */

/* PAUSE — the pocket watch. Playing: lid open on its hinge, dial blank (the
   plate carries no numerals and no hands, by the no-text law). Paused
   ([aria-pressed="true"]): the lid is shut. The base does not move; only the
   lid drops, which is what a closing watch actually does. */
.hud-bar button[data-ctl="pause"] { --hb-drop: 0px; }
.hud-bar button[data-ctl="pause"] .hb-face {
  background-position: 0% 0%;                        /* r1c1 watch OPEN   */
}
.hud-bar button[data-ctl="pause"][aria-pressed="true"] {
  --hb-drop: calc(var(--hb-face) * 0.1000);
}
.hud-bar button[data-ctl="pause"][aria-pressed="true"] .hb-face {
  background-position: 50% 0%;                       /* r1c2 watch CLOSED */
}

/* SOUND — the desk bell. Muted ([aria-pressed="true"]) is not a slash and not
   a crossed-out speaker: a folded grey cloth is laid over the dome. The cloth
   IS the mute, made physical — you can see why the bell cannot ring. */
.hud-bar button[data-ctl="sound"] {
  --hb-drop: calc(var(--hb-face) * 0.1433);
}
.hud-bar button[data-ctl="sound"] .hb-face {
  background-position: 100% 0%;                      /* r1c3 bell bare    */
}
.hud-bar button[data-ctl="sound"][aria-pressed="true"] {
  --hb-drop: calc(var(--hb-face) * 0.1567);
}
.hud-bar button[data-ctl="sound"][aria-pressed="true"] .hb-face {
  background-position: 0% 100%;                      /* r2c1 bell + cloth */
}

/* SETTINGS — the drawer pull. The settings card hangs off it. */
.hud-bar button[data-ctl="settings"] {
  --hb-drop: calc(var(--hb-face) * 0.1900);
}
.hud-bar button[data-ctl="settings"] .hb-face {
  background-position: 50% 100%;                     /* r2c2 drawer pull  */
}

/* --- the paper lift (law 3) --------------------------------------------- */
.hud-bar button[data-ctl]:hover .hb-face,
.hud-bar button[data-ctl]:focus-visible .hb-face {
  transform: translateY(var(--pp-lift-y));
  filter: var(--hb-shadow-lift);
}

/* --- THE WORDS — clip-hidden labels, paper captions ----------------------
   The label span keeps the accessible text it has always carried and is hidden
   by the `.quiet-live` clip-path idiom (law 5), never display:none. The visible
   caption is `attr(aria-label)`, so it is the SAME words the button announces
   and it cannot drift out of sync with the state: "Pause"/"Resume",
   "Sound on"/"Sound off", "Settings", "Record 3". */
.hud-bar .hb-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A tooltip as paper: a small typed chit lying under the object. `top` cancels
   the control's own baseline drop so every caption lands on one line. */
.hud-bar button[data-ctl]::after {
  content: attr(aria-label);
  position: absolute;
  left: 50%;
  top: calc(100% + 9px - var(--hb-drop, 0px));
  transform: translateX(-50%) rotate(-1.2deg);
  padding: 2px 6px;
  font-family: var(--font-type);
  font-size: 9px;
  line-height: 1.25;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--pp-type);
  background: var(--pp-card);
  box-shadow: var(--pp-shadow-rest);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--pp-t-lift) var(--pp-ease-paper);
}
.hud-bar button[data-ctl]:hover::after,
.hud-bar button[data-ctl]:focus-visible::after { opacity: 1; }

/* --- RECORD — the commonplace book's ribbon, hanging over the edge -------
   The button box is only the ribbon's own 34px column so the 150px tail is
   decoration and not a dead zone over the panel; the face overhangs it with
   pointer-events:none. `align-self: flex-start` + a negative top start the
   ribbon a hair above the frame so it reads as coming from behind the wood. */
.hud-bar button[data-ctl="record"] {
  --hb-drop: 0px;
  align-self: flex-start;
  top: -3px;
  width: calc(var(--hb-ribbon-w) * 0.36);   /* the ribbon column, 33px @92  */
  height: calc(var(--hb-ribbon-w) * 1.00);  /* clickable down to y~92       */
}
.hud-bar button[data-ctl="record"] .hb-face {
  position: absolute;
  left: 50%;
  top: -8px;
  width: var(--hb-ribbon-w);
  height: calc(var(--hb-ribbon-w) * 1.75);  /* 240:420 canvas               */
  transform: translateX(-50%);
  background-image: url('../assets/ui/ribbon-bookmark-cut.webp');
  background-size: 100% 100%;
  background-position: 0 0;
  pointer-events: none;
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.66));
}
.hud-bar button[data-ctl="record"]:hover .hb-face,
.hud-bar button[data-ctl="record"]:focus-visible .hb-face {
  transform: translateX(-50%) translateY(var(--pp-lift-y));
  filter: drop-shadow(0 6px 9px rgba(0, 0, 0, 0.62));
}
/* the caption clears the frayed tip instead of landing on the silk */
.hud-bar button[data-ctl="record"]::after {
  top: calc(var(--hb-ribbon-w) * 1.68);
}

/* The count: a paper tag tied on near the tip, the number typed in ink. The
   engine writes this text node; nothing here invents a word. */
.ledger-tab .ledger-count {
  position: absolute;
  left: 50%;
  top: calc(var(--hb-ribbon-w) * 1.08);
  transform: translateX(-50%) rotate(-3deg);
  margin: 0;
  min-width: 1.1em;
  padding: 1px 4px;
  font-family: var(--font-type);
  font-size: 9px;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-align: center;
  color: var(--pp-type);
  background: var(--pp-card);
  box-shadow: var(--pp-shadow-rest);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   THE LOUPE — the Quiet Rooms control (v1.8 §22.8).

   50-ui-redress §1: the HUD is a painted desk edge and its controls are small
   brass and enamel OBJECTS lying on it. This one is a jeweller's loupe. In an
   ordinary scene it lies OPEN ON ITS HINGE, lens up — the stack is live and the
   object says so without a word. Under `@quiet` it lies FOLDED SHUT AND FLAT on
   the wood, with no lens showing.

   IT NEVER LIGHTS. No pulse, no glow, no bloom, no outline, no tooltip, no
   first-time flare, no "new" dot. It gets the same 2-3px paper-lift hover
   shadow every other object on the desk edge has, and nothing else — no colour
   of invitation, no red, no green, no warm/cool valence anywhere near it. A
   player who does not think to pick it up will not be told to.

   It carries no `data-ctl`, so the sprite crop and the paper caption above
   cannot reach it: no lettering, no uppercase tracking, no border box and NO
   TOOLTIP, because it is an object and not a label. The only thing the desk
   edge adds is the contact shadow every other object on this wood has, so it
   reads as lying on the strip rather than floating.
   -------------------------------------------------------------------------- */
.hud-bar button.loupe {
  position: relative;
  width: 34px;
  height: 26px;
  flex: none;
  padding: 0;
  background: none;
  border: 0;
  transition: box-shadow var(--t-ui) ease, transform var(--t-ui) ease;
}
/* the paper lift, and nothing else */
.hud-bar button.loupe:not(:disabled):hover,
.hud-bar button.loupe:not(:disabled):focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.5);
}
.hud-bar button.loupe:disabled { cursor: default; }

.loupe-body {
  position: absolute;
  inset: 0;
  display: block;
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.55));
}

/* the barrel: a small flat cylinder lying on the wood */
.loupe-barrel {
  position: absolute;
  left: 3px;
  top: 7px;
  width: 20px;
  height: 12px;
  border: 1px solid rgba(185, 166, 124, 0.55);
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(185, 166, 124, 0.20), rgba(185, 166, 124, 0.06));
}
/* the lens: only visible when the thing is open on its hinge */
.loupe-lens {
  position: absolute;
  right: 2px;
  top: 4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid rgba(201, 210, 212, 0.55);
  background: radial-gradient(circle at 38% 34%, rgba(201, 210, 212, 0.22), rgba(30, 78, 86, 0.18));
  opacity: 0;
  transform: rotate(-38deg) translateX(-6px);
  transition: opacity var(--t-ui) ease, transform var(--t-ui) ease;
}
.loupe-hinge {
  position: absolute;
  left: 20px;
  top: 10px;
  width: 4px;
  height: 6px;
  border-radius: 1px;
  background: rgba(185, 166, 124, 0.6);
}
.loupe.open .loupe-lens { opacity: 1; transform: rotate(0deg) translateX(0); }
.loupe.folded .loupe-lens { opacity: 0; }

/* The hold ring, closing around the barrel. Same brass, same shape and same
   behaviour as the [PORTRAIT] ring — early release drains it back and that is
   the whole of the "penalty". */
.loupe-ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 30px;
  height: 30px;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.loupe-ring .lr-track { fill: none; stroke: rgba(201, 210, 212, 0.14); stroke-width: 2; }
.loupe-ring .lr-fill {
  fill: none;
  stroke: var(--hn-gold);
  stroke-width: 2;
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}
.loupe:not(.holding) .loupe-ring .lr-fill { transition: stroke-dashoffset 380ms ease; }

/* The tray — the room, listed. Under `locked` every row is unselectable, with
   no explanation, no refusal text, no icon and no sound. */
.loupe-tray {
  pointer-events: auto;
  margin: 0 1rem;
  width: min(280px, calc(100% - 2rem));
  background: var(--surface);
  border: 1px solid var(--surface-line);
  border-radius: 2px;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
}
.loupe-tray[hidden] { display: none; }
.lt-rows { list-style: none; margin: 0; padding: 0.3rem 0; }
.lt-row {
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--font-ui);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  color: var(--text);
  background: none;
  border: none;
  padding: 0.45rem 0.8rem;
  cursor: pointer;
  transition: background var(--t-ui) ease;
}
.lt-row:hover:not(:disabled),
.lt-row:focus-visible { background: rgba(201, 210, 212, 0.08); }
.lt-row:disabled { cursor: default; opacity: 0.45; }
.lt-row.holding { background: rgba(201, 210, 212, 0.10); }

@media (prefers-reduced-motion: reduce) {
  .hud-bar button.loupe,
  .loupe-lens,
  .loupe:not(.holding) .loupe-ring .lr-fill { transition: none; }
  /* There is no keyframe animation on the desk edge, so there is nothing to
     land: removing the transitions leaves every end state exactly where it was.
     The lift collapses to the deeper shadow alone (ui-paper.css already zeroes
     --pp-lift-y and --pp-t-lift under reduce), which is the whole statement. */
  .hud-bar button[data-ctl],
  .hud-bar button[data-ctl] .hb-face,
  .hud-bar button[data-ctl]::after { transition: none; }
}

/* --------------------------------------------------------------------------
   FOCUS ON THE DESK EDGE — the paper keyline, on every control on the strip.
   The precedent is `.settings-panel :focus-visible` below. It is the one other
   red in this sheet and it exists only under keyboard focus, so the
   one-saturated-element law still reads true at rest. (The choice menu's own
   focus mark is graphite instead — see THE FOCUS MARK above; no red goes near
   an option.)

   THE LOUPE SELECTOR CARRIES `:not(:disabled)` ON PURPOSE, and this is the one
   cascade fact the fold had to restate: the loupe's paper-lift rule above is
   `.hud-bar button.loupe:not(:disabled):focus-visible` and also declares
   box-shadow. Written plainly, this keyline would tie at fewer classes and the
   lift shadow would win — a control with no visible focus mark. Matched
   specificity and stated after it, so the keyline lands (the lift's transform
   still applies; only the shadow is replaced, which is what focus should do).
   -------------------------------------------------------------------------- */
.hud-bar button[data-ctl]:focus-visible,
.hud-bar button.loupe:not(:disabled):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(142, 47, 42, 0.9);
  border-radius: 1px;
}

/* --------------------------------------------------------------------------
   NARROW — one desk, smaller.
   -------------------------------------------------------------------------- */
@media (max-width: 719px) {
  :root {
    --hb-h: 44px;
    --hb-face: 34px;
    --hb-ribbon-w: 72px;
    --hb-pad-x: 10px;
  }
  .hud-bar { gap: 8px; }
  .hud-bar .fac-chip { font-size: 0.5rem; letter-spacing: 0.14em; }
}

/* The aria-live line. Visually hidden — it is an announcement, not a caption,
   and the rule forbids the screen from saying any of this. */
.quiet-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* HOLD THE PORTRAIT — the third thing a glyph buys, on the OBSERVE strip
   beside Region Focus and the rewind. Styled as a focus row and nothing more:
   it states what it does and never what it is for. */
.cf-row.cf-hold .cf-subject { letter-spacing: 0.04em; }

/* Observation Ledger — pull-down strip of committed observations this scene.
   Row grammar: subject · cue name · polarity chip · on: "stimulus" */
.ledger-strip {
  pointer-events: auto;
  margin: 0 1rem;
  width: min(680px, calc(100% - 2rem));
  margin-left: auto;
  max-height: 42vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--surface-line);
  border-top: none;
  border-radius: 0 0 3px 3px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
}
.ledger-strip[hidden] { display: none; }

.ledger-rows {
  list-style: none;
  margin: 0;
  padding: 0.4rem 0;
}
.ledger-rows:empty::after {
  content: 'nothing on the record yet';
  display: block;
  padding: 0.8rem 1.1rem;
  font-style: italic;
  font-family: var(--font-prose);
  color: var(--text-dim);
  font-size: 0.88rem;
}

.ledger-rows li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.7rem;
  padding: 0.55rem 1.1rem;
  border-bottom: 1px solid rgba(30, 78, 86, 0.25);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}
.ledger-rows li:last-child { border-bottom: none; }

.ledger-rows .l-subject {
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: 0.64rem;
  color: var(--hn-rain);
}
.ledger-rows .l-cue { color: var(--hn-silver); }
/* v1.10 §24.1 — A13. A silhouette's Record row carries canon's NEGATIVE label
   ("hands still"), and the row wears the same hollow shape the chip does: an
   outline in place of a fill. Shape, not colour — same Gift Silver as every
   other row, no red, no green, no gilt, and no dimming, because an absence is
   not a lesser reading. It is the SHAPE channel doing the work in the one place
   the player reviews what they logged. */
.ledger-rows .l-cue.l-null {
  padding: 0 0.32em;
  border: 1px solid rgba(201, 210, 212, 0.45);
  border-radius: 1px;
  background: none;
}
.ledger-rows .l-stim {
  flex-basis: 100%;
  font-family: var(--font-prose);
  font-style: italic;
  color: var(--text-dim);
  font-size: 0.8rem;
}
.ledger-rows .l-stim::before { content: 'on: '; font-style: normal; opacity: 0.7; }

/* Shimmer rows — something existed that Sam couldn't read */
.ledger-rows li.ghost-row { opacity: 0.65; font-style: italic; }

/* "Seen, unread" — a real Record entry, not a placeholder, so it is NOT dimmed
   the way a ghost row is. Marked by its own gold rule: this is a thing that was
   available. Once the read commits without it, the rule goes hollow — the row
   stops being an errand and becomes a record. */
.ledger-rows li.unread-row {
  border-left: 2px solid rgba(185, 166, 124, 0.55);
  padding-left: 0.5rem;
}
.ledger-rows li.unread-row .l-cue { color: var(--hn-gold); font-style: italic; }
.ledger-rows li.unread-row.expired { border-left-color: rgba(143, 167, 173, 0.35); }
.ledger-rows li.unread-row.expired .l-cue { color: var(--hn-rain); }

/* Hold the Portrait's receipt. It is the SPEND'S RECEIPT and not an annotation:
   it states that the baseline moved and never which direction it means. No
   polarity chip, no stimulus, no interpretation — there is nothing to say. */
.ledger-rows li.rebaseline-row .l-cue {
  color: var(--hn-rain);
  font-style: italic;
  letter-spacing: 0.06em;
}

/* Record entries use one uninterrupted page crop. Keep the hanging ribbon
   and its caption above the reading area, including when entries scroll. */
html body .ledger-strip {
  box-sizing: border-box;
  margin-top: calc(var(--hb-ribbon-w) * 1.75 - var(--hb-h) + 24px);
  width: min(680px, calc(100% - 2rem));
  max-height: min(60dvh, calc(100dvh - var(--hb-ribbon-w) * 1.75 - 3rem));
  padding: .6rem 0;
  background: url('../assets/ui/ledger-spread-blank.webp') 12.3% 20% / 255% auto no-repeat,
    var(--pp-ledger, #e8dfc9);
  border: 1px solid rgba(72, 58, 38, .4);
  scrollbar-color: #736348 #e8dfc9;
  scrollbar-width: thin;
  overscroll-behavior: contain;
}
html body .ledger-strip .ledger-rows { padding: 0 1.25rem 0 max(2rem, 9%); }
html body .ledger-strip .ledger-rows li {
  align-items: baseline;
  gap: .4rem .65rem;
  padding: .8rem 0;
  line-height: 1.5;
  font-size: calc(.95rem * var(--reading-scale, 1));
  letter-spacing: .025em;
  border-bottom: 1px solid rgba(72, 58, 38, .24);
  overflow-wrap: anywhere;
}
html body .ledger-strip .ledger-rows li:last-child { border-bottom: 0; }
html body .ledger-strip .ledger-rows .l-subject,
html body .ledger-strip .ledger-rows .l-cue,
html body .ledger-strip .ledger-rows .l-stim,
html body .ledger-strip .pol-chip { top: 0; min-width: 0; }
html body .ledger-strip .ledger-rows .l-subject {
  font-size: calc(.8rem * var(--reading-scale, 1));
  letter-spacing: .12em;
  color: #514937;
}
html body .ledger-strip .ledger-rows .l-cue { color: #272822; }
html body .ledger-strip .ledger-rows .l-stim {
  font-size: calc(1rem * var(--reading-scale, 1));
  line-height: 1.6;
  color: #403d34;
}
html body .ledger-strip .pol-chip {
  font-size: calc(.75rem * var(--reading-scale, 1));
  letter-spacing: .08em;
  color: #514937;
}
html body .ledger-strip .ledger-rows:empty::after {
  top: 0;
  padding: .8rem 0;
  line-height: 1.5;
  font-size: calc(1.1rem * var(--reading-scale, 1));
}
html body .hud-bar button[data-ctl="record"] { min-width: 44px; }

/* Polarity chips — triple-channel: SHAPE + LABEL (+ pattern). All silver;
   color never distinguishes comfort from discomfort. */
.pol-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--hn-silver);
}
.pol-chip i {
  display: inline-block;
  width: 10px;
  height: 10px;
  flex: none;
}
/* comfort — soft round ring, solid line */
.pol-comfort i { border: 2px solid var(--hn-silver); border-radius: 50%; }
/* discomfort — angular (rotated square), hatched fill */
.pol-discomfort i {
  border: 2px solid var(--hn-silver);
  transform: rotate(45deg) scale(0.9);
  background: repeating-linear-gradient(45deg,
    var(--hn-silver) 0 1px, transparent 1px 3px);
}
/* neutral — square, dotted line */
.pol-neutral i { border: 2px dotted var(--hn-silver); }
/* noted (no authored polarity) — small tick */
.pol-noted i {
  width: 10px; height: 5px;
  border-left: 2px solid var(--hn-silver);
  border-bottom: 2px solid var(--hn-silver);
  transform: rotate(-45deg) translateY(-2px);
}

/* --------------------------------------------------------------------------
   Codex drawer — pings (cue cards) + the drawer itself.
   Cards show WHAT fired: cue name, tell one-liner, authored note, the
   anchored line. Never meaning — the shipped cue DB has none to leak.
   -------------------------------------------------------------------------- */
#codex-drawer { pointer-events: none; font-family: var(--font-ui); }

.ping-stack {
  position: absolute;
  top: 3.4rem;
  right: 0;
  width: min(340px, 86vw);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding-right: 0.9rem;
  pointer-events: none;
}

.codex-ping {
  background: var(--surface);
  border: 1px solid var(--surface-line);
  border-right: 3px solid var(--hn-silver);
  border-radius: 3px 0 0 3px;
  padding: 0.7rem 0.9rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  transform: translateX(calc(100% + 1rem));
  transition: transform 400ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 400ms ease;
  pointer-events: auto;
}
.codex-ping.in { transform: translateX(0); }
.codex-ping.out { opacity: 0; transform: translateX(30%); }

.codex-ping .ping-cue {
  font-size: 0.62rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--hn-rain);
  margin-bottom: 0.3rem;
}
.codex-ping .ping-name {
  font-family: var(--font-prose);
  font-size: 0.95rem;
  color: var(--hn-white);
  margin-bottom: 0.25rem;
}
.codex-ping .ping-tell {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--hn-silver);
}
.codex-ping .ping-note {
  font-family: var(--font-prose);
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--text-dim);
  margin-top: 0.35rem;
}
.codex-ping .ping-stim {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.76rem;
  color: var(--hn-rain);
  margin-top: 0.4rem;
}
.codex-ping .ping-stim::before { content: 'on: '; font-style: normal; }

.codex-toggle {
  position: absolute;
  top: 0.65rem;
  right: 1rem;
  pointer-events: auto;
  font-family: var(--font-ui);
  font-size: 0.66rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--hn-rain);
  background: rgba(14, 28, 34, 0.72);
  border: 1px solid var(--surface-line);
  border-radius: 2px;
  padding: 0.35rem 0.8rem;
  cursor: pointer;
  transition: color var(--t-ui) ease, border-color var(--t-ui) ease;
}
.codex-toggle:hover,
.codex-toggle:focus-visible { color: var(--hn-white); border-color: var(--accent); }

.codex-panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(380px, 92vw);
  background: rgba(10, 20, 24, 0.97);
  border-left: 1px solid var(--surface-line);
  box-shadow: -18px 0 50px rgba(0, 0, 0, 0.55);
  padding: 1.1rem 1.2rem 2rem;
  overflow-y: auto;
  pointer-events: auto;
  transform: translateX(0);
  transition: transform 320ms ease;
}
.codex-panel[hidden] { display: none; }

.codex-panel h2 {
  margin: 0 0 0.2rem;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--hn-silver);
}
.codex-panel .codex-sub {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--text-dim);
  margin-bottom: 1.1rem;
}
.codex-panel h3 {
  margin: 1.3rem 0 0.5rem;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 0.64rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--hn-rain);
  border-bottom: 1px solid rgba(30, 78, 86, 0.35);
  padding-bottom: 0.35rem;
}

.codex-cues, .codex-portraits {
  list-style: none;
  margin: 0;
  padding: 0;
}
.codex-cues li {
  padding: 0.5rem 0;
  border-bottom: 1px solid rgba(30, 78, 86, 0.18);
}
.codex-cues .c-id {
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  color: var(--hn-rain);
  text-transform: uppercase;
}
.codex-cues .c-name {
  font-family: var(--font-prose);
  font-size: 0.92rem;
  color: var(--hn-silver);
  margin: 0.15rem 0;
}
.codex-cues .c-tell {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--text-dim);
  line-height: 1.5;
}
.codex-cues:empty::after,
.codex-portraits:empty::after {
  content: 'nothing yet';
  display: block;
  font-family: var(--font-prose);
  font-style: italic;
  color: var(--text-dim);
  font-size: 0.84rem;
  padding: 0.5rem 0;
}
.codex-portraits li {
  display: inline-block;
  margin: 0.25rem 0.45rem 0.25rem 0;
  padding: 0.3rem 0.75rem;
  border: 1px solid var(--surface-line);
  border-radius: 2px;
  font-family: var(--font-prose);
  font-size: 0.85rem;
  color: var(--hn-silver);
}

.codex-close {
  position: absolute;
  top: 0.8rem;
  right: 0.9rem;
  background: none;
  border: none;
  color: var(--hn-rain);
  font-family: var(--font-ui);
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  cursor: pointer;
  padding: 0.3rem 0.5rem;
}
.codex-close:hover, .codex-close:focus-visible { color: var(--hn-white); }

/* --------------------------------------------------------------------------
   Reduced motion
   -------------------------------------------------------------------------- */
/* Every keyframe in this sheet lands a VISIBLE end state. theme.css clamps
   animations to 0.01ms globally, which would have snapped the pencil tick to
   its 100% frame (opacity .30 — a caret you cannot see), so both carets are
   pinned to a settled, legible value instead. The letterbox caret's companion
   is restated here rather than left to css/stage.css, because this sheet
   replaced the animation stage.css's own companion names; it is stated AFTER
   the `.lb-caret` rules it companions and after that sheet, so it lands. */
@media (prefers-reduced-motion: reduce) {
  .advance-caret.show { animation: none; opacity: 0.9; }
  .lb-caret.show { animation: none; opacity: 0.85; }
  .codex-ping { transition: none; }
  /* The paper transitions. Nothing here gates state on a transition, so
     removing them leaves every end state exactly where it was; the lead slip's
     lift is the one transform, and it is pinned flat with matched specificity
     (its own hover rule is four classes deep and would otherwise win). */
  .cm-options button,
  .cm-options .opt-text,
  .cm-options button.lead,
  .cf-row,
  .cf-row .cf-what,
  .cm-file { transition: none; }
  .cm-options button.lead,
  .cm-options button.lead:hover:not(:disabled),
  .cm-options button.lead:focus-visible { transform: none; }
}

/* ==========================================================================
   THE PARKED LINE — choice mode keeps the sentence the choice ANSWERS
   ==========================================================================
   What was here: `.db.under-choice { opacity: 0 }` and, in css/stage.css,
   `.lb-caption` at .28 under an open menu — both written to stop the finished
   line "bleeding through and fighting the options for legibility". The pad
   then rose OVER the line and clipped it: at 1280x720 the caption's own client
   rect intersected the menu box by its full width on every SAY and READ menu
   measured, which is the "— becau / She look / devastat" the play-through
   caught. A choice is a question about a paragraph the player can no longer
   read, and that is not a legibility trade, it is a missing premise.

   SO THE LINE MOVES INSTEAD OF FADING. It parks on the strip under the desk
   edge — above the pad, never behind it — and stays legible there until the
   pick. There is NO runtime measurement holding this together and nothing for
   js to keep in sync: THE FIT reserves `--cm-park` out of the menu's own
   max-height, so the pad's top edge cannot reach this strip at any option
   count or viewport. The two numbers are declared as a pair on :root for
   exactly that reason — change one and you must change the other.

   ONE RULE FOR BOTH VESSELS. The doubled class buys (0,4,0) so this outranks
   `.stage-frame.spread .lb-caption`'s centring AND lands after
   `.stage-frame.letterboxed.bubbles-under-choice .lb-caption`'s .28, which is
   the same specificity in the earlier sheet — the ordering hazard css/stage.css
   already paid for once at that selector. `left/right/width` are restated so
   the spread's 30% column becomes the same full-width strip: above the pad
   there is no board to keep the type off, and a paragraph reads better across
   the desk than down a 384px gutter.

   THE SCRIM is the letterbox band's ghost, not a panel: it is the type's own
   ground, opaque enough at the top to carry serif text over paint and gone by
   the bottom edge, so no rectangle meets the art. The speech bubbles keep the
   .28 step-back css/stage.css gives them — they are part of the picture, and
   the picture is meant to step back. This line is not.
   ========================================================================== */
.stage-frame.bubbles-under-choice.bubbles-under-choice .lb-caption {
  opacity: 1;
  top: max(0px, calc(var(--hb-h) + 0.25rem - var(--hb-frame-top)));
  bottom: auto;
  left: 0;
  right: 0;
  width: auto;
  height: auto;
  max-height: var(--cap-park-h);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  transform: none;
  justify-content: flex-start;
  /* THE RIGHT-HAND MARGIN IS THE DESK EDGE'S, NOT THE CAPTION'S. Parked, the
     line runs the full width of the frame and the last thing on that width is
     the commonplace book's ribbon, which hangs 161px down off the wood with the
     count tied to it — a measured 92px column whose left edge sits
     `--hb-pad-x + 194.6px` in from the right at every viewport, because the
     ribbon's own width and the two brass objects outboard of it are fixed
     lengths. The first cut of this ran the text under the silk and ate a word:
     at 1280x720 the ch02 interlude's line lost "questions" behind it (measured
     overlap 92 x 44.8). 206px is that 194.6 plus a gutter, so the measure stops
     short of the ribbon instead of being read through it. */
  padding: 0.35rem calc(var(--hb-pad-x) + 206px) 0.6rem clamp(14px, 4.5cqw, 54px);
  background: linear-gradient(to bottom,
    rgba(9, 12, 16, 0.74) 0, rgba(9, 12, 16, 0.66) 62%, rgba(9, 12, 16, 0) 100%);
}
.stage-frame.bubbles-under-choice.bubbles-under-choice .lb-caption::-webkit-scrollbar {
  width: 0;
  height: 0;
}
/* The hairline belongs to a caption sitting in a bar. Parked, the strip's own
   ground is the edge, and a rule across the top of it would be a second one. */
.stage-frame.bubbles-under-choice.bubbles-under-choice .lb-caption::before {
  content: none;
}

/* `.db` is the headless degrade the router only reaches with no stage on
   screen (see the dialogue-box note at the top of this sheet), so it has no
   frame to park inside — `position: fixed` is what gives it the viewport to
   measure the same strip against. It keeps `pointer-events: none` and gains no
   z: parked above the pad, it never needs to outrank it. */
.db.under-choice {
  position: fixed;
  left: 0;
  right: 0;
  top: max(0px, calc(var(--hb-h) + 0.25rem));
  bottom: auto;
  width: min(920px, 94%);
  margin: 0 auto;
  max-height: var(--cap-park-h);
  overflow-y: auto;
  scrollbar-width: none;
  opacity: 0.95;
  transform: none;
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease;
}
.db.under-choice::-webkit-scrollbar { width: 0; height: 0; }

/* THE `.pause-btn` / `.gear-btn` GLYPH RULE IS GONE, and this note is its
   headstone so nobody re-adds it. It existed to stop the flat text pills from
   letterspacing a "❚❚" and a "⚙" like a word. There are no text pills any more:
   both controls are 44px sprite faces, their `.hb-label` (which still carries
   those glyphs, and the accessible name) is clip-hidden, and the visible
   caption is a `::after` with its own type. Every declaration in it — including
   two `!important`s that would have outranked anything the strip wanted to say
   — was therefore dead, and dead `!important` on a HUD control is exactly the
   thing that bites the next person. */

/* --------------------------------------------------------------------------
   Paused state — a quiet corner marker; the frame does not gray out (the
   held shot stays a shot), but the player gets an unmistakable "held" cue.
   -------------------------------------------------------------------------- */
/* §6's "no naked dialogs" sweep reaches this one too: it was a bordered chip of
   UI text over the art. It is now a small typed paper tag laid on the shot —
   the same card paper as the settings card, ink type, a paper shadow instead of
   a stroke. Still the same one word, still pointer-events: none, still z 8.

   THE CLEARANCE, and why `top` is an expression rather than 0.9rem.
   This tag is drawn inside `#panel-layer`, which carries no z-index and so
   orders at 0 among #stage's children, while `#hud` is z-index 25. No z inside
   the panel layer can ever reach past the HUD. That never mattered while the
   HUD was four transparent text buttons with a gap in the middle; the desk edge
   put 54px of OPAQUE painted wood there, and the tag was drawn — correctly, at
   14.4px — underneath it. Moving the tag is the fix, not the stacking context —
   lowering #hud beneath the panel layer would bury every control, and raising
   the tag would lay a PAUSED chit on the desk, which is not what the
   composition says.
   The offset is not a constant because `.stage-frame` is letterboxed and
   `top` is measured from ITS top, not the viewport's. On a 16:9-or-wider window
   the frame fills the stage, its top is 0, and the tag is occluded; on a taller
   window the frame is top-anchored at the strip's lip (stage.css margin,
   --hb-frame-top = min(--hb-h, slack)) and needs less help. So the offset is
   exactly "however much of the clearance the frame's own offset has not
   already provided", and `max()` floors it at the old value so this can only
   ever push the tag DOWN, never up.
     1280x720: frame-top 0  -> top 68px   (tag at viewport y 68-87, clear)
     1280x760: frame-top 40 -> top 28px   (viewport y 68, clear)
     1280x900: frame-top 54 -> top 0.9rem (viewport y 68.4, clear)
   Everything else — paper, ink, tracking, rotation, shadow, pointer-events,
   z — is unchanged. */
body.playback-paused .stage-frame::after {
  content: 'PAUSED';
  position: absolute;
  top: max(0.9rem, calc(var(--hb-h) + 14px - var(--hb-frame-top)));
  left: 50%;
  transform: translateX(-50%) rotate(-0.8deg);
  font-family: var(--font-type);
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--pp-type, #22242b);
  background-color: var(--pp-card, #ede6d6);
  background-image: var(--plate-card-face);
  background-repeat: no-repeat;
  background-size: 144.1% auto;
  background-position: center;
  border: 0;
  border-radius: 1px;
  box-shadow: var(--pp-shadow-pinned, 0 2px 5px rgba(0, 0, 0, 0.5));
  padding: 0.3rem 0.85rem;
  pointer-events: none;
  z-index: 8;
}
/* Under `body.bare` the strip is gone (css/stage.css), so there is nothing to
   clear and the tag goes back where it belongs. Three classes, so it beats the
   two-class rule above; never !important, so it never fights bare. */
body.playback-paused.bare .stage-frame::after { top: 0.9rem; }

/* --------------------------------------------------------------------------
   Playback settings panel — a popover hanging DOWN from the HUD bar.

   This was `bottom: 3.2rem`, which anchors a popover above its container — the
   right rule for a HUD along the bottom of the screen. #hud is pinned to the
   TOP (index.html: `#hud { position:absolute; top:0; left:0; right:0 }`) and is
   only as tall as the bar, so `bottom: 3.2rem` resolved to `top: -281px` and put
   all 280px of the panel, including the whole difficulty control, off-screen
   above the viewport. It was unreachable at every window size, not just small
   ones, because the offset is measured from a container that is always ~49px tall.

   max-height + scroll is the belt-and-braces half: the panel must stay usable on
   a short window even as rows are added to it, and a settings popover you cannot
   reach is indistinguishable from a broken build.

   THE CARD NOW HANGS OFF THE DRAWER PULL, so its `top` rides `--hb-h` instead of
   a hard 3.2rem: 3.2rem is 51.2px, which tucked the card 3px UNDER a 54px strip.
   One expression, and it follows the strip down to 44px on a narrow window.
   -------------------------------------------------------------------------- */
/* THE PINNED INDEX CARD — design/50-ui-redress.md §1 ("Settings/pause — an index
   card menu, typewritten, pinned at one corner") and §6 ("no naked dialogs").
   The popover keeps every id, handler, ARIA attribute and the geometry above;
   only the skin changes. It is now a card of ui/index-card-pinned's own paper
   with a brass pin cut from ui/pin-sprites, and every control is a mark made on
   it: typed labels, pencil ticks.

   NO CORRECTNESS GRAMMAR. The ticks are graphite, the hints are ink, and the
   difficulty control (Observed tell) is three ballot boxes with a pencil tick in
   the chosen one — the standing rule holds, nothing here may read as red/green
   or as an option being the right one.

   THE PAPER WINDOW: the plate is 800x560 with the card lying slightly rotated
   inside it and a painted pin at its top-left. x200-678 / y100-432 is the
   largest window that is nothing but paper (measured min luminance 173), which
   is 167.4% x 168.7% at 62.1% / 43.9% — deliberately clear of the painted pin,
   because the pin below is drawn from the sprite sheet instead and so keeps its
   size and its corner whatever shape the card takes. */
.settings-panel {
  position: absolute;
  right: 1rem;
  top: calc(var(--hb-h) + 4px);
  width: min(340px, 88vw);
  max-height: calc(100vh - 4.6rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--pp-card, #ede6d6);
  background-image: var(--plate-index-card);
  background-repeat: no-repeat;
  background-size: 167.4% 168.7%;
  background-position: 62.1% 43.9%;
  border: 0;
  border-radius: 1px;
  box-shadow: var(--pp-shadow-contact, 0 16px 48px rgba(0, 0, 0, 0.6));
  padding: 0.95rem 1.05rem 1rem 1.05rem;
  color: var(--pp-type, #22242b);
  transform: rotate(-0.5deg) translateY(0);
  pointer-events: auto;
  z-index: 9;
  transition: transform var(--pp-t-lift, 140ms) var(--pp-ease-paper, ease),
              box-shadow var(--pp-t-lift, 140ms) var(--pp-ease-paper, ease);
}
/* Paper lift, never a glow (§3): reaching for the card raises it off the desk
   by the pin. Transform + shadow only — nothing here can move layout. */
.settings-panel:hover,
.settings-panel:focus-within {
  transform: rotate(-0.5deg) translateY(var(--pp-lift-y, -2px));
  box-shadow: var(--pp-shadow-contact, 0 16px 48px rgba(0, 0, 0, 0.6)),
              0 6px 14px rgba(0, 0, 0, 0.4);
}
.settings-panel[hidden] { display: none; }
/* The pin. Cell (0,0) of the 5x2 sprite sheet — six brass pins on row 1, four
   paperclips on row 2 — so `background-size: 500% 200%` and origin 0 0. It sits
   INSIDE the card's corner rather than overhanging it, because the panel is a
   scroll container and an overhanging pseudo-element would be clipped away. */
.settings-panel::before {
  content: '';
  position: absolute;
  left: 0.3rem;
  top: 0.2rem;
  width: 2rem;
  aspect-ratio: 4 / 5;
  background: var(--plate-pin-sprites) 0 0 / 500% 200% no-repeat;
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.45));
  pointer-events: none;
}
.settings-panel .set-title {
  margin: 0 0 0.55rem 1.6rem;
  font-family: var(--font-type);
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--pp-type, #22242b);
}
/* Focus on paper is the paper idiom's red keyline, not the gold stage accent. */
.settings-panel :focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(142, 47, 42, 0.9);
  border-radius: 1px;
}
.set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.55rem 0;
  border-top: 1px solid rgba(34, 36, 43, 0.18);
}
.set-row.col { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.set-meta { display: flex; flex-direction: column; gap: 0.18rem; }
.set-name {
  font-family: var(--font-type);
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--pp-type, #22242b);
}
.set-hint {
  font-family: var(--font-type);
  font-size: 0.58rem;
  line-height: 1.55;
  letter-spacing: 0.02em;
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}

/* --------------------------------------------------------------------------
   PENCIL TICKS. Both controls are the same mark: a ruled ballot box, and a tick
   drawn into it in graphite when the option is the live one. The box and the
   tick are CSS strokes rather than glyphs on purpose — a checkbox character
   would come from whatever fallback face the machine has, and this has to look
   like the same pencil on every machine. The word (On / Off / Learner / …) is
   still the JS-written label and still the accessible name; nothing about the
   role="switch"/aria-checked wiring moved.
   -------------------------------------------------------------------------- */
.set-toggle,
.set-seg-btn {
  position: relative;
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  background: none;
  border: 0;
  border-radius: 0;
  cursor: pointer;
  padding: 0.3rem 0.1rem 0.3rem 1.35rem;
  text-align: left;
  transition: color var(--pp-t-lift, 140ms) var(--pp-ease-paper, ease);
}
.set-toggle::before,
.set-seg-btn::before {          /* the ruled box, drawn a little off-square */
  content: '';
  position: absolute;
  left: 0.1rem;
  top: 50%;
  width: 0.72rem;
  height: 0.72rem;
  box-sizing: border-box;
  border: 1px solid rgba(58, 63, 69, 0.55);
  transform: translateY(-50%) rotate(-1.6deg);
}
.set-toggle::after,
.set-seg-btn::after {           /* the tick, in graphite, slightly hand-slanted */
  content: '';
  position: absolute;
  left: 0.24rem;
  top: 50%;
  width: 0.5rem;
  height: 0.26rem;
  box-sizing: border-box;
  border-left: 2px solid var(--pp-graphite, #3a3f45);
  border-bottom: 2px solid var(--pp-graphite, #3a3f45);
  transform: translateY(-68%) rotate(-47deg);
  opacity: 0;
  transition: opacity var(--pp-t-lift, 140ms) var(--pp-ease-paper, ease);
}
.set-toggle.on,
.set-seg-btn.on { color: var(--pp-type, #22242b); }
.set-toggle.on::after,
.set-seg-btn.on::after { opacity: 1; }
.set-toggle:hover,
.set-seg-btn:hover { color: var(--pp-type, #22242b); }
.set-toggle { flex: none; min-width: 3.6rem; }

.set-seg {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
.set-seg-btn { flex: 0 0 auto; }
.set-mode-hint {
  margin: 0.5rem 0 0;
  font-family: var(--font-type);
  font-style: italic;
  font-size: 0.58rem;
  line-height: 1.55;
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}

@media (prefers-reduced-motion: reduce) {
  .set-toggle::after,
  .set-seg-btn::after { transition: opacity 140ms linear; }
}

/* THE RE-LISTEN — the fourth thing a glyph buys (v1.11 §25.3), on the OBSERVE
   strip beside Region Focus, the rewind and Hold the Portrait. Styled as a
   focus row and nothing more: same shape, same price grammar, no emphasis, no
   marker on the line it names. It states what it does and never what it is
   for — "listen for the break" would be the chrome doing the hearing. */
.cf-row.cf-replay .cf-subject { letter-spacing: 0.04em; }
/* Reading preferences scale copy without enlarging the surrounding desk. */
body .line-text { font-size: calc(var(--fs-line) * var(--reading-scale, 1)); }
body .cm-options button { font-size: calc(clamp(1.12rem, 1.7vw, 1.3rem) * var(--reading-scale, 1)); }
body .cm-options .kind-insufficient .opt-text { font-size: calc(.86rem * var(--reading-scale, 1)); }
body .bubble--slip .bubble-text { font-size: calc(clamp(15px, min(2.1vh, 1.2vw), 19px) * var(--reading-scale, 1)); }
body.clear-reading .line-text,
body.clear-reading .cm-options button,
body.clear-reading .cm-options .kind-insufficient .opt-text {
  font-family: Arial, Helvetica, sans-serif;
  font-style: normal;
  letter-spacing: normal;
}
body.clear-reading .db.narration .line-text { color: var(--pp-card, #ede6d6); }
body.clear-reading .cm-options button { color: var(--pp-type, #22242b); }
/* Report previews carry the consequence of a late-game choice. They must
   follow the reading size along with the sentence that files them. */
body .cm:is(.menu-say, .menu-read) .opt-file {
  font-size: calc(.9rem * var(--reading-scale, 1));
  line-height: 1.45;
  margin-top: .5rem;
  top: 0;
}
body.clear-reading .cm:is(.menu-say, .menu-read) {
  --rp: calc(1.75rem * var(--reading-scale, 1));
}
body.clear-reading .cm:is(.menu-say, .menu-read) .opt-file {
  font: calc(1rem * var(--reading-scale, 1))/1.45 Arial, Helvetica, sans-serif;
  letter-spacing: normal;
}
.settings-panel .set-close,
.portrait-observe {
  display: block; padding: .7rem 1rem; margin: .75rem auto 0;
  color: var(--pp-type, #22242b); background: var(--pp-card, #ede6d6);
  border: 1px solid currentColor; border-radius: 2px; font: 700 .85rem var(--font-type);
  cursor: pointer; min-height: 44px;
}
.settings-panel .set-close { width: 100%; }
.settings-panel .set-close:focus-visible,
.portrait-observe:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
.portrait-observe:disabled { opacity: .65; cursor: default; }
.settings-panel .set-seg { flex-wrap: wrap; }

/* The pause/settings index card (design/50-ui-redress §1). Existing painted
   paper and brass pin; all lettering and conventional controls remain live.
   The native dialog still owns focus/inertness. Only the form body scrolls. */
.playback-menu {
  --pm-ink: #292923;
  --pm-pencil: #555147;
  --pm-rule: rgba(71,61,41,.3);
  --pm-brass: #877346;
  color-scheme:light;
  position:fixed; inset:0; margin:auto;
  width:min(608px, calc(100vw - 48px)); max-width:none;
  max-height:min(880px, calc(100dvh - 64px)); padding:0;
  color:var(--pm-ink); background-color:var(--pp-card,#ede6d6);
  background-image:linear-gradient(rgba(237,230,214,.32),rgba(237,230,214,.32)),var(--plate-index-card);
  background-repeat:no-repeat; background-size:100% 100%,167.4% 168.7%;
  background-position:0 0,62.1% 43.9%;
  border:1px solid #a89b7c; border-radius:1px;
  box-shadow:1px 2px 0 #b4a68a, 2px 4px 0 #5a5140,
    0 3px 7px rgba(0,0,0,.6), 0 22px 65px rgba(0,0,0,.65);
  font:16px/1.5 var(--font-prose,Georgia,serif);
  overflow:hidden; pointer-events:auto;
}
.playback-menu[open] { display:flex; flex-direction:column; }
.playback-menu[hidden] { display:none; }
.playback-menu::backdrop { background:rgba(3,11,15,.72); }
.pm-header { position:relative; padding:29px 32px 23px 82px; border-bottom:1px solid var(--pm-rule); flex:none; }
.pm-header::before {
  content:''; position:absolute; left:20px; top:17px; width:35px; aspect-ratio:4/5;
  background:var(--plate-pin-sprites) 0 0 / 500% 200% no-repeat;
  filter:drop-shadow(1px 3px 2px rgba(0,0,0,.4)); pointer-events:none;
}
.pm-title { margin:0; font:700 30px/1.15 var(--font-type,monospace); color:var(--pm-ink); }
.pm-subtitle { margin:7px 0 0; font:italic 15px/1.4 var(--font-prose,Georgia,serif); color:var(--pm-pencil); }
.pm-content { min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:4px 32px 12px; scrollbar-width:thin; scrollbar-color:#95876b transparent; }
.pm-pause-actions { display:grid; gap:13px; padding:27px 0 18px; }
.pm-pause-actions[hidden], .pm-preferences[hidden], .pm-muted-note[hidden] { display:none; }
.pm-footer { flex:none; display:flex; flex-wrap:wrap; justify-content:flex-end; gap:12px; padding:18px 32px; border-top:1px solid var(--pm-rule); background:rgba(141,120,78,.055); }
.pm-key-hint { margin:0; width:100%; font:italic 14px/1.4 var(--font-prose,Georgia,serif); color:var(--pm-pencil); text-align:center; }
.pm-button {
  border:1px solid var(--pm-pencil); border-radius:1px; padding:11px 16px; min-height:46px;
  font:700 15px/1.35 var(--font-type,monospace); cursor:pointer; color:var(--pm-ink);
  transition:box-shadow var(--pp-t-lift,140ms), transform var(--pp-t-lift,140ms);
}
.pm-primary { border:3px double var(--pm-ink); background:rgba(74,60,34,.075); }
.pm-primary:hover { background:rgba(74,60,34,.12); box-shadow:var(--pp-shadow-lift); transform:translateY(-2px); }
.pm-secondary { background:transparent; border-color:var(--pm-rule); }
.pm-secondary:hover { background:rgba(255,249,229,.25); box-shadow:var(--pp-shadow-rest); transform:translateY(-2px); }
.pm-pause-actions .pm-button { padding:17px 20px; text-align:left; font-size:17px; }
.pm-pause-actions .pm-primary::after { content:'\2192'; float:right; margin-left:12px; }
.pm-section { border:0; border-bottom:1px solid var(--pm-rule); padding:22px 0 24px; }
.pm-section:last-child { border-bottom:0; }
.pm-section-title { margin:0 0 13px; font:700 15px/1.4 var(--font-type,monospace); letter-spacing:.13em; text-transform:uppercase; color:var(--pm-pencil); }
.pm-label { display:block; font:700 16px/1.35 var(--font-type,monospace); color:var(--pm-ink); }
.pm-hint { display:block; margin:4px 0 0; font:14px/1.45 var(--font-prose,Georgia,serif); color:var(--pm-pencil); }
.pm-meta { display:flex; flex-direction:column; min-width:0; }
.pm-switch-row { display:flex; gap:18px; align-items:center; justify-content:space-between; min-height:48px; padding:8px 0; cursor:pointer; }
.pm-switch-control { display:flex; gap:8px; align-items:center; flex:none; }
.pm-switch-status { width:26px; font:14px/1.4 var(--font-type,monospace); color:var(--pm-pencil); }
.pm-switch {
  appearance:none; -webkit-appearance:none; position:relative; width:46px; height:26px;
  border:1px solid #8a806b; border-radius:2px; background:rgba(63,53,34,.16); margin:0; cursor:pointer;
  box-shadow:inset 0 1px 3px rgba(36,29,17,.15);
}
.pm-switch::before { content:''; position:absolute; top:3px; left:3px; width:18px; height:18px; border:1px solid #8a7850; border-radius:1px; background:linear-gradient(100deg,#ece2c6,#cec19d); box-shadow:0 1px 2px rgba(0,0,0,.25); }
.pm-switch:checked { background:#565449; border-color:#565449; }
.pm-switch:checked::before { left:23px; }
.pm-slider-row { margin:18px 0 2px; }
.pm-slider-label { display:flex; align-items:center; justify-content:space-between; gap:16px; }
.pm-slider-value { color:var(--pm-pencil); font:700 15px/1.4 var(--font-type,monospace); min-width:48px; text-align:right; }
.pm-range {
  appearance:none; -webkit-appearance:none; display:block; width:100%; height:44px;
  margin:3px 0 0; padding:0; border:0; border-radius:0; cursor:pointer;
  background:repeating-linear-gradient(90deg,var(--pm-rule) 0 1px,transparent 1px 10%) center calc(50% + 9px) / calc(100% - 20px) 5px no-repeat;
}
.pm-range::-webkit-slider-runnable-track { height:3px; border:0; background:linear-gradient(to right,var(--pm-pencil) 0 var(--pm-fill),#b6aa8e var(--pm-fill) 100%); box-shadow:0 1px 0 rgba(255,255,255,.3); }
.pm-range::-webkit-slider-thumb { appearance:none; -webkit-appearance:none; width:22px; height:22px; margin-top:-9px; border:3px solid var(--pm-brass); border-radius:50%; background:#e8dcc0; box-shadow:0 1px 2px rgba(39,32,17,.45),inset 0 0 0 1px #b3a278; }
.pm-range::-moz-range-track { height:3px; border:0; background:#b6aa8e; }
.pm-range::-moz-range-progress { height:3px; background:var(--pm-pencil); }
.pm-range::-moz-range-thumb { width:16px; height:16px; border:3px solid var(--pm-brass); border-radius:50%; background:#e8dcc0; box-shadow:0 1px 2px rgba(39,32,17,.45); }
.pm-select-row { margin:17px 0 2px; }
.pm-select { display:block; width:100%; margin-top:6px; min-height:44px; border:1px solid var(--pm-rule); border-bottom-color:var(--pm-pencil); border-radius:1px; color:var(--pm-ink); background:rgba(255,249,229,.25); padding:8px 11px; font:16px/1.4 var(--font-type,monospace); }
.pm-select option { background:#e8dfc9; color:#292923; }
.pm-radio-group { margin:16px 0; padding:0; border:0; min-width:0; }
.pm-radio-group legend { padding:0; margin-bottom:8px; }
.pm-radio-row { display:flex; gap:12px; align-items:flex-start; padding:13px 12px; margin:7px 0; border:1px solid var(--pm-rule); border-radius:1px; cursor:pointer; }
.pm-radio-row:has(input:checked) { border-color:var(--pm-pencil); background:rgba(74,60,34,.075); }
.pm-radio { width:19px; height:19px; margin:2px 0 0; flex:none; accent-color:#555147; }
.pm-muted-note { margin:14px 0 0; padding:10px 12px; border-left:3px solid #8c7855; background:rgba(140,120,85,.09); font-size:14px; }
.playback-menu :is(button,input,select):focus-visible { outline:3px solid #8e2f2a; outline-offset:3px; }
.playback-menu :is(input[type=range],input[type=checkbox],input[type=radio]):focus-visible { outline-offset:4px; }
body.clear-reading .playback-menu :is(.pm-title,.pm-subtitle,.pm-label,.pm-hint,.pm-button,.pm-section-title,.pm-select,.pm-key-hint) { font-family:Arial,Helvetica,sans-serif; }
@media (prefers-reduced-motion:reduce) { .playback-menu .pm-button { transition:none; transform:none; } }
body.reduce-motion .playback-menu .pm-button { transition:none; transform:none; }
@media (max-width:640px) {
  .playback-menu { width:calc(100vw - 24px); max-height:calc(100dvh - 24px); }
  .pm-header { padding:22px 18px 18px 65px; }
  .pm-header::before { left:15px; top:14px; width:28px; }
  .pm-title { font-size:25px; }
  .pm-content { padding:3px 20px 10px; }
  .pm-footer { padding:13px 20px; gap:8px; }
  .pm-footer .pm-button { flex:1 1 100%; min-height:44px; padding:9px 12px; }
  .pm-switch-row { gap:12px; }
  .pm-label { font-size:15px; }
  .pm-pause-actions .pm-button { font-size:16px; padding:16px; }
}
@media (max-height:600px) {
  .playback-menu { max-height:calc(100dvh - 24px); }
  .pm-header { padding-top:15px; padding-bottom:13px; }
  .pm-title { font-size:24px; }
  .pm-subtitle { font-size:14px; }
  .pm-footer { padding-top:10px; padding-bottom:10px; }
}

/* Pause and Settings need visible names on touch screens, where an object
   tooltip cannot introduce its action. Keep the existing 44px button targets,
   sprite identity, focus behavior, and bare-scene ownership. */
.hud-bar button:is([data-ctl="pause"], [data-ctl="settings"]) {
  top: 0;
  width: 44px;
  height: 44px;
  display: grid;
  grid-template-rows: 27px 13px;
  align-content: center;
  justify-items: center;
}
.hud-bar button:is([data-ctl="pause"], [data-ctl="settings"]) .hb-face {
  width: 27px;
  height: 27px;
}
.hud-bar button:is([data-ctl="pause"], [data-ctl="settings"]) .hb-label {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  font: 600 11px/13px system-ui, sans-serif;
  letter-spacing: 0;
  color: #f0e6d4;
  text-shadow: 0 1px 2px #000;
}
.hud-bar button:is([data-ctl="pause"], [data-ctl="settings"])::after {
  content: none;
}

/* A phone cannot fit a growing row of faculty IDs beside the controls. The
   count uses the free line beneath Attention instead of pushing Pause away.
   The existing accessible full names and Commonplace Book list stay intact. */
@media (max-width: 719px) {
  .hud-bar .faculties:not([hidden]) {
    position: absolute;
    left: calc(var(--hb-pad-x) + var(--hb-face) + 8px);
    top: 29px;
    height: 12px;
    display: block;
    font: 10px/12px system-ui, sans-serif;
    white-space: nowrap;
    pointer-events: none;
  }
  .hud-bar .faculties .fac-chip { display: none; }
  .hud-bar .faculties:not([hidden])::after {
    content: attr(data-count) ' faculties';
    display: block;
    font: 10px/12px system-ui, sans-serif;
    letter-spacing: 0;
    color: var(--hb-bone);
    text-shadow: 0 1px 1px #000;
  }
}
