/* ==========================================================================
   TELL — stage.css · panel layer, letterbox, observation-layer treatments
   ==========================================================================
   Owns: the 16:9 stage frame, panel crossfades (cut/fade/slow), the cue
   insert card, region vignette + two-pulse highlight (desaturated gold —
   never red/green), the in-place cue spotlight + note callout (v1.2 §16 cue
   framing), shimmer (unreadable heat-haze), dimmed (pre-Gift static
   scramble, "(unseen)"), the Still Portrait hold ring, ghost-replay frames,
   and the tension atmosphere. DOM contract: docs/dom-contract.md.
   ========================================================================== */

/* ==========================================================================
   PAPER DOCKINGS — the overlay lane's shipped look (W4, 2026-08-31)
   ==========================================================================
   The eleven small overlays that dock onto the picture are PAPER OBJECTS
   docked onto it, not dark-glass software chips: a torn note slip pinned
   beside the region, a cut chit, a gauge card, a ruled ledger strip, a card
   tray. Words are runtime type (Courier Prime for labels/stamps, the prose
   face for authored sentences); grounds are plates; hovers are paper lifts
   and graphite underlines. Elected from css/skins/overlay-a.css and folded
   into the base here — the `?skin=overlay-a` candidate file is now historical
   and no longer the source of truth for these surfaces.

   PLATES ARE 9-SLICED, NOT STRETCHED. Every alpha plate in this kit carries a
   wide baked transparent margin, and `background-size: 100% 100%` scales that
   margin with the box — so content lands on the tear or off the paper, and
   worse the further the box is from the plate's aspect. Both plate-bearing
   surfaces (the slip and the tray) use `border-image … fill` with slices
   measured off the files, which pins the margins to a constant size and makes
   the border-width the content safe area BY CONSTRUCTION. Plates too small to
   slice (the chips) take flat stock instead.

   Plate absence degrades gracefully: a paper-toned gradient sits under the
   border-image, clipped to the padding box, and shows through if a plate ever
   404s.

   THE LAWS, honoured:
   1. Blank plates + runtime type — no lettering is drawn by any asset here.
   2. ONE saturated element: --pp-red appears exactly once, as the
      :focus-visible keyline (--ov-focus). Nothing else here is saturated.
   3. Hovers are paper lift / graphite underline. No glow anywhere.
   4. No `order:`, no `*-reverse` — nothing in this lane reorders rows.
   5. Nothing is hidden with display:none on a label span.
   6. Every interactive element restyled here ships a visible :focus-visible.
   7. Transitions ride --pp-t-* tokens only; the one motion (the slip settling)
      ships a reduced-motion companion that lands the END state visibly.
   8. body.bare untouched — nothing here sets display on HUD chrome.
   -------------------------------------------------------------------------- */
:root {
  /* Type ON the picture. The stage ground is dark, so "ink" inverts to the
     paper tone: the TYPED REGISTER is carried by the face (Courier Prime) and
     the tracking, never by the hue. Everything here is paper/graphite range. */
  --ov-ink: rgba(237, 230, 214, 0.88);
  --ov-ink-dim: rgba(237, 230, 214, 0.62);
  --ov-ink-faint: rgba(237, 230, 214, 0.34);
  --ov-rule: rgba(237, 230, 214, 0.30);
  --ov-focus: rgba(142, 47, 42, 0.9);      /* law 2 keyline — the one red */

  /* ---- PLATE GROUNDS, AND THE PATH TRAP THEY AVOID ----------------------
     VERIFIED IN CHROME, 2026-08-31, on the live server: a relative url()
     carried inside a custom property is resolved against the stylesheet that
     USES the variable, not the one that DECLARES it. (Gecko resolves at the
     declaration site instead, so the two engines disagree and neither is safe
     to lean on.) The candidate skin lived in css/skins/ and consumed
     ui-paper.css's `--plate-*` vars, which resolved to /css/assets/ui/… — a
     404, and a 404 on an image layer draws NOTHING rather than degrading.
     Declared AND used in this one file, both engines agree: stage.css lives in
     css/, so `../assets/ui/x` is the correct path from here either way.
     Filenames verified against assets/ui/ rather than guessed. */
  --ov-plate-card:   url('../assets/ui/card-face.webp');      /* 640x420 a  */
  /* THE BLANK TWIN, 2026-08-31 (owner's rule-lock ruling: writing sits ON the
     line, so the ruling has to be a CSS fact the type grid can be built on).
     ledger-spread-blank.webp is the same 1600x1000 canvas with the printed
     ruling erased and everything else — spine, worn edges, both dried-red
     margin rules — untouched, so the `right center / cover` crop below is
     unchanged. The name changed with the file because `--ov-plate-ruled` would
     now be a lie; grep says `--ov-ruled` was its only reader. */
  --ov-plate-page:   url('../assets/ui/ledger-spread-blank.webp'); /*1600x1000*/
  --ov-plate-pins:   url('../assets/ui/pin-sprites.png');     /* 800x400 a  */
  --ov-plate-slip:   url('../assets/ui/note-slip-cut.webp');  /* 700x420 a  */

  /* --plate-note-slip stays in the chain behind it so that the day ui-paper.css
     declares its plates root-relative this file needs one deletion and nothing
     else; the flat paper fill is the last resort. */
  --ov-slip: var(--ov-plate-slip, var(--plate-note-slip,
    linear-gradient(var(--pp-card, #ede6d6), var(--pp-card, #ede6d6))));
  --ov-tray: var(--ov-plate-card);
  --ov-ruled: var(--ov-plate-page);

  /* A brass pin, cropped out of the 5x2 sprite sheet (row 1, col 2). */
  --ov-pin-size: 500% 200%;
  --ov-pin-cell: 25% 0%;

  /* ---- 9-SLICE GEOMETRY, MEASURED OFF THE PLATES ------------------------
     `background-size: 100% 100%` squashes a plate's BAKED TRANSPARENT MARGINS
     along with its paint, so the transparent band scales with the box and eats
     the content. Measured on the shipped files (alpha > 200):

       note-slip-cut.webp  700x420 — paper x[29..671] y[45..372].
         The TOP is torn: the per-column paper edge ramps from y=45 to y=103,
         so rows are only fully opaque from y=103 down. The other three edges
         are STRAIGHT, with 29/29/48px of transparent margin l/r/b. There is
         ONE torn edge, not two.
       card-face.webp      640x420 — paper x[102..538] y[51..366]: margins of
         102/102/51/54px, i.e. a THIRD of the plate's width is empty.

     Sliced instead, the margins and the tear render at a CONSTANT size and the
     border-width becomes the content safe area by construction — content
     cannot reach a torn or transparent edge at any box size. Slices are source
     px, each set 4px inside the measured paper edge so the middle region is
     solid paint. `fill` paints that middle across the padding box. */
  --ov-slip-slice: 104 33 52 33;      /* top tear+margin / sides / bottom     */
  --ov-slip-edge: 24px 9px 13px;      /* rendered safe area: top / sides / bot */
  --ov-card-slice: 55 106 58 106;
  --ov-card-edge: 12px 16px 14px;
}

/* --------------------------------------------------------------------------
   Stage frame — panels are authored 1600×900; the frame keeps a strict 16:9
   box centered in the viewport. The bars around it ARE the letterbox (the
   body's harbor ink). Region coordinates map 1:1 onto this frame.
   -------------------------------------------------------------------------- */
#panel-layer { overflow: hidden; }

/* Start fade-in — a black wash over the whole stage that lifts when a scene
   begins (BEGIN / CONTINUE), so the game fades up from dark instead of cutting. */
.stage-fadein {
  position: absolute;
  inset: 0;
  z-index: 45;
  background: #070d11;
  opacity: 0;
  pointer-events: none;
  transition: opacity 950ms ease;
}
.stage-fadein.block { opacity: 1; transition: none; }
@media (prefers-reduced-motion: reduce) { .stage-fadein { transition: opacity 300ms ease; } }

.stage-frame {
  /* A size container so the letterbox caption's `cqw` resolves against the STAGE.
     Without this there is no container in the ancestor chain and cqw silently
     falls back to the small viewport, so narration did not scale with the stage
     at all and overflowed its band on smaller windows. Bubbles are unaffected —
     they sit inside .comic-frame / .panel-slot, which are their own containers. */
  container-type: inline-size;
  position: absolute;
  inset: 0;
  /* THE FRAME MEETS THE DESK EDGE, not the middle of the window. Centring the
     letterbox slack put half of it ABOVE the frame, where the opaque desk edge
     already stands — a dead black band between the wood's lower lip and the
     art (owner: "no need for the black bar on top of the screen anymore").
     The slack is 100vh − the frame's own height, and the top margin takes
     min(--hb-h, slack): on a 16:9-or-wider window slack is 0 and nothing
     moves; on a taller window the frame rises until its top edge sits exactly
     at the strip's lower lip, and every remaining pixel of slack goes to the
     BOTTOM bar, where there is no furniture. `auto` bottom absorbs it; `auto`
     sides still centre. ui.css's --hb-frame-top (the PAUSED tag's clearance)
     states the same expression and must move with this one. */
  margin: min(var(--hb-h, 54px), calc(100vh - min(100vh, 100vw * 0.5625))) auto auto;
  aspect-ratio: 16 / 9;
  width: min(100%, calc(100vh * 16 / 9), calc(100dvh * 16 / 9));
  max-height: 100%;
  background: var(--hn-ink-deep);
  box-shadow: 0 0 0 1px rgba(30, 78, 86, 0.35);
  overflow: hidden;
}
/* The bare room has no desk edge to meet — the strip is put away below — so
   the frame goes back to the centred letterbox. */
body.bare .stage-frame { margin: auto; }

body.interlude .stage-frame {
  box-shadow: 0 0 0 1px rgba(201, 151, 63, 0.28);
}

/* B18 §13.4 — `@bare`. THE ROOM WITH NO INSTRUMENTS. The scene's own sigil drops
   this class (js/scene-player.js, beside the interlude toggle) and the shell puts
   its furniture away: the HUD bar goes, and with it the loupe it carries; the
   Commonplace Book's handle goes. Nothing is greyed and nothing is announced —
   the instruments are simply not in the room. Each selector carries `body.bare`
   for the one class more than the rule it beats (`.hud-bar`'s own `display:flex`,
   and the codex handle's rule, which is injected from js/codex.js AFTER this
   sheet and so needs the specificity, not the order).
   50-ui-redress W0 — and `!important`, because a candidate-lane sheet loads
   AFTER this one and a plain `html.<lane-class> .hud-bar` would TIE this rule
   on specificity (0,2,1) and win on order: the instruments would come back in
   the room with no instruments. The bare room is not a lane's decision. */
body.bare .hud-bar { display: none !important; }
body.bare #codex-drawer .cx-handle { display: none !important; }

/* --- panel stack: two slots crossfade --------------------------------- */
.panel-stack { position: absolute; inset: 0; }

/* --------------------------------------------------------------------------
   Comic reader — 4:5 beats become panels on a vertical page. The page (an
   inner layer) is translated up as each new panel arrives, so the reader moves
   down the page like a comic; panels already read stay above, dimmed. Wide
   splashes bypass this and fill the frame (they clear the page).
   -------------------------------------------------------------------------- */
.panel-stack.comic {
  overflow: hidden;
  /* THE BOARD. Not paper — the developer looked at a cream page and said a book
     doesn't make sense here, and they were right: a warm sheet next to paintings
     built out of near-blacks fights them. This is the same "mounted object on a
     ground" grammar in the palette the game already owns. */
  background:
    radial-gradient(118% 92% at 52% 42%, #16282b 0%, #0e1a1c 62%, #080f13 100%);
}
.comic-page {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  display: flex;
  flex-direction: row;              /* horizontal strip — reads left to right */
  align-items: center;
  gap: clamp(10px, 1.6vw, 26px);
  padding: 0 42%;                   /* room to seat the first and scroll the last */
  will-change: transform;
}
.comic-frame {
  flex: none;
  position: relative;
  aspect-ratio: 4 / 5;               /* height is set inline; width derives */
  background: #05090c;
  border: 1px solid rgba(190, 220, 215, 0.20);   /* hairline keyline, not a frame */
  box-shadow: 0 22px 52px rgba(0, 0, 0, 0.62), 0 2px 10px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  opacity: 1;
  transform: none;
  transition: opacity 520ms ease, transform 520ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* The authored transition, same contract as .panel-slot above: the script says
   cut / fade / slow and a tall beat has to honour it too. Without these the
   frame took the 520ms default whatever the beat asked for, which is why a
   hard cut and a slow dissolve looked identical on every comic panel. */
.comic-frame.t-cut   { transition-duration: var(--t-cut); }
.comic-frame.t-fade  { transition-duration: var(--t-fade); }
/* t-xfade on the FRAME now covers one case only: an `xfade` with no predecessor
   to replace (a B-half opening a comic page). The pair case is the rule below. */
.comic-frame.t-xfade { transition-duration: var(--t-xfade); }
.comic-frame.t-slow  { transition-duration: var(--t-slow); }
.comic-frame.entering { opacity: 0; transform: translateX(42px) scale(0.985); }
.comic-frame.read { opacity: 0.42; }          /* panels you've already read recede */
.comic-frame .comic-art {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;               /* decoded frame aspect preserves the whole authored plate */
  display: block;
}
/* THE A/B DISSOLVE HAPPENS INSIDE ONE FRAME, not between two of them (see
   comicXfade in js/scene-player.js). B is appended over A in A's own seat and
   fades up; A is removed once B is fully opaque, so the pair reads as a single
   picture changing — which is the whole authored meaning of the token. Only the
   arriving image carries `.xf`, so nothing else on the board can animate on it,
   and at reduce-motion / fast-forward the classes are never applied at all. */
.comic-frame .comic-art.xf { transition: opacity var(--t-xfade) ease; }
.comic-frame .comic-art.xf-in { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .comic-frame { transition: opacity 220ms ease; }
  .comic-frame.entering { transform: none; }
  /* The A/B dissolve is CONTENT (the change is the cue), so it still happens —
     it just happens at the same shortened rate this block gives the frame. The
     player's own Motion setting is the harder switch: at that one the swap is
     instant, because scene-player never applies these classes at all. */
  .comic-frame .comic-art.xf { transition-duration: 220ms; }
}

.panel-slot {
  position: absolute;
  inset: 0;
  opacity: 1;
  transition: opacity var(--t-fade) ease;
}
.panel-slot.entering { opacity: 0; }
.panel-slot.t-cut   { transition-duration: var(--t-cut); }
.panel-slot.t-fade  { transition-duration: var(--t-fade); }
/* xfade is a TRUE cross-dissolve on this path: the arriving slot fades up over the
   outgoing one, which scene-player.js holds in the DOM for the full duration before
   removing it. That only reads as one picture changing when the two plates are
   pixel-locked (B chained off A's job id) — see docs/tellscript-grammar.md §22.11. */
.panel-slot.t-xfade { transition-duration: var(--t-xfade); }
.panel-slot.t-slow  { transition-duration: var(--t-slow); }

.panel-slot img,
.panel-slot video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Blurred-fill backdrop: a cover-fitted, blurred, darkened copy so a 4:5
   "comic panel" beat shot fills the 16:9 frame with no dead letterbox bars.
   A true 16:9 plate's sharp art (below) covers it completely. */
.panel-fill {
  object-fit: cover;
  filter: blur(26px) brightness(0.5) saturate(0.9);
  transform: scale(1.12);           /* hide the blur's soft edges */
  z-index: 0;
}

/* The real image: whole, uncropped, centered. */
.panel-art {
  object-fit: contain;
  z-index: 1;
}
/* ...except when the plate is only *almost* the frame's aspect. Every wide plate
   in the game is 1376x768 = 1.7917, not 16:9 = 1.7778 — a 0.78% miss, which
   `contain` honours by leaving a 2.79px gap per edge at 1280x720 where the
   blurred half-brightness .panel-fill shows through. Ken Burns hides it on about
   half the splashes and not at all when the player has Motion reduced, so it is
   a permanent smear band on plates that are, to the eye, the right shape. Below
   the tolerance set in splashPanel, cover instead: it crops ~0.4% off the sides,
   which is invisible, where the band is not. */
.panel-slot.snap .panel-art { object-fit: cover; }
.panel-slot.snap .panel-fill { display: none; }   /* nothing left for it to fill */

/* A motion loop rides exactly where its still sits. */
.panel-clip {
  object-fit: contain;
  z-index: 2;
}

/* Ken Burns — a slow, barely-there drift so a held shot keeps breathing.
   Applied to the sharp art + its blurred fill so they move together. The two
   variants alternate per panel so cuts don't all push the same direction.
   16 s, then it settles (no infinite bounce — this is atmosphere, not a GIF). */
.panel-slot.kb .panel-art,
.panel-slot.kb .panel-fill { will-change: transform; }
.panel-slot.kb-a .panel-art { animation: kb-a 16s ease-out both; }
.panel-slot.kb-a .panel-fill { animation: kb-a 16s ease-out both; }
.panel-slot.kb-b .panel-art { animation: kb-b 16s ease-out both; }
.panel-slot.kb-b .panel-fill { animation: kb-b 16s ease-out both; }
/* the fill is already scaled 1.12; its keyframes keep that baseline */
.panel-slot.kb-a .panel-fill { animation-name: kb-a-fill; }
.panel-slot.kb-b .panel-fill { animation-name: kb-b-fill; }

@keyframes kb-a {
  from { transform: scale(1.0)  translate(0, 0); }
  to   { transform: scale(1.06) translate(-1.4%, -1.0%); }
}
@keyframes kb-b {
  from { transform: scale(1.06) translate(1.2%, 1.0%); }
  to   { transform: scale(1.0)  translate(0, 0); }
}
@keyframes kb-a-fill {
  from { transform: scale(1.12) translate(0, 0); }
  to   { transform: scale(1.18) translate(-1.2%, -0.8%); }
}
@keyframes kb-b-fill {
  from { transform: scale(1.18) translate(1.0%, 0.8%); }
  to   { transform: scale(1.12) translate(0, 0); }
}

/* --------------------------------------------------------------------------
   Tension atmosphere — 20-mechanics: tension is "visible as atmosphere",
   never a number. Pressure renders as a storm gathering at the frame edges.
   Driven by --tension (0..1) set on #stage by the HUD.
   -------------------------------------------------------------------------- */
.tension-atmos {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  opacity: calc(var(--tension) * 0.85);
  transition: opacity 900ms ease;
  background:
    radial-gradient(140% 100% at 50% 50%,
      transparent 52%,
      rgba(7, 14, 18, 0.55) 82%,
      rgba(7, 14, 18, 0.9) 100%),
    linear-gradient(180deg, rgba(30, 78, 86, 0.14), transparent 30%);
}

/* --------------------------------------------------------------------------
   FX layer — shimmer / dimmed overlays live here, positioned in % of the
   frame (region [x,y,w,h] in 1600×900 maps directly).
   -------------------------------------------------------------------------- */
.fx-layer { position: absolute; inset: 0; pointer-events: none; z-index: 4; }

.fx-region { position: absolute; }

/* --- SHIMMER — ability missing: a faint heat-haze; the signal exists,
       Sam can't parse it. Silver only, no meaning, no card. ------------- */
.fx-shimmer {
  border-radius: 50% / 42%;
  background:
    repeating-linear-gradient(115deg,
      rgba(201, 210, 212, 0.10) 0 3px,
      rgba(201, 210, 212, 0.02) 3px 7px);
  box-shadow:
    inset 0 0 22px rgba(201, 210, 212, 0.18),
    0 0 30px 6px rgba(201, 210, 212, 0.08);
  backdrop-filter: blur(2.5px) contrast(1.08);
  -webkit-backdrop-filter: blur(2.5px) contrast(1.08);
  animation: shimmer-haze 2.1s ease-in-out infinite;
}

@keyframes shimmer-haze {
  0%, 100% { transform: translateY(0) skewX(0deg);   opacity: 0.75; }
  50%      { transform: translateY(-2px) skewX(1.2deg); opacity: 1; }
}

/* --- FAINT (v1.4) — sub-threshold: Sam SAW it and did not decode it.
       Read against shimmer, which it must never be confused with:
         shimmer  a heat-haze that MOVES  — an unparseable thing, no offer
         faint    a still aperture ring   — a legible thing, unpaid for
       So: no haze, no blur, no skew. A thin gold-silver ring that opens once
       and holds, because the observation is sharp — what is missing is not
       clarity but attention. Costs no stage dim and no duck: a glance. ---- */
.fx-faint {
  border-radius: 44% / 48%;
  border: 1px solid rgba(185, 166, 124, 0.55);
  box-shadow:
    inset 0 0 16px rgba(185, 166, 124, 0.10),
    0 0 18px 2px rgba(10, 20, 24, 0.5);
  animation: faint-open 420ms ease-out;
}

@keyframes faint-open {
  0%   { transform: scale(1.06); opacity: 0; }
  100% { transform: scale(1);    opacity: 1; }
}

/* The dock: who / where / on which words — and nothing else. Never the cue
   name, never a note, never polarity. The player has not paid for meaning. */
/* The dock hangs below its region by default and flips ABOVE it (`.above`,
   bottom-anchored) for regions low in the plate — feet and legs, which is most
   of what Low Truth prices. `.right` right-anchors the label for regions on the
   right-hand side. scene-player picks the variant from the region numbers
   alone; nothing here is measured at run time.
   The entrance animates OPACITY ONLY: a transform keyframe outranks inline
   style for its whole duration, and these two variants live in `transform`. */
.faint-dock {
  position: absolute;
  transform: translateY(7px);
  max-width: 26ch;
  font-family: var(--font-ui);
  text-shadow: 0 1px 3px var(--hn-ink-deep);
  animation: faint-dock-in 420ms ease-out;
}
.faint-dock.above { transform: translateY(-7px); }
.faint-dock.right { text-align: right; }

@keyframes faint-dock-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.fd-head { font-size: 0.74rem; letter-spacing: 0.05em; }
.fd-subject { color: var(--hn-white); }
.fd-where { margin-left: 0.4rem; color: var(--hn-gold); }
.fd-on {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.72rem;
  color: var(--hn-rain);
  margin-top: 0.1rem;
}
.fd-kicker { font-family: var(--font-ui); font-style: normal; opacity: 0.7; margin-right: 0.25rem; }
.fd-tag {
  font-size: 0.56rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--hn-gold);
  opacity: 0.75;
  margin-top: 0.22rem;
}

/* --- DIMMED — pre-Gift dramatic irony: 30% static-scramble, tagged
       "(unseen)". The player reads it; Sam cannot. --------------------- */
.fx-dimmed {
  opacity: 0.3;
  border-radius: 4px;
  background:
    repeating-linear-gradient(0deg,
      rgba(201, 210, 212, 0.55) 0 1px,
      transparent 1px 3px),
    repeating-linear-gradient(90deg,
      rgba(92, 107, 120, 0.4) 0 2px,
      transparent 2px 5px);
  mix-blend-mode: screen;
  animation: static-scramble 340ms steps(3) infinite;
}

@keyframes static-scramble {
  0%   { background-position: 0 0, 0 0; }
  50%  { background-position: 0 2px, 3px 0; }
  100% { background-position: 0 -1px, -2px 0; }
}

/* Tag under a shimmer/dimmed region — shape+label, never color-coded */
.fx-tag {
  position: absolute;
  width: max-content;
  font-family: var(--font-ui);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  color: var(--hn-silver);
  opacity: 0.8;
  font-style: italic;
  text-shadow: 0 1px 3px var(--hn-ink-deep);
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.45;
}

/* Linger trace (realistic cue timing) — after a SUSTAINED posture's beat ends,
   a faint outline stays on its region in the wide shot: in the room those arms
   would still be crossed. Quiet, non-pulsing, desaturated silver (never a
   meaning color); cleared the instant the panel changes. */
.fx-linger {
  position: absolute;
  border: 1px solid rgba(201, 210, 212, 0.22);
  border-radius: 40% / 46%;
  box-shadow: inset 0 0 18px rgba(201, 210, 212, 0.07);
  opacity: 0;
  transition: opacity 700ms ease;
  pointer-events: none;
}
.fx-linger.in { opacity: 1; }

/* --------------------------------------------------------------------------
   Cue insert — the Gift firing. A 4:5 portrait card inset over the scene
   (design/30-assets.md), Gift Silver, framed in the desaturated-gold
   two-pulse. Slides in over a dimmed stage.
   -------------------------------------------------------------------------- */
.cue-backdrop {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: rgba(7, 14, 18, 0.55);
  opacity: 0;
  will-change: opacity;
  transition: opacity 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.cue-backdrop.in { opacity: 1; }

/* The card eases up and settles rather than snapping in — enter and leave are
   both smooth so the Gift beat flows out of the scene and back into it. */
.cue-insert {
  position: absolute;
  z-index: 6;
  top: 50%;
  left: 50%;
  height: 86%;
  aspect-ratio: 4 / 5;
  transform: translate(-50%, -44%) scale(0.965);
  opacity: 0;
  will-change: opacity, transform;
  transition: opacity 420ms cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 480ms cubic-bezier(0.22, 0.61, 0.36, 1);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
  background: var(--hn-ink-deep);
}
.cue-insert.in { opacity: 1; transform: translate(-50%, -50%) scale(1); }

/* The narration bands cover 12.5% at each edge. Keep the observation's label
   and card inside their clear middle area on desktop; phone cards have their
   own layout below. */
@media (min-width:641px) {
  .stage-frame.letterboxed .cue-insert { height:70%; }
}

.cue-insert img,
.cue-insert video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(0.25) contrast(1.05); /* the Gift is colorless */
}

/* Two-pulse gold trace around the insert (1.6s) — the highlight beat.
   Desaturated gold: never red, never green — color must not smuggle meaning. */
.cue-insert::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 1px solid rgba(185, 166, 124, 0);
  box-shadow: 0 0 0 rgba(185, 166, 124, 0);
}
.cue-insert.pulse::after {
  animation: cue-two-pulse 1.6s ease-out 1;
}

@keyframes cue-two-pulse {
  0%   { border-color: rgba(185, 166, 124, 0);   box-shadow: 0 0 0 0 rgba(185, 166, 124, 0); }
  12%  { border-color: rgba(185, 166, 124, 0.9); box-shadow: 0 0 26px 2px rgba(185, 166, 124, 0.35); }
  40%  { border-color: rgba(185, 166, 124, 0.15); box-shadow: 0 0 8px 0 rgba(185, 166, 124, 0.08); }
  58%  { border-color: rgba(185, 166, 124, 0.85); box-shadow: 0 0 22px 2px rgba(185, 166, 124, 0.3); }
  100% { border-color: rgba(185, 166, 124, 0.25); box-shadow: 0 0 6px 0 rgba(185, 166, 124, 0.06); }
}

/* ---- THE TWO-PANE CARD (v1.8 §22.10 — the `absent` cue) -------------------
   An absence has no picture of its own. Two panes at ONE scale: the docked
   baseline's crop of the cue's box on the left, the live crop on the right.
   NO CAPTION, NO ARROW, NO LABEL ON EITHER PANE, no "before/after", no
   annotation, and — the standing law — no red and no green anywhere, nothing
   that marks one half as the wrong one. The hairline between them is the only
   mark, and it is a seam, not a pointer. Same colourless treatment as every
   other cue render: the Gift does not editorialise in hue. */
.absent-panes {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: rgba(185, 166, 124, 0.28);   /* the seam, one hairline wide */
  overflow: hidden;
}
.absent-panes .ap-pane {
  position: relative;
  overflow: hidden;
  background: var(--hn-ink-deep);
  display: flex;
  align-items: center;
  justify-content: center;
}
.absent-panes .ap-pane img {
  filter: saturate(0.25) contrast(1.05);
  display: block;
  /* `fill`, never `cover`: the crop is the same PROPORTIONAL per-axis 1600x900
     mapping regionCss and tools/cue-zoom.py use, and `cover` would re-crop the
     crop — the preserveAspectRatio="slice" bug the cue vignette already paid
     for once. */
  object-fit: fill;
}
/* Unregioned fallback: two whole pictures, still at one scale. */
.absent-panes.whole .ap-pane img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The in-place path docks the pair at the TOP of the frame — clear of the
   dialogue box, which stays lit through an in-place beat, and clear of the
   region the spotlight is on. Region space is the panel box, so percentages
   resolve against painted pixels on any plate shape. */
.absent-panes.docked {
  inset: auto;
  top: 4%;
  left: 50%;
  transform: translateX(-50%) translateY(-6px);
  width: 46%;
  aspect-ratio: 2 / 1;
  z-index: 7;
  opacity: 0;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.55);
  transition: opacity 360ms cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 360ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.absent-panes.docked.in { opacity: 1; transform: translateX(-50%) translateY(0); }
.rm .absent-panes.docked { transition: none; transform: translateX(-50%); }

/* Region vignette (real 16:9 cue art only): SVG overlay darkens everything
   outside the authored region; the ellipse border carries the pulse. */
.cue-vignette {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* One ring grammar for both render paths: the insert card's vignette and the
   in-place spotlight draw the SAME ellipse in the same desaturated gold.
   (Without .cue-spotlight here the ellipse falls back to SVG defaults — black
   fill, no stroke — and blacks out the very region it is meant to light.) */
.cue-vignette .vignette-ring,
.cue-spotlight .vignette-ring {
  fill: none;
  stroke: var(--hn-gold);
  stroke-width: 3;
  opacity: 0;
}
.cue-insert.pulse .vignette-ring {
  animation: ring-two-pulse 1.6s ease-out 1 forwards;
}

@keyframes ring-two-pulse {
  0%   { opacity: 0; }
  12%  { opacity: 0.95; }
  40%  { opacity: 0.2; }
  58%  { opacity: 0.85; }
  100% { opacity: 0.4; }
}

/* --------------------------------------------------------------------------
   In-place cue (grammar v1.2 §16) — the second render path. The establishing
   shot STAYS lit and so does the dialogue box; everything OUTSIDE the authored
   region darkens under a soft-edged spotlight (SVG inverse mask, same
   technique as .cue-vignette), the SAME two-pulse ring lands on the region
   where it sits in the wide shot, and the chip + meaning-free note float
   beside it. No backdrop, no card, no new art — it rides the panel on stage.
   Region math is identical to regionCss()/fxRegionEl: [x,y,w,h] in 1600×900
   maps 1:1 onto the letterboxed .stage-frame.
   -------------------------------------------------------------------------- */
.cue-spotlight {
  position: absolute;
  inset: 0;
  z-index: 5;                 /* over panel + tension atmos, under chip/note */
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.cue-spotlight.in { opacity: 1; }

/* The ring is literally the insert path's ring — same class, same stroke, same
   keyframes (see .cue-vignette .vignette-ring above); only the parent differs,
   so the highlight grammar cannot drift between the two paths. */
.cue-spotlight.pulse .vignette-ring {
  animation: ring-two-pulse 1.6s ease-out 1 forwards;
}

/* v1.14 §27 — A15 THE OPEN BOOK: the disclosure menu's MULTI-HOLE spotlight.
   Same mask, same scrim, same ring — N of them, because the menu offers two or
   three of Sam's own tells at once and each row is paired to its own lit region
   by `data-cue`.

   THE RINGS DO NOT PULSE. They rest at the two-pulse's own resting opacity, so
   the geometry is identical to every other spotlight in the game and only the
   emphasis differs: nothing is being pointed OUT here. The regions are a LIST,
   and a list that blinked would be the chrome ranking it.

   HOVER/FOCUS RAISES THE PARTNER, and that is the entire interaction: the same
   stroke, more of it. No glow, no colour change, no red and no green, and
   nothing at all on the row itself beyond the paper-lift the menu already has
   (50-ui-redress §Motion). The chrome may never say which tell is the good one
   to give. */
.cue-spotlight.disclosure .vignette-ring {
  opacity: 0.4;
  transition: opacity var(--t-fade) ease, stroke-width var(--t-fade) ease;
}
.cue-spotlight.disclosure .vignette-ring.raised {
  opacity: 0.95;
  stroke-width: 4;
}

/* --------------------------------------------------------------------------
   THE E6 FLICKER — design/beats/ch28-closing-time.md §7.6, and it is the ONLY
   sanctioned uninvited cue-glow in the game.

   Ending 6 is the one where the borrowing does not stop. The last menu of the
   last scene comes up, and at the edge of the frame the light the Gift used to
   arrive in turns over once, by itself, for nobody — the faculty the chapter
   spent itself retiring, arriving unasked. It is not a cue: it docks nothing,
   opens nothing, reads nothing, and js/scene-player.js fires it once per scene
   entry and never again.

   SO IT IS THE CUE GRAMMAR'S COLOUR AND NONE OF ITS GEOMETRY. --hn-gold is the
   ring's own stroke (see .cue-vignette .vignette-ring above) because a NEW
   colour would be a new thing rather than a familiar one returning — the whole
   beat is recognition. But there is no ring, no ellipse, no region and nothing
   to point at: an inset bloom at the frame's edge, peaking at 0.22 and gone.
   Something in the corner of the eye, which is where this always lived.

   Reduced motion kills it twice — the module refuses to mount it under
   settings.reduceMotion, and this query is the floor under the OS setting for
   any path that ever reaches the element anyway.
   -------------------------------------------------------------------------- */
.e6-flicker {
  position: absolute;
  inset: 0;
  z-index: 6;                 /* the spotlight's layer: over the panel, under chip/note */
  pointer-events: none;
  opacity: 0;
  box-shadow: inset 0 0 clamp(18px, 3.4vw, 46px) clamp(4px, 0.7vw, 10px) var(--hn-gold);
  animation: e6-flicker 2s ease-in-out 1 forwards;
}

@keyframes e6-flicker {
  0% { opacity: 0; }
  38% { opacity: 0.22; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .e6-flicker { animation: none; opacity: 0; }
}

/* Chip dock — the weather chip parked at the region's lower-left corner (upper
   corner when the region sits low in the frame). The chip itself is unchanged:
   glyph + pips + word, never color alone. */
.cue-chip-dock {
  position: absolute;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  /* 10px of travel down the axis the dock already flows on, clearing the note
     slip that the engine anchors to the same region — see the CLEARANCE note
     under .cue-chip-dock's layout block below. */
  transform: translateY(10px);
  transition: opacity var(--t-fade) ease;
}
.cue-chip-dock.in { opacity: 1; }
.cue-chip-dock.above { transform: translateY(calc(-100% - 10px)); }
.cue-chip-dock .weather-chip { position: static; }

/* Note callout — THE NOTE SLIP. The meaning-free note is a torn slip pinned
   beside the region and tied to it by a hairline connector. States signal,
   never meaning; no grading language ever reaches this box. Signal, never
   meaning: every existing state channel (side, production, silhouette) is
   re-expressed on the paper rather than replaced. */
.cue-note {
  position: absolute;
  z-index: 6;
  transform: translateY(-50%);
  pointer-events: none;
  box-sizing: border-box;
  /* THE SAFE AREA IS THE BORDER. 24px of tear at the top, 13px of margin at
     the bottom, 9px at the sides — constant at any note length — and the
     padding below is breathing room ON the paper, not clearance from it. */
  padding: 0.45rem 0.75rem 0.55rem;
  /* The ground if the plate is ever missing or 404s: a paper rectangle inside
     the safe area, painted UNDER the border-image and hidden the moment it
     loads. Graceful absence survives the slicing. */
  background: linear-gradient(var(--pp-card, #ede6d6), var(--pp-card, #ede6d6)) padding-box;
  border-style: solid;
  border-width: var(--ov-slip-edge);
  border-color: transparent;              /* the box goes; the slip is the box */
  border-image: var(--ov-slip) var(--ov-slip-slice) fill stretch;
  border-radius: 0;
  box-shadow: none;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.cue-note.in { opacity: 1; }

/* Floating slip: it hangs off the region, so it takes the drop-shadow FILTER
   (respects the plate's torn alpha; a box-shadow would draw a rectangle behind
   a torn edge) and one degree of settle. */
.cue-note:not(.on-card) {
  transform: translateY(-50%) rotate(-0.5deg) translateX(0);
  filter: drop-shadow(0 3px 7px rgba(0, 0, 0, 0.52));
  transition: opacity var(--t-fade) ease,
              transform var(--pp-t-settle) var(--pp-ease-paper);
}
/* Pre-`in` is where it settles FROM. `.in` is the engine's own class and the
   only thing that moves — no animation, nothing gated on one. */
.cue-note:not(.on-card):not(.in) {
  transform: translateY(calc(-50% + 5px)) rotate(-1.7deg);
}
.cue-note.side-left:not(.on-card) { transform: translateY(-50%) rotate(0.5deg); }
.cue-note.side-left:not(.on-card):not(.in) {
  transform: translateY(calc(-50% + 5px)) rotate(1.7deg);
}

/* The pin. One brass head at the slip's high corner, cropped from the sprite
   sheet — decorative only, and it carries no state. */
.cue-note:not(.on-card)::after {
  content: '';
  position: absolute;
  /* An abspos child is laid out against the PADDING box, so these offsets are
     measured from inside the safe area. `top: -7px` lifts the head back INTO
     the sliced top edge so it straddles the tear — half on the ragged edge,
     half on the paper, the way a pin actually goes through a slip. */
  top: -7px;
  right: 6px;
  left: auto;
  width: 16px;
  height: 16px;
  pointer-events: none;
  background: var(--ov-plate-pins) no-repeat;
  background-size: var(--ov-pin-size);
  background-position: var(--ov-pin-cell);
}

/* THE PIN SITS AT THE CORNER AWAY FROM THE REGION. A `side-right` callout has
   the region on its LEFT — that is the edge carrying the connector and the
   inset pencil rule — so the pin takes the top-RIGHT corner and the two stop
   competing for one corner. `side-left` mirrors it. */
.cue-note.side-left:not(.on-card)::after { right: auto; left: 6px; }

/* THE GUTTER IS WHAT MAKES IT SAFE AT ANY KICKER LENGTH. Moving the head is
   not a fix on its own — a long subject, a cue name, an A10 suffix and an A13
   interior word all land in the same wrapping flex row, so any corner can be
   reached by text eventually. Reserving the pin's band as padding on the
   kicker means the row's content box STOPS before the head, on every wrapped
   line, whatever the words are. 26px against a head that occupies the outer
   22px leaves 4px of daylight.
   Only the kicker needs it: the head ends 9px into the padding box, well
   inside the kicker's own line, so .cue-note-text never reaches it. */
.cue-note:not(.on-card) .cue-note-kicker { padding-right: 26px; }
.cue-note.side-left:not(.on-card) .cue-note-kicker {
  padding-right: 0;
  padding-left: 26px;
}

/* THE SIDE CHANNEL survives, moved off the border and onto the paper. The
   border now carries the plate, and a `border-left: 1px` here would have
   collapsed the left safe area to 1px and squeezed the slice into it — so the
   region-facing rule is drawn as an inset line at the padding edge instead. An
   inset shadow paints inside the padding box, which puts the pencil rule ON
   the paper just inside the torn edge, where a ruled margin belongs. */
.cue-note.side-right { box-shadow: inset 1px 0 0 0 var(--pp-rule, rgba(43, 58, 85, 0.28)); }
.cue-note.side-left  { box-shadow: inset -1px 0 0 0 var(--pp-rule, rgba(43, 58, 85, 0.28)); }

/* The card path's docked note — laid across the top of the insert rather than
   floated beside a region. Transcribed from the inline block js/scene-player.js
   used to stamp on it (50-ui-redress W0); the values are unchanged. `z-index:
   auto` is load-bearing: it keeps the note UNDER the card's pulse border and the
   weather chip, and the bottom hairline reuses the paper ruling, so no new
   palette enters here.

   NO SLICE HERE, and no plate: the docked note is a strip TAPED ACROSS the
   card's top edge, and note-slip's one torn edge is exactly the edge that gets
   clipped flush against the card. A strip is not a torn slip, so it takes flat
   stock and keeps the single bottom hairline — which also means the docked path
   cannot inherit the tear-collision at all. `.on-card` notes carry no `side-*`
   class (js/scene-player.js:3691), so the inset side rules never reach this one,
   and it takes a real box-shadow because it is a full rectangle with nothing
   torn hanging off it. */
.cue-note.on-card {
  left: 0;
  right: 0;
  top: 0;
  bottom: auto;
  transform: none;
  max-width: none;
  z-index: auto;
  border-image: none;
  border-width: 0 0 1px;
  border-bottom-color: var(--pp-rule, rgba(43, 58, 85, 0.28));
  border-radius: 0;
  background: linear-gradient(var(--pp-card, #ede6d6), var(--pp-card, #ede6d6)) padding-box;
  padding: 0.6rem 1rem 0.7rem;
  filter: none;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.42);
}

/* Type on paper: ink, now literally. The kicker keeps its uppercase — the
   typed register is carried by the face and the tracking, not by case. */
.cue-note .cue-note-kicker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em;
  font-family: var(--font-type);
  font-size: max(12px, 0.75rem);
  line-height: 1.35;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}
.cue-note .cn-subject { color: var(--pp-type, #22242b); }
.cue-note .cn-cue { color: var(--pp-type-dim, rgba(34, 36, 43, 0.62)); }

.cue-note .cue-note-text {
  margin: 0.45rem 0 0;
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--pp-type, #22242b);
  text-shadow: none;                       /* ink on paper needs no shade pool */
}

/* Hairline connector, region edge → callout. A PENCIL RULE, so it reads as
   drawn on the same page as the slip; 1px, no arrowhead: it points, it does not
   assert. Not the red thread — the one-red budget is spent on :focus-visible. */
.cue-note-link {
  position: absolute;
  z-index: 6;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg, var(--ov-rule), rgba(237, 230, 214, 0.08));
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.cue-note-link.in { opacity: 1; }

/* --------------------------------------------------------------------------
   Limbic Weather (A02) — barometric edge-vignette + color-temperature shift
   over the subject's region (full-panel when unauthored), rendered while a
   visible cue-event carries polarity and the player holds A02. Polarity and
   intensity ONLY; hard-forbidden from carrying content. Comfort = settled
   warm-silver, discomfort = cold pressure — the Gift Silver family throughout,
   NEVER red/green; the chip's glyph+pips carry the coding so color is never
   the only channel. design/abilities/02-limbic-weather.md.
   -------------------------------------------------------------------------- */
.fx-weather {
  position: absolute;
  z-index: 5;                 /* over the cue backdrop, under the insert (6) */
  pointer-events: none;
  border-radius: 50% / 44%;
  opacity: 0;
  transition: opacity 700ms ease;
  /* silver fallback; polarity retints below */
  --w-tint: 201, 210, 212;
  --w-alpha: 0.3;
  --w-spread: 20px;
}
.fx-weather.full { inset: 0; border-radius: 0; }
.fx-weather.in { opacity: 1; }

/* intensity — five-step gradient is polarity × weight; weight steps here */
.fx-weather[data-weight="1"] { --w-alpha: 0.28; --w-spread: 18px; }
.fx-weather[data-weight="2"] { --w-alpha: 0.44; --w-spread: 30px; }
.fx-weather[data-weight="3"] { --w-alpha: 0.6;  --w-spread: 44px; }

/* color temperature — silver warmed toward gold / cooled toward teal */
.fx-weather[data-polarity="comfort"]    { --w-tint: 205, 196, 170; } /* settled warm-silver */
.fx-weather[data-polarity="discomfort"] { --w-tint: 122, 156, 166; } /* cold pressure */

.fx-weather::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow:
    inset 0 0 var(--w-spread) calc(var(--w-spread) / 3) rgba(var(--w-tint), var(--w-alpha)),
    0 0 calc(var(--w-spread) * 1.4) 4px rgba(var(--w-tint), calc(var(--w-alpha) * 0.5));
}

/* drift archetypes — texture of the signal, never its meaning */
@keyframes weather-drift {
  0%, 100% { transform: translateX(0) scale(1); }
  50%      { transform: translateX(1.2%) scale(1.012); }
}
@keyframes weather-compress {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(0.984); }
}
.fx-weather[data-front="drift"]::before  { animation: weather-drift 9s ease-in-out infinite; }
.fx-weather[data-front="flight"]::before { animation: weather-drift 7s ease-in-out infinite; } /* cool drift toward exits */
.fx-weather[data-front="fight"]::before  { animation: weather-compress 5.5s ease-in-out infinite; } /* warm compression */
.fx-weather[data-front="freeze"]::before { animation: none; } /* pressure-drop stillness */
.fx-weather[data-front="freeze"] {
  backdrop-filter: saturate(0.72) brightness(0.96); /* held-frame desaturation */
  -webkit-backdrop-filter: saturate(0.72) brightness(0.96);
}

/* Weather chip — THE SLIP, SMALL. Docked on the cue insert. Glyph (comfort =
   soft round ring / discomfort = angular hatched, the ledger's shape grammar)
   + intensity pips + the polarity word: triple-channel, colorblind-safe by
   construction, preserved element for element. Only the ground and the mark
   colour move: glass pill -> paper chit, silver -> typewriter ink.

   THE CHIP TAKES NO PLATE. A chip is ~20px tall, and the slip's safe area
   alone is 37px — there is no box small enough to slice, and any plate
   stretched to chip size is its own baked margins squashed into noise. So the
   chip is a CUT CHIT: flat stock, one printed edge, a contact shadow. The
   paper identity lives in the objects big enough to carry it. */
.weather-chip {
  position: absolute;
  left: 5%;
  bottom: 3.5%;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  font-family: var(--font-type);
  font-size: 0.55rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--pp-type, #22242b);
  background: var(--pp-card, #ede6d6);
  border: 1px solid var(--pp-rule, rgba(43, 58, 85, 0.28));
  border-radius: 0;
  padding: 0.3rem 0.6rem;
  transform: rotate(-1deg);
  box-shadow: var(--pp-shadow-pinned, 0 2px 5px rgba(0, 0, 0, 0.5));
}
.weather-chip .w-glyph { display: inline-block; width: 9px; height: 9px; flex: none; }
/* comfort — soft round ring, inked */
.weather-chip.pol-comfort .w-glyph {
  border: 2px solid var(--pp-type, #22242b);
  border-radius: 50%;
}
/* discomfort — angular, hatched, inked. Shape + pattern both survive. */
.weather-chip.pol-discomfort .w-glyph {
  border: 2px solid var(--pp-type, #22242b);
  transform: rotate(45deg) scale(0.9);
  background: repeating-linear-gradient(45deg,
    var(--pp-type, #22242b) 0 1px, transparent 1px 3px);
}
.weather-chip .w-pips { display: inline-flex; gap: 3px; }
.weather-chip .w-pip {
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  border: 1px solid var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  background: transparent;
}
.weather-chip .w-pip.lit { background: var(--pp-type, #22242b); }
.weather-chip .w-word {
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  letter-spacing: 0.14em;
}

/* --------------------------------------------------------------------------
   The Soothing Hand (A04) — warm-silver pacifier halo + trigger-timestamp
   echo. The glow marks the pacifier FAMILY (load, full stop — never guilt);
   the echo re-presents the anchored stimulus quote: which sentence made the
   hand move. Warm-silver — silver family warmed toward gold, distinct from
   the weather layer's cold pressure, never red/green.
   design/abilities/04-soothing-hand.md §UI Treatment.
   -------------------------------------------------------------------------- */
.fx-soothe {
  --soothe: 222, 211, 184;      /* warm-silver — the soothing family's one color */
  position: absolute;
  z-index: 5;                   /* over backdrop + weather, under the insert (6) */
  pointer-events: none;
  border-radius: 50% / 44%;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.fx-soothe.full { inset: 0; border-radius: 0; }
.fx-soothe.in { opacity: 1; }
.fx-soothe::before {
  content: '';
  position: absolute;
  inset: -6%;
  border-radius: inherit;
  box-shadow:
    inset 0 0 34px 8px rgba(222, 211, 184, 0.3),
    0 0 30px 10px rgba(222, 211, 184, 0.2);
  animation: soothe-breathe 4.6s ease-in-out infinite; /* self-comfort has a rhythm */
}
@keyframes soothe-breathe {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}

/* The highlight's soothing variant: same two-pulse grammar, warm-silver. */
.cue-insert.soothe.pulse::after { animation-name: soothe-two-pulse; }
@keyframes soothe-two-pulse {
  0%   { border-color: rgba(222, 211, 184, 0);    box-shadow: 0 0 0 0 rgba(222, 211, 184, 0); }
  12%  { border-color: rgba(222, 211, 184, 0.9);  box-shadow: 0 0 26px 2px rgba(222, 211, 184, 0.35); }
  40%  { border-color: rgba(222, 211, 184, 0.15); box-shadow: 0 0 8px 0 rgba(222, 211, 184, 0.08); }
  58%  { border-color: rgba(222, 211, 184, 0.85); box-shadow: 0 0 22px 2px rgba(222, 211, 184, 0.3); }
  100% { border-color: rgba(222, 211, 184, 0.25); box-shadow: 0 0 6px 0 rgba(222, 211, 184, 0.06); }
}
.cue-insert.soothe .vignette-ring,
.cue-spotlight.soothe .vignette-ring {
  stroke: rgb(222, 211, 184);
  filter: drop-shadow(0 0 5px rgba(222, 211, 184, 0.55));
}

/* The echo — "the words spoken the moment it began". The quote itself, warm-
   lit, with one beat of emphasis as it lands. Signal only: it timestamps the
   trigger, it never grades it (the caption marks the sentence that HURT,
   never the sentence that lied). */
.cue-echo {
  z-index: 2;
  pointer-events: none;
  background: rgba(10, 20, 24, 0.8);
  border: 1px solid rgba(222, 211, 184, 0.4);
  border-radius: 4px;
  padding: 0.45rem 0.7rem;
  opacity: 0;
  transition: opacity 0.45s ease;
}
.cue-echo.in {
  opacity: 1;
  animation: echo-ink 1.4s ease-out 1 forwards; /* the beat of emphasis */
}
@keyframes echo-ink {
  0%   { box-shadow: 0 0 0 0 rgba(222, 211, 184, 0); }
  30%  { box-shadow: 0 0 24px 4px rgba(222, 211, 184, 0.38); }
  100% { box-shadow: 0 0 10px 1px rgba(222, 211, 184, 0.14); }
}
.cue-echo .echo-kicker {
  display: block;
  margin-bottom: 0.22rem;
  font-family: var(--font-ui);
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgb(222, 211, 184);
}
.cue-echo .echo-text {
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.92rem;
  line-height: 1.4;
  color: var(--hn-silver);
  text-shadow: 0 0 12px rgba(222, 211, 184, 0.35);
}
/* card-docked (insert path): centered above the chip corners */
.cue-echo.on-card {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 13%;
  text-align: center;
}
/* dock-parked (in-place path): stacks under the chips beside the region */
.cue-echo.in-dock { max-width: min(340px, 30vw); }

/* --------------------------------------------------------------------------
   Ventral Compass (A05) — the needle chip + shield brackets. Dial + rotated
   needle + the polarity word: bearing is carried by shape and label, never by
   color alone. Shields render as raised-shield hatching in the cold-silver
   family — a fact about posture, not hostility (half the raised shields in
   any room are cold hands and a warm mug).
   design/abilities/05-ventral-compass.md §UI Treatment.
   -------------------------------------------------------------------------- */
/* THE GAUGE CARD. The dial STAYS: it is an instrument, and bearing is carried
   by the needle's angle and the word, never by colour. It is re-mounted on a
   small chit and re-inked, and the needle's own 0.6s settle is left alone.
   Same no-plate ruling as the weather chip, and card-face made it unarguable:
   that plate's paint occupies only x[102..538] of 640 and y[51..366] of 420, so
   a THIRD of its width is transparent margin — stretched to a chip it put the
   dial and the word outside the painted card entirely. Flat chit here too. */
.needle-chip {
  position: absolute;           /* on the insert card: mirrors the weather chip */
  right: 5%;
  bottom: 3.5%;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--pp-type, #22242b);
  background: var(--pp-card, #ede6d6);
  border: 1px solid var(--pp-rule, rgba(43, 58, 85, 0.28));
  border-radius: 0;
  padding: 0.32rem 0.65rem;
  transform: rotate(0.6deg);
  box-shadow: var(--pp-shadow-pinned, 0 2px 5px rgba(0, 0, 0, 0.5));
}
.cue-chip-dock .needle-chip { position: static; }
.needle-chip .needle-dial { width: 18px; height: 18px; flex: none; display: block; }
.needle-chip .nd-ring {
  fill: none;
  stroke: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  stroke-width: 1.5;
}
.needle-chip .nd-needle {
  transform-origin: 12px 12px;
  transition: transform 0.6s ease;  /* the needle settles, it never snaps */
}
.needle-chip .nd-tip { fill: var(--pp-type, #22242b); }
.needle-chip .nd-tail { fill: var(--pp-type-faint, rgba(34, 36, 43, 0.34)); }
.needle-chip .n-word {
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  letter-spacing: 0.12em;
}

/* The chip dock stacks weather chip, needle and echo on one anchor point. */
.cue-chip-dock {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

/* CLEARANCE FROM THE SLIP. The collision is structural, not cosmetic: the
   engine anchors BOTH objects to the same region — the dock is pinned at the
   region's LEFT edge and grows rightward (js/scene-player.js:3854, `left:
   min(PCT_X(x), 92)%`), while the callout is placed past the region's RIGHT
   edge plus GAP (:3872). Whenever a chip is wider than the region plus that
   gap — which any narrow region makes certain, and a face or a pair of hands is
   a narrow region — the dock's tail runs under the slip. Neither element knows
   the other's width, and only JS knows the region's, so CSS cannot PROVE
   separation. It buys it twice:

   (a) push the dock further along the axis it already flows on, away from the
       region the callout is centred on. The chip's top edge sat ~4px above the
       slip's paper bottom, so 10px of travel clears it outright. `.above` flips
       the direction, exactly as the base's own -100% does, so a low region
       keeps its flip.
   (b) let the chips wrap when a right-side callout exists, which halves the
       dock's reach (the tracked word is what makes the pill wide). This only
       ever SHRINKS the dock, so unlike a horizontal retreat it cannot push a
       chip out through the frame's left edge.

   The echo (.cue-echo.in-dock, max-width 340px) is left alone: it is a
   sentence, not a chip, and capping it would be a legibility regression.
   Static offsets, not motion — nothing here needs a reduced-motion companion. */
.cue-chip-dock .weather-chip,
.cue-chip-dock .needle-chip {
  flex-wrap: wrap;
  row-gap: 2px;
}
.cue-chip-dock:has(~ .cue-note.side-right) .weather-chip,
.cue-chip-dock:has(~ .cue-note.side-right) .needle-chip {
  max-width: 11em;
}

/* Shield brackets — hatched corners on the region (wide shot) or the card
   (.on-card). Cold-silver hatching: armor over the airway, not a verdict. */
.fx-barrier {
  --brk: rgba(157, 185, 193, 0.7);
  position: absolute;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
  background:
    linear-gradient(var(--brk), var(--brk)) left 0 top 0 / 16px 2px,
    linear-gradient(var(--brk), var(--brk)) left 0 top 0 / 2px 16px,
    linear-gradient(var(--brk), var(--brk)) right 0 top 0 / 16px 2px,
    linear-gradient(var(--brk), var(--brk)) right 0 top 0 / 2px 16px,
    linear-gradient(var(--brk), var(--brk)) left 0 bottom 0 / 16px 2px,
    linear-gradient(var(--brk), var(--brk)) left 0 bottom 0 / 2px 16px,
    linear-gradient(var(--brk), var(--brk)) right 0 bottom 0 / 16px 2px,
    linear-gradient(var(--brk), var(--brk)) right 0 bottom 0 / 2px 16px;
  background-repeat: no-repeat;
}
.fx-barrier.in { opacity: 1; }
.fx-barrier::before {          /* the raised-shield hatching, faint */
  content: '';
  position: absolute;
  inset: 2px;
  background: repeating-linear-gradient(45deg,
    rgba(157, 185, 193, 0.07) 0 2px, transparent 2px 8px);
}
.fx-barrier.on-card { inset: 3%; }
.fx-barrier.full { inset: 0; }

/* Skip affordance shown once the insert has landed — 'click to continue',
   TYPED WORDS, NO BOX. This class has three build sites in js/scene-player.js
   and .review-hint is the fourth site of the same sentence; both take the
   .bk-action register: typewriter face, small, tracked, lowercase, floating on
   the picture with nothing drawn round it. The `.in` opacity is the engine's
   and is untouched. */
.cue-skip-hint {
  position: absolute;
  z-index: 7;
  right: 2.5%;
  bottom: 3%;
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: lowercase;
  color: var(--ov-ink-dim);
  text-shadow: 0 1px 4px rgba(3, 8, 11, 0.85);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.cue-skip-hint.in { opacity: 0.7; }

/* #ae-hint — the act curtain's CONTINUE, same typed register, and the only
   place this lane sets text-transform to make a baked-uppercase string read
   lowercase. Deliberately dim: the base keeps this hint almost out of sight and
   that reading is preserved.

   SPECIFICITY NOTE — the base #ae-hint rule lives in index.html's shell <style>
   block (index.html:742), which is a stylesheet later in document order than
   this file's <link>. A bare `#ae-hint` here (1,0,0) would TIE that rule and
   lose on order, so the selector keeps the `html ` ancestor (1,0,1) that the
   elected candidate's own prefix carried. That is the minimum raise that wins,
   it wins by specificity rather than by order, and index.html is not this
   file's to edit. */
html #ae-hint {
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: lowercase;
  color: var(--ov-ink-faint);
}

/* --------------------------------------------------------------------------
   Still Portrait hold — ten seconds of nothing but attention.
   Press-and-hold (pointer or Space); ambient text dims while holding;
   the ring fills; early release resets the ring — no penalty, no fail.
   -------------------------------------------------------------------------- */
.portrait-hold {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.1rem;
  background: rgba(7, 14, 18, 0.72);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.portrait-hold.in { opacity: 1; }

.portrait-hold .portrait-still {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
}
.portrait-hold .portrait-still img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.15) brightness(0.55) contrast(1.05);
}

.portrait-title {
  font-family: var(--font-type);
  font-size: 0.78rem;
  letter-spacing: 0.3em;
  text-indent: 0.34em;
  text-transform: uppercase;
  color: var(--ov-ink);
}

.portrait-subject {
  font-family: var(--font-prose);
  font-size: 1.35rem;
  color: var(--hn-white);
  letter-spacing: 0.06em;
}

.portrait-ring { width: 148px; height: 148px; }
.portrait-ring .ring-track {
  fill: none;
  stroke: rgba(201, 210, 212, 0.18);
  stroke-width: 3;
}
.portrait-ring .ring-fill {
  fill: none;
  stroke: var(--hn-silver);
  stroke-width: 3;
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
  /* dashoffset driven by JS during the hold; transition only on reset */
}
.portrait-ring.resetting .ring-fill {
  transition: stroke-dashoffset 450ms ease;
}
.portrait-hold.done .ring-fill {
  stroke: var(--hn-gold);
  filter: drop-shadow(0 0 8px rgba(185, 166, 124, 0.5));
}

/* Ambient note dims while the player holds — the world going quiet */
.portrait-note {
  max-width: 34em;
  text-align: center;
  font-style: italic;
  color: var(--text-dim);
  font-size: 0.98rem;
  line-height: 1.6;
  padding: 0 2rem;
  transition: opacity 600ms ease;
}
.portrait-hold.holding .portrait-note { opacity: 0.22; }

/* The hold instruction — typed lowercase ink on the picture. (.mirror-hint is
   the same class, so the mirror's instruction moves with it.) */
.portrait-hint {
  font-family: var(--font-type);
  font-size: 0.62rem;
  letter-spacing: 0.2em;
  text-transform: lowercase;
  color: var(--ov-ink-dim);
  text-shadow: 0 1px 4px rgba(3, 8, 11, 0.85);
  opacity: 0.8;
  transition: opacity 600ms ease;
}
.portrait-hold.holding .portrait-hint { opacity: 0.2; }

/* --------------------------------------------------------------------------
   THE MIRROR (v1.6, [MIRROR]) — the render stack aimed at Sam's own reflection.

   Every layer below is live and reports NOTHING, and that is the beat: the
   shimmer draws no deviation, the weather band holds one reading, the underglow
   comes up on an empty lower frame, the echo strip is an empty formatted
   element rather than a missing one, and the sentence rail never draws at all
   (there is no rule for it here because it is never built).

   THE TRAY IS SHORT AND NOTHING MARKS THE GAP. No hollow outline (that idiom
   belongs to #13 The Null, five chapters later), no greyed ghost, no strike,
   no tooltip. There is no rule below for an absent row because there is no
   element for one. Do not add one.

   No red, no green, no alert state anywhere in here. It is not a malfunction.
   -------------------------------------------------------------------------- */
.mirror-beat {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  background: rgba(7, 14, 18, 0.86);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.mirror-beat.in { opacity: 1; }
.mirror-beat.out { opacity: 0; }

.mirror-stack { position: absolute; inset: 0; z-index: -1; overflow: hidden; }

/* Blurred cover copy under the fitted plate — the .panel-fill contract, so a
   3:4 bathroom plate fills the 16:9 frame with no dead letterbox bars. */
.mirror-fill {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(26px) brightness(0.4) saturate(0.5);
  transform: scale(1.12);           /* hide the blur's soft edges */
}

/* THE PLATE BOX IS THE PLATE'S OWN SHAPE, not the stage's.
   `aspect-ratio` is stamped inline from data/mirror-fields.json (or, for an
   unmeasured plate, from the decoded image), height-driven exactly like
   .comic-frame and .cue-insert. This is what makes .mirror-field's panel-space
   percentages resolve against painted pixels: cover-cropping a 880x1168 plate
   into the 16:9 stage threw ~40% of the picture off screen, and the field with
   it. Every bathroom plate is portrait; a wide plate would want a max-width
   clamp, and a measured row, before it ships. */
.mirror-plate {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}
.mirror-art {
  width: 100%;
  height: 100%;
  object-fit: cover;                /* the box is the art's aspect: no crop */
  filter: saturate(0.2) brightness(0.6) contrast(1.04);
}

/* The silver plane the plate paints empty, and the sprite composited into it.
   Geometry is per-plate (data/mirror-fields.json, measured off the installed
   PNG) and applied in panel space (regionCss) against the fitted plate box, so
   it sits on the painted glass at any frame size. */
.mirror-field {
  position: absolute;
  overflow: hidden;
  background: linear-gradient(160deg, rgba(150, 166, 172, 0.16), rgba(90, 106, 116, 0.1));
  box-shadow: inset 0 0 40px rgba(7, 14, 18, 0.55);
}
.mirror-reflection {
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 52% at 50% 40%, rgba(201, 210, 212, 0.14), transparent 72%);
}

/* #1 the deviation layer: present, live, and with nothing to draw. */
.mirror-shimmer { position: absolute; inset: 0; }

/* #2 the weather band: one reading, level, for the whole beat. */
.mirror-weather {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 6%;
  height: 10px;
  display: flex;
  align-items: center;
}
.mirror-weather .mw-line {
  flex: 1;
  height: 1px;
  background: rgba(201, 210, 212, 0.42);
}

/* #3 low truth: the underglow, over an empty lower frame. feetVisible is false
   in this room by design — nothing is composed for it to find. */
.mirror-underglow {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 26%;
  background: linear-gradient(to top, rgba(201, 210, 212, 0.1), transparent);
  pointer-events: none;
}

/* #5 the needle: it points at the glass, which is to say at nothing. */
.mirror-needle {
  position: absolute;
  right: 6%;
  top: 6%;
  width: 30px;
  height: 30px;
}
.mirror-needle .needle-dial { width: 30px; height: 30px; display: block; }
/* THE BEARING FAILURE — one repaint. The leaf is drawn; the bearing is not.
   No animation, no hold, no zoom: the head is simply not there for a frame. */
.mirror-needle.no-bearing .nd-tip { opacity: 0; }

.mirror-side {
  position: absolute;
  left: 3.5%;
  top: 12%;
  width: 24%;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  font-family: var(--font-ui);
}
/* #1 the portrait strip — with his own face in it. Nothing announces it. */
.mirror-strip {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.62rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--hn-silver);
}
.mirror-strip .ms-face {
  width: 26px;
  height: 34px;
  flex: none;
  border: 1px solid rgba(201, 210, 212, 0.45);
  background: linear-gradient(180deg, rgba(201, 210, 212, 0.16), transparent);
}
/* #4 the echo transcript: EMPTY, and visible as an empty formatted element —
   which is not the same thing as absent, and is the whole point. */
.mirror-echo { min-height: 2.1rem; display: flex; align-items: flex-start; }
.mirror-echo .me-rule {
  display: block;
  width: 100%;
  height: 1px;
  background: rgba(222, 211, 184, 0.22);
}
/* Sam's mirror, promoted from its corner: what a room can still read off him. */
.mirror-tray { display: flex; flex-direction: column; gap: 0.28rem; }
.mirror-tray .mt-row {
  font-family: var(--font-prose);
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  border-left: 1px solid rgba(201, 210, 212, 0.3);
  padding-left: 0.5rem;
}

.mirror-ring { width: 148px; height: 148px; }
.mirror-note { max-width: 34em; text-align: center; }
.mirror-beat.holding .mirror-note { opacity: 0.22; }
.mirror-beat.holding .mirror-hint { opacity: 0.2; }
.mirror-beat.done .ring-fill {
  stroke: var(--hn-gold);
  filter: drop-shadow(0 0 8px rgba(185, 166, 124, 0.5));
}
/* The live region carries the one polite announcement and nothing visual. */
.mirror-live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* --------------------------------------------------------------------------
   Ghost replay (A07 ring-buffer stub) — last beats replayed as translucent
   frames with a silver timestamp strip.
   -------------------------------------------------------------------------- */
.ghost-layer {
  position: absolute;
  inset: 0;
  z-index: 9;
  background: rgba(7, 14, 18, 0.82);
}
.ghost-layer[hidden] { display: none; }

.ghost-frame {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ghost-frame img {
  max-width: 88%;
  max-height: 78%;
  object-fit: contain;
  opacity: 0.6;                    /* ghost frames at 60% */
  filter: saturate(0.1) contrast(1.02);
}

.ghost-strip {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.7rem 1.4rem 0.9rem;
  background: linear-gradient(0deg, rgba(7, 14, 18, 0.95), transparent);
  font-family: var(--font-ui);
  font-size: 0.74rem;
  letter-spacing: 0.16em;
  color: var(--hn-silver);
  display: flex;
  gap: 1.2rem;
  align-items: baseline;
}
/* v1.6 — the scrubber's change track. One tick per beat in the buffer, and on
   a MEASURING rewind every one of them stays empty for the whole length. The
   blank track is what two Attention bought: proof that nothing deviated. It is
   not styled as a failure — same silver, same weight, simply unfilled. */
.ghost-track {
  position: absolute;
  left: 1.4rem;
  right: 1.4rem;
  bottom: 2.6rem;
  height: 6px;
  display: flex;
  gap: 3px;
  align-items: stretch;
}
.ghost-track .gt-tick {
  flex: 1;
  border: 1px solid rgba(201, 210, 212, 0.28);
  border-radius: 1px;
  background: rgba(201, 210, 212, 0.14);
}
.ghost-track.empty .gt-tick { background: transparent; }

.ghost-strip .ghost-time { color: var(--hn-gold); }
.ghost-strip .ghost-what {
  font-family: var(--font-prose);
  font-style: italic;
  letter-spacing: normal;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Read replay (v1.1 [REVIEW]) — the player's committed reads, one card at a
   time over a dimmed stage. Ledger-strip family: subject plate, italic serif
   stimulus, silver cue names; kind tags reuse the choice grammar (insufficient
   keeps the calibration color); sin rows carry a quiet brass "verdict" chip.
   No grading language, no red/green, nothing scored.
   -------------------------------------------------------------------------- */
.review-layer {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 4% 6%;
  background: rgba(7, 14, 18, 0.82);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.review-layer.in { opacity: 1; }

.review-head {
  display: flex;
  align-items: baseline;
  gap: 1.1rem;
  font-family: var(--font-ui);
  text-transform: uppercase;
}
.review-head .review-title {
  font-size: 0.78rem;
  letter-spacing: 0.3em;
  color: var(--hn-silver);
}
.review-head .review-range {
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  color: var(--hn-rain);
}

.review-card-mount { width: min(620px, 92%); }

.review-card {
  background: var(--surface);
  border: 1px solid var(--surface-line);
  border-left: 3px solid var(--hn-teal-bright);
  border-radius: 3px;
  padding: 1rem 1.3rem 1.1rem;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
}

.review-card .rv-plate {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.9rem;
  margin-bottom: 0.55rem;
  font-family: var(--font-ui);
  text-transform: uppercase;
}
.review-card .rv-subject {
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  color: var(--hn-rain);
}
.review-card .rv-kind {
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  color: var(--text-dim);
}
.review-card .rv-kind.kind-insufficient { color: var(--hn-calibration); }
/* the hindsight stamp — in-fiction brass, identical weight to the kind tag */
.review-card .rv-verdict {
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  color: var(--hn-brass);
  border: 1px solid rgba(201, 151, 63, 0.45);
  border-radius: 999px;
  padding: 0.1rem 0.55rem;
}
.review-card .rv-stim {
  margin: 0 0 0.6rem;
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text-dim);
}
.review-card .rv-cues {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.7rem;
  margin-bottom: 0.6rem;
}
.review-card .rv-cue {
  font-family: var(--font-ui);
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hn-silver);
  border: 1px solid rgba(201, 210, 212, 0.3);
  border-radius: 999px;
  padding: 0.12rem 0.6rem;
}
.review-card .rv-call {
  margin: 0;
  font-family: var(--font-prose);
  font-size: 1.02rem;
  line-height: 1.55;
  color: var(--hn-silver);
}
.review-card .rv-empty {
  font-family: var(--font-prose);
  font-style: italic;
  color: var(--text-dim);
  text-align: center;
  padding: 0.6rem 0;
}

/* THE PAGER, TYPED. The engine writes "3 / 12" (js/scene-player.js:6531) and
   CSS cannot change a text node, so the word is added as typed type in front of
   it: "page 3 / 12". "of" would need the JS — see the return. */
.review-count {
  font-family: var(--font-type);
  font-size: 0.62rem;
  letter-spacing: 0.2em;
  text-transform: lowercase;
  color: var(--ov-ink-dim);
}
.review-count::before { content: 'page '; }
/* the fourth 'click to continue' site — same register as .cue-skip-hint */
.review-hint {
  font-family: var(--font-type);
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: lowercase;
  color: var(--ov-ink-dim);
  text-shadow: 0 1px 4px rgba(3, 8, 11, 0.85);
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   Ledger disclosure (v1.1 [DISCLOSE]) — a held quiet beat. The ledger strip
   opens across the frame, Meridian brass accented, and the entry inks in.
   The presentation never names the first-noticer.
   -------------------------------------------------------------------------- */
.disclose-layer {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(7, 14, 18, 0.7);
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.disclose-layer.in { opacity: 1; }

.disclose-strip {
  width: min(560px, 88%);
  background: rgba(28, 21, 13, 0.94);
  border-top: 1px solid var(--hn-brass);
  border-bottom: 1px solid var(--hn-brass);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6), inset 0 0 40px rgba(201, 151, 63, 0.06);
  padding: 1.1rem 1.6rem 1.3rem;
  transform: scaleY(0.06);
  transform-origin: 50% 50%;
  transition: transform 480ms ease;
}
.disclose-strip.ink { transform: scaleY(1); }

.disclose-strip .dl-head {
  font-family: var(--font-ui);
  font-size: 0.66rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--hn-brass);
  margin-bottom: 0.7rem;
}
.disclose-strip .dl-body > * {
  opacity: 0;
  filter: blur(3px);
  transition: opacity 700ms ease, filter 700ms ease;
}
.disclose-strip.ink .dl-body > * { opacity: 1; filter: blur(0); }
.disclose-strip.ink .dl-body > *:nth-child(1) { transition-delay: 420ms; }
.disclose-strip.ink .dl-body > *:nth-child(2) { transition-delay: 780ms; }
.disclose-strip.ink .dl-body > *:nth-child(3) { transition-delay: 1120ms; }

.disclose-strip .dl-tell {
  font-family: var(--font-prose);
  font-size: 1.4rem;
  color: var(--hn-white);
  letter-spacing: 0.04em;
  margin-bottom: 0.45rem;
}
.disclose-strip .dl-line {
  font-family: var(--font-prose);
  font-size: 0.95rem;
  line-height: 1.55;
  color: #a89578;
  margin-bottom: 0.55rem;
}
.disclose-strip .dl-legibility {
  font-family: var(--font-ui);
  font-size: 0.66rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--hn-silver);
}

/* --------------------------------------------------------------------------
   Reduced motion — the observation layer stays legible with all motion
   replaced by static emphasis states (mechanics contract: reduced-motion
   mode replaces shimmer/ghost animation; nothing flashes).
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  /* Ken Burns is a `both`-filled one-shot, so theme.css's blanket
     `animation-duration: 0.01ms !important` does NOT disable it — it JUMPS the
     art to the end keyframe on frame one. Measured: .panel-art renders at
     matrix(1.06,0,0,1.06,-19,-7.6) = 1356.8px inside a 1280px frame on kb-a,
     and identity on kb-b, so consecutive splashes alternated 106% / 100% with a
     hard cut and no motion to explain it. Kill the animation outright. */
  .panel-slot.kb-a .panel-art, .panel-slot.kb-b .panel-art,
  .panel-slot.kb-a .panel-fill, .panel-slot.kb-b .panel-fill { animation: none; }
  .fx-shimmer { animation: none; opacity: 0.85; }
  .fx-faint, .faint-dock { animation: none; }
  .fx-dimmed { animation: none; }
  .cue-insert, .cue-backdrop, .panel-slot, .portrait-hold, .mirror-beat { transition: none; }
  /* v1.6: the bearing failure is a STATIC no-bearing state held 400ms in
     reduced motion (js/scene-player.js failBearing) — same absence, no
     animation. Nothing here may animate it back. */
  .mirror-needle .nd-needle { transition: none; }
  .cue-insert.pulse::after {
    animation: none;
    border-color: rgba(185, 166, 124, 0.6);
    box-shadow: 0 0 14px 1px rgba(185, 166, 124, 0.2);
  }
  .cue-insert.pulse .vignette-ring { animation: none; opacity: 0.7; }
  /* v1.2 in-place: the spotlight lands settled — no fade, no pulse; the ring
     holds at a steady emphasis so the region is still unambiguously marked. */
  .cue-spotlight, .cue-note, .cue-note-link, .cue-chip-dock { transition: none; }
  .cue-spotlight.pulse .vignette-ring { animation: none; opacity: 0.7; }
  /* §27: the disclosure rings CUT between resting and raised. The pairing is
     the information and it must still be legible — only the fade goes. */
  .cue-spotlight.disclosure .vignette-ring { transition: none; }
  .portrait-ring.resetting .ring-fill { transition: none; }
  .tension-atmos { transition: none; }
  .fx-weather { transition: none; }
  .fx-weather::before { animation: none !important; } /* weather holds still; the tint alone reads */
  /* B3: A04/A05 idioms land settled — the halo holds, the echo inks in
     without the emphasis flare, the needle points without swinging. */
  .fx-soothe { transition: none; }
  .fx-soothe::before { animation: none !important; opacity: 1; }
  .cue-insert.soothe.pulse::after {
    animation: none;
    border-color: rgba(222, 211, 184, 0.6);
    box-shadow: 0 0 14px 1px rgba(222, 211, 184, 0.2);
  }
  .cue-echo { transition: none; }
  .cue-echo.in { animation: none; box-shadow: 0 0 10px 1px rgba(222, 211, 184, 0.14); }
  .needle-chip .nd-needle { transition: none; }
  .fx-barrier { transition: none; }
  /* v1.1: replay cards swap statically; the disclosure strip renders settled */
  .review-layer { transition: none; }
  .disclose-layer { transition: none; }
  .disclose-strip { transition: none; transform: scaleY(1); }
  .disclose-strip .dl-body > * { transition: none; opacity: 1; filter: none; }
}

/* ------------------------------------------------- speech: two vessels only
   Developer picks, 2026-07-25:
     DIALOGUE  -> "grow upward". No container, no connector, no dot. Lettering is
                  bottom-anchored on a baseline just above the speaker's head and
                  grows UP into the empty sky, so the newest words never move.
     NARRATOR + MONOLOGUE -> cinematic letterbox bars.

   Why no balloon: a painted balloon read as an after-edit — a flat vector shape
   with a machine-perfect radius shares nothing with brushwork and directional
   light. Open lettering has no shape to look pasted on, and it serves the
   premise: the words stay faint while the bodies stay loud.

   Blocks live INSIDE the panel they are anchored to (see bubbleHost) and are
   positioned in the panel's own 1600x900 space — the same space cue regions use —
   so they sit beside their speaker and never cover a tell. Text is engine-drawn,
   never baked into art, so the voice-synced typewriter keeps working. */
.bubble-layer{
  position:absolute; inset:0; z-index:6;
  pointer-events:none;                 /* clicks belong to the advance handler */
  transition:opacity .22s ease;
}
.stage-frame.bubbles-under-choice .bubble,
.stage-frame.bubbles-under-choice .lb-caption{ opacity:.28; }

/* A block is parented to the PANEL it belongs to (.comic-frame in comic mode,
   .panel-slot for a splash) so its percentages resolve against that panel's box
   and, in comic mode, it scrolls with its frame. Both are therefore size
   containers, so lettering scales with the PANEL rather than the stage. */
/* Every possible bubble host must be a size container, or `cqw` in .bubble-text
   resolves against the wrong box: the fallback .bubble-layer was not one, so the
   lettering rendered far too large, the measured block was ~4x too tall, and the
   fit test wrongly sent ordinary dialogue to the letterbox. */
.comic-frame, .panel-slot, .bubble-layer{ container-type:inline-size; }

/* ---------------------------------------------------------- dialogue block */
.bubble{
  position:absolute;                   /* left/bottom/width set from the anchor */
  box-sizing:border-box;
  pointer-events:none;
  padding:.5em .2em;
  color:#F7F2E7;
  z-index:7;
  animation:bubble-in .18s ease-out both;
}
/* The soft darkening that buys legibility without drawing a shape.
   It hangs off .bubble-text, NOT .bubble. .bubble is as wide as the ANCHOR,
   which is measured for the pair's longest line, so a scrim inset from it was
   sized to the longest thing the speaker ever says on that panel rather than to
   the words on screen: "Mm." rendered 22px of text under a 137px wash, and
   "Hey." 37px of text under 451px — a dark ellipse sitting mostly to the RIGHT
   of the word, over lit artwork, clipped at the panel edge. .bubble-text is
   shrink-to-fit below, so this now tracks the rendered line.
   SUPERSEDED for the slip (below), which has a real paper ground and needs no
   wash; the rule is kept for a bubble built without .bubble--slip. */
.bubble:not(.bubble--slip) .bubble-text::before{
  content:""; position:absolute; inset:-34% -18%; z-index:-1; border-radius:50%;
  background:radial-gradient(ellipse at center,
    rgba(6,12,18,.80) 0%, rgba(6,12,18,.60) 46%, rgba(6,12,18,0) 74%);
}
@keyframes bubble-in{ from{ opacity:0; transform:translateY(3px); } to{ opacity:1; transform:none; } }

.bubble-text{
  /* Shrink-to-fit so the box IS the rendered line: the scrim above and the
     caret below both key off it, and both were wrong while this filled the
     anchor's full width. A line that wraps still fills the block, because
     fit-content resolves to the available width once max-content exceeds it. */
  width:fit-content; max-width:100%;
  margin:0; position:relative; z-index:2;
  font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-style:italic;
  font-size:clamp(11px,1.45cqw,19px);
  line-height:1.36;
  text-wrap:pretty;
  text-shadow:0 1px 3px rgba(0,0,0,.95), 0 0 15px rgba(0,0,0,.85);
}

/* ==========================================================================
   THE SPEECH SLIP — 2026-09-02.  Give dialogue a body.
   ==========================================================================
   Two real-speed first-time players lost the June/Sam banter entirely and read
   "I'm leaving" — eleven pixels of white italic on a black rain window, in the
   corner of the frame opposite the woman saying it — as SAM's line. Open
   lettering carried no name, no ground and no pointer; the only speaker signal
   was which side of the picture the words landed on, and on that panel the
   authored block puts Jordan's words on Sam's side.

   The fix is not a balloon. design/50-ui-redress.md: everything the player
   touches is paper, and the note slip is already the game's own small paper
   object (the cue callouts are printed on it). Speech gets the SAME slip —
   9-sliced off the same --ov-slip-* geometry, so the tear and the transparent
   margins stay a constant size and the border IS the content's safe area.

   Four laws, one object:
     NAME    typed small caps in --font-type, tracked, on a printed hairline —
             the .speaker-plate / .lb-name grammar, restated per speaker.
     BODY    ≥15px at 1280x720, upright ink on paper. Italic is left to *marks*
             (.rv-em) and to narration; a line entirely in it says neither.
     GROUND  paper, so the words stop competing with brushwork.
     TAIL    a short ink stroke aimed at the anchor's `head` point (js
             drawBubbleTail): the pointer the anchors have described since
             2026-07-25 and nothing had ever drawn.

   No reordering anywhere in here: the name is first in the DOM and first on the
   screen, in both seats and on both sides (ch13's inversion law).
   ========================================================================== */
.bubble--slip{
  --slip-min: 190px;                   /* read by the inline width in js       */
  padding:.34rem .58rem .46rem;
  color:var(--pp-type,#22242b);
  /* The ground if the plate 404s: paper inside the safe area, under the
     border-image and hidden the moment it loads. (.cue-note's recipe.) */
  background:linear-gradient(var(--pp-card,#ede6d6),var(--pp-card,#ede6d6)) padding-box;
  border-style:solid;
  border-width:var(--ov-slip-edge);
  border-color:transparent;            /* the box goes; the slip is the box */
  border-image:var(--ov-slip) var(--ov-slip-slice) fill stretch;
  border-radius:0;
  /* drop-shadow, not box-shadow: it respects the plate's torn alpha, where a
     box-shadow would draw a rectangle behind a ragged edge. */
  filter:drop-shadow(0 4px 11px rgba(0,0,0,.58));
  animation:bubble-in var(--pp-t-lift,140ms) var(--pp-ease-paper,ease-out) both;
}
/* One degree of settle, so it reads as paper laid down rather than a panel of
   UI. Direction follows the side it is pinned from — nothing else does. */
.bubble--slip[data-side="left"]{ rotate:-.55deg; }
.bubble--slip[data-side="right"]{ rotate:.55deg; }

.bubble-name{
  display:block;
  font-family:var(--font-type,ui-monospace,Menlo,monospace);
  font-size:clamp(10px,min(1.6vh,.92vw),13px);
  letter-spacing:.2em; text-indent:.06em;
  text-transform:uppercase;
  line-height:1.15;
  color:var(--pp-stamp-blue,#2b3a55);
  border-bottom:1px solid var(--pp-rule,rgba(43,58,85,.28));
  padding-bottom:.2em; margin-bottom:.3em;
}
/* The reading face is the letterbox's face — one voice of type across vessels.
   `vh` and `vw` together, not `cqw`: the two possible hosts are a 1280-wide
   splash slot and a ~450-wide comic frame, so a container axis sizes the same
   sentence twice as differently as the stage does. min() takes whichever of
   height or width the frame is actually limited by. */
.bubble--slip .bubble-text{
  width:auto; max-width:100%;
  font-style:normal;
  font-size:clamp(15px,min(2.1vh,1.2vw),19px);
  line-height:1.42;
  color:var(--pp-type,#22242b);
  text-shadow:none;
}
/* THE DOCK. A line too tall for the sky above its baseline drops to the panel's
   bottom edge on the SPEAKER's side rather than being ejected to the cinematic
   bars: it stays in the picture it was anchored to, and the tail still aims at
   the mouth. Refused in turn by js when it would become a wall (>44% of the
   panel's height), which is the only route left to the letterbox. */
.bubble--dock{
  left:3.5%; right:auto; top:auto; bottom:3.6%;
  width:min(93%,max(60%,240px));
}
.bubble--dock[data-side="right"]{ left:auto; right:3.5%; }

/* The tail. Length and angle come from js; everything else is the pen.
   transform-origin at its left edge is what lets `rotate() translateX()` start
   the stroke exactly where the ray leaves the paper.

   IT IS CUT FROM THE SLIP, NOT INKED ON THE PANEL. A --pp-type stroke is the
   obvious choice and it is invisible: near-black on a night restaurant, a dark
   bedroom, a rainy street — every panel this game has. Measured on the shipped
   art, June's dock aimed a perfectly correct 2px ink line at her head across
   Sam's black trousers and nothing reached the screenshot. The paper stock
   reads on every ground the story owns, and it says the right thing besides:
   the slip is a torn thing reaching toward the person who spoke. The dark rim
   is what keeps it off a pale plate (a kitchen, a lamp). */
.bubble-tail{
  position:absolute; left:50%; top:50%;
  width:var(--tail-len,22px); height:3px;
  margin-top:-1.5px;
  transform-origin:0 50%;
  border-radius:1px;
  background:linear-gradient(to right,
    var(--pp-card,#ede6d6) 0%, var(--pp-card,#ede6d6) 58%,
    rgba(237,230,214,.28) 100%);
  box-shadow:0 0 0 .5px rgba(10,14,20,.55);
  pointer-events:none;
}
.bubble-tail[hidden]{ display:none; }

/* ------------------------------------------------------------- letterbox */
.lb-band{
  position:absolute; left:0; right:0; height:0; z-index:8;
  background:#05090d; pointer-events:none;
  transition:height 420ms cubic-bezier(0.22,0.61,0.36,1);
}
.lb-top{ top:0; }
.lb-bot{ bottom:0; }
.stage-frame.letterboxed .lb-band{ height:12.5%; }

/* A narration bar must reserve space, not hide the action underneath it.
   Keep the wide art and its region-coordinate host together at the decoded
   aspect. Phone portrait reading and cue/replay owners retain their layouts. */
@media (min-width:641px), (orientation:landscape) {
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-composition,.presentation-sequence.replay)) > .lb-top{height:0;}
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-composition,.presentation-sequence.replay)) > .panel-stack:not(.comic) > .panel-slot{
    inset:0 auto 14% 50%;width:auto;max-width:100%;aspect-ratio:var(--panel-art-aspect,16 / 9);transform:translateX(-50%);
  }
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-composition,.presentation-sequence.replay)) > .panel-stack:not(.comic) > .panel-slot .panel-art{
    object-fit:contain;animation:none;transform:none;
  }
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-composition,.presentation-sequence.replay)) > .panel-stack:not(.comic) > .panel-slot .panel-fill{visibility:hidden;}
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading) > .bubble-layer > .presentation-sequence:not(.replay){
    inset:0 auto 14% 50%;width:auto;max-width:100%;aspect-ratio:var(--sequence-art-aspect,16 / 9);transform:translateX(-50%);
  }
  .stage-frame.letterboxed:not(.spread):not(.cue-reading):not(.cue-insert-reading) > .bubble-layer > .presentation-sequence.reduced:not(.replay){
    inset:0 0 14%;width:auto;aspect-ratio:auto;transform:none;
  }
}

/* ---------------------------------------------------------------- the spread
   On a TALL beat the narrator stops arriving as cinematic bars sliding over the
   art and is simply set as type on the board beside the panel — the facing-page
   reading, without the paper. Bars were the last film device left in the
   presentation and the one thing that most contradicted the comic reading.

   Wide plates keep the bars: a 16:9 splash fills the stage edge to edge, so
   there is no board left to set type on. `spread` is only ever applied when the
   panel stack is in comic mode, so that rule enforces itself.
   -------------------------------------------------------------------------- */
.stage-frame.spread .lb-band{ height:0; }        /* no bars on a spread */
.stage-frame.spread{ box-shadow:none; }
.stage-frame.spread .lb-caption{
  left:7%; right:auto; top:50%; bottom:auto; height:auto;
  width:30%; max-height:74%; overflow-y:auto;
  transform:translateY(-50%);
  padding:0; justify-content:center;
}
.stage-frame.spread .lb-text{
  font-size:clamp(15px,1.32vw,20px); line-height:1.62;
  color:#D9E2DE;
  text-shadow:0 1px 12px rgba(0,0,0,.6);
}
.stage-frame.spread .lb-name{ margin-bottom:.6em; }
/* the board stays clean: panels you have already read step aside for the type */
.panel-stack.comic.spread .comic-frame.read{ opacity:0; }

/* Ordinary portrait-phone beats: readable picture above readable text.
   Cue sheets, timed comparisons and paired compositions retain their layouts. */
@media (max-width:640px) and (orientation:portrait) {
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) {
    --phone-story-art-height:min(52dvh, calc((100vw - 64px) * 1.25));
    width:100%; height:calc(100dvh - var(--hb-h,44px)); max-height:none;
    aspect-ratio:auto; margin:var(--hb-h,44px) auto 0;
  }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack > .comic-page {
    inset:0; width:100%; padding:0; display:block; transform:none!important;
  }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack > .comic-page > .comic-frame {
    position:absolute; left:calc(50% - 6px); top:38px; margin:0;
    height:var(--phone-story-art-height)!important; width:auto!important;
    aspect-ratio:4 / 5; transform:translateX(-50%)!important;
  }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack > .comic-page > .comic-frame.read { visibility:hidden; }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) .comic-art { object-fit:contain; }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition, .portrait-hold)) > .lb-caption {
    inset:calc(var(--phone-story-art-height) + 54px) 36px 24px 18px;
    width:auto; height:auto; max-height:none; margin:0; padding:0;
    transform:none; justify-content:flex-start; overflow:auto;
    pointer-events:auto; overscroll-behavior:contain; touch-action:pan-y;
  }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .lb-caption .lb-text {
    flex:none; margin:0; font-size:calc(16px * var(--reading-scale,1)); line-height:1.55;
  }
  .stage-frame:has(> .panel-stack):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .lb-band { height:0; }
  .stage-frame:has(> .panel-stack:not(.comic)):not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) {
    --phone-story-art-height:calc((100vw - 54px) * .5625);
  }
  .stage-frame:not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack:not(.comic) > .panel-slot {
    inset:38px 36px auto 18px; height:var(--phone-story-art-height);
  }
  .stage-frame:not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack:not(.comic) > .panel-slot .panel-art {
    object-fit:contain; animation:none; transform:none;
  }
  .stage-frame:not(.cue-reading):not(.cue-insert-reading):not(:has(.presentation-sequence, .presentation-composition)) > .panel-stack:not(.comic) > .panel-slot .panel-fill { visibility:hidden; }
  .phone-story-caption:focus-visible { outline:2px solid var(--hn-silver,#c9d2d4); outline-offset:4px; }
  /* Observation has its own controls. Reserve a separate reading area for the
     completed story line instead of letting its taller phone box cover Observe. */
  .stage-frame:has(> .portrait-hold) > .lb-caption {
    inset:auto 36px 12px 18px; width:auto; height:20dvh; max-height:none;
    margin:0; padding:0; transform:none; justify-content:flex-start;
    overflow:auto; pointer-events:auto; overscroll-behavior:contain; touch-action:pan-y;
  }
  .stage-frame > .portrait-hold {
    bottom:calc(20dvh + 24px); gap:clamp(.35rem,1.1dvh,.75rem);
    padding:12px 18px; box-sizing:border-box; overflow-y:auto;
  }
  .stage-frame > .portrait-hold > :not(.portrait-still) { flex-shrink:0; }
  .stage-frame > .portrait-hold .portrait-still img { object-fit:contain; }
  .stage-frame > .portrait-hold .portrait-ring { width:clamp(64px,12dvh,100px); height:clamp(64px,12dvh,100px); }
  .stage-frame > .portrait-hold .portrait-note { margin:0; padding:0; font-size:14px; line-height:1.4; }
  .stage-frame > .portrait-hold .portrait-hint { max-width:100%; text-align:center; font-size:11px; line-height:1.45; letter-spacing:.08em; }
}
/* NO SEAT CHANGE FOR THE SPREAD. This used to push the page from padding-left
   42% to 57% — a 15%-of-stage (192px at 1280) jump carried by an UNTRANSITIONED
   property, while the compensating transform was animated. Because the vessel is
   recomputed per line, that fired 27 times on ch01's longest path, 8 of them with
   a fully-painted panel on screen, which is the sideways teleport the developer
   saw. The panel now sits in one place whether or not the narration column is
   up, so flipping the vessel moves nothing. The column lives in the board to its
   left either way. */

/* True bars, not a framed card: while letterboxed the frame drops its hairline
   outline so the black reads as one continuous field, the way a widescreen film
   does. That ring was exactly what made the first attempt look like a box. */
.stage-frame.letterboxed{ box-shadow:none; }

.lb-caption{
  position:absolute; left:0; right:0; bottom:0; height:12.5%;
  z-index:9; display:flex; flex-direction:column; justify-content:center;
  padding:0 clamp(14px,4.5cqw,54px);
  opacity:0; pointer-events:none;
  transition:opacity 260ms ease 140ms;   /* fades in behind the bands */
}
.stage-frame.letterboxed .lb-caption{ opacity:1; }
/* …EXCEPT UNDER AN OPEN CHOICE. The dim above (see .bubbles-under-choice, in the
   speech block) is the rule that the line you have already read steps back while
   the menu is the live thing. It was authored at three classes and so was
   silently defeated by the three-class rule directly above — same specificity,
   later in the file, opacity back to 1 — and only for the LETTERBOX vessel: the
   in-scene bubble half of the same declaration worked, so half the game's lines
   dimmed under a menu and half did not. Four classes, and stated after the rule
   it has to beat, so neither ordering nor specificity can lose it again. */
.stage-frame.letterboxed.bubbles-under-choice .lb-caption{ opacity:.28; }
/* THE NAME IS THE OTHER HALF OF THE SPEECH REDRESS (2026-09-02). The letterbox
   is the vessel every chapter after the first uses for dialogue, and its slug
   was set at 7.9px on a 1280 stage — present in the DOM, absent from the eye.
   Same grammar as the slip's .bubble-name and .speaker-plate: --font-type,
   tracked caps, on its own line above the words. Nothing moves; it is read. */
.lb-name{
  font-family:var(--font-type,ui-monospace,Menlo,monospace);
  font-size:clamp(10px,.82cqw,13px);
  letter-spacing:.2em; text-indent:.06em; text-transform:uppercase;
  color:#a8c089; margin-bottom:.4em;
  /* THE SLUG SITS ON ITS UNDERLINE — .speaker-plate's grammar, restated in the
     vessel that actually ships (js routes every unanchored speaker here, which
     is every chapter after the first). `align-self` shrinks the span to its own
     words plus a short printed run, so the rule reads as the line a typed slug
     is stamped on rather than as a divider across the frame. It is the mark
     that separates SOMEONE SAID THIS from the narrator, who gets no slug and
     therefore no rule. No layout moves: a hidden name still display:none. */
  align-self:flex-start;
  padding:0 2.4em .24em 0;
  border-bottom:1px solid rgba(147,173,118,.4);
}
.lb-name[hidden]{ display:none; }
/* ≥15px at 1280x720 — the floor the whole redress is measured against. The band
   is 12.5% of the frame (90px at 720), so the ceiling stays low enough that two
   lines and a slug still sit inside it. */
.lb-text{
  margin:0;
  font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-size:clamp(15px,1.2cqw,18px);
  line-height:1.46; color:#E9E3D6; text-wrap:pretty;
}
.lb-caret{
  display:inline-block; width:.44em; height:.06em; margin-left:.3em;
  background:currentColor; color:#E9E3D6; opacity:0; vertical-align:baseline;
}

/* EMPHASIS inside a scripted line. The display transform in scene-player.js
   turns *word* into this and drops *(stage directions)* entirely; the mark is
   italic and NOTHING else — no weight, no colour, no size. One voice of type,
   and the emphasis has to survive the letterbox, a speech block, an option row
   and the Record card identically. */
.rv-em{ font-style:italic; }

.bubble-caret{
  /* INLINE, and inside the <p> — not a sibling of it.
     As a block-level sibling it claimed a line of its own: ~19px of dead height
     on every speech block, which the fit test measured and which pushed real
     dialogue into the letterbox for nothing. Absolutely positioning it fixed the
     height but pinned it to the right edge of the ANCHOR box, which is sized for
     the pair's longest line — so on a short line the caret floated off alone in
     the middle of the picture. An inline-block joins the last line instead: it
     trails the final word, and being shorter than the text it adds no height.
     buildChars() already preserves it across a rebuild. Matches .lb-caret. */
  display:inline-block; width:.44em; height:.06em; margin-left:.3em;
  background:currentColor; opacity:0; vertical-align:baseline;
}
.bubble-caret.show, .lb-caret.show{
  opacity:.85; animation:caret-blink 1.05s steps(1,end) infinite;
}
@keyframes caret-blink{ 0%,60%{opacity:.85;} 61%,100%{opacity:0;} }

@media (prefers-reduced-motion: reduce){
  .bubble{ animation:none; }
  .lb-band, .lb-caption{ transition:none; }
  .bubble-caret.show, .lb-caret.show{ animation:none; }
}


/* --------------------------------------------------------- establishing card
   Held for a beat at every fresh scene entry, before the first panel. Purely a
   title on the board — deliberately no voice-over, because a spoken line here
   would take a VO id and renumber every recording after it in the chapter.
   Sits above everything (z 12) so it also covers a panel that is mid-fade.
   -------------------------------------------------------------------------- */
.scene-card{
  position:absolute; inset:0; z-index:12;
  display:flex; align-items:center; justify-content:center;
  background:radial-gradient(118% 92% at 50% 46%, #16282b 0%, #0c1618 58%, #060c0f 100%);
  opacity:1;
  cursor:pointer;
}
.scene-card[data-phase="enter"]{ animation:scene-card-enter var(--scene-card-enter,600ms) ease both; }
.scene-card[data-phase="exit"]{ animation:scene-card-exit var(--scene-card-exit,440ms) ease both; }
@keyframes scene-card-enter{ from{opacity:0} to{opacity:1} }
@keyframes scene-card-exit{ from{opacity:1} to{opacity:0} }
.scene-card-title{
  font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-size:clamp(15px,2.05vw,31px);
  line-height:1.35; letter-spacing:.012em;
  color:#DCE6E3; text-align:center; text-wrap:balance;
  max-width:min(78%,900px); overflow-wrap:anywhere;
  text-shadow:0 2px 18px rgba(0,0,0,.7);
  opacity:1;
}
.scene-card:not(.rm)[data-phase="enter"] .scene-card-title{
  animation:scene-title-enter var(--scene-card-enter,600ms) cubic-bezier(0.22,0.61,0.36,1) both;
}
@keyframes scene-title-enter{ from{transform:translateY(6px)} to{transform:none} }
/* Both the timer and the visible phase freeze together, including mid-fade.
   .rm follows the game's saved preference, which already defaults to the OS. */
body.playback-paused .scene-card,
body.playback-paused .scene-card-title{ animation-play-state:paused !important; }

/* --------------------------------------------------------------------------
   A06 SENTENCE RAIL (v1.5) — congruent chips chained along the bottom of the
   frame. Two links = a chain; three = a sentence, and the clasp closes it.
   The rail states WHICH channels agreed and never what the agreement means:
   a clasped rail is still only a claim, and Chapter 11 exists to prove it.
   -------------------------------------------------------------------------- */
.chain-rail {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.5rem 0.9rem 0.6rem;
  pointer-events: none;
  background: linear-gradient(to top, rgba(10, 20, 24, 0.72), rgba(10, 20, 24, 0));
}
.chain-rail[hidden] { display: none; }

.cr-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.15rem;
  font-family: var(--font-ui);
  font-size: 0.66rem;
  letter-spacing: 0.05em;
}

/* Each link is a chip; the hairline BETWEEN links is the chain drawing itself,
   so it is a pseudo-element on every link but the first. */
.cr-link {
  color: var(--hn-silver);
  border: 1px solid rgba(201, 210, 212, 0.32);
  border-radius: 2px;
  padding: 0.16rem 0.42rem;
  background: rgba(10, 20, 24, 0.55);
  text-shadow: 0 1px 2px var(--hn-ink-deep);
}
.cr-link + .cr-link { margin-left: 0.55rem; position: relative; }
.cr-link + .cr-link::before {
  content: '';
  position: absolute;
  left: -0.55rem;
  top: 50%;
  width: 0.55rem;
  height: 1px;
  background: rgba(201, 210, 212, 0.45);
  transform-origin: left center;
  animation: cr-draw 320ms ease-out;
}

@keyframes cr-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* The clasp — the only "you did it" in the game. Shape, not colour: a closed
   bracket at the sentence's right end. */
.cr-row.sentence .cr-link { border-color: rgba(185, 166, 124, 0.55); }
.cr-row.sentence .cr-link + .cr-link::before { background: rgba(185, 166, 124, 0.7); }
.cr-clasp {
  margin-left: 0.4rem;
  width: 9px;
  height: 15px;
  border: 1.5px solid var(--hn-gold);
  border-left: none;
  border-radius: 0 3px 3px 0;
  animation: cr-clasp-in 380ms ease-out;
}

@keyframes cr-clasp-in {
  from { opacity: 0; transform: translateX(-4px); }
  to   { opacity: 1; transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .cr-link + .cr-link::before, .cr-clasp { animation: none; }
}

/* --------------------------------------------------------------------------
   A07 — the onset frame. The scrub runs cold and fast; the change frame stops,
   lifts out of the ghost, and states WHERE it started and ON WHICH WORDS. It
   never says what the change meant — resolving that is a separate purchase.
   -------------------------------------------------------------------------- */
.ghost-frame.onset img { opacity: 0.82; filter: saturate(0.35) contrast(1.06); }
.ghost-frame.onset .ghost-strip { background: linear-gradient(0deg, rgba(7, 14, 18, 0.98), transparent); }
.ghost-frame.onset .ghost-what { color: var(--hn-white); font-style: normal; }

.ghost-onset {
  position: absolute;
  left: 0; right: 0;
  bottom: 3.4rem;
  padding: 0 1.4rem;
  font-family: var(--font-prose);
  font-style: italic;
  font-size: 0.92rem;
  line-height: 1.4;
  color: var(--hn-gold);
  text-align: center;
  text-shadow: 0 1px 4px var(--hn-ink-deep);
  animation: onset-in 260ms ease-out;
}

@keyframes onset-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* The rewind row on the OBSERVE strip — same grammar as a focus row, set apart
   by its price and by sitting last: the tool for what is already gone. */
.cf-rewind { border-style: dashed; }
.cf-rewind .cf-subject { color: var(--hn-gold); }
.cf-rewind .cf-when { font-style: normal; font-family: var(--font-ui); font-size: 0.72rem; }

@media (prefers-reduced-motion: reduce) { .ghost-onset { animation: none; } }

/* ── DIEGETIC SCREEN LINES ──────────────────────────────────────────────────
   The one place in this game where words in the world are legible. Panels are
   painted with ABSOLUTELY NO TEXT, which is right everywhere except the few
   shots whose SUBJECT is a specific sentence — the archive line Sam is staring
   at, where every other line on the screen is deliberately blank. There the
   abstract stroke reads as a placeholder, so the runtime lays the real sentence
   over it. Positioned in 1600x900 PANEL space by mountScreenLine(), parented to
   the panel, and rotated onto the screen plane so it sits IN the laptop.

   Deliberately NOT styled like the speech layer: this is not somebody talking,
   it is glass. No bubble, no ground, no drop shadow — just cold screen type. */
.screen-line{
  position:absolute; display:flex; align-items:center;
  transform-origin:0 50%; pointer-events:none; z-index:6;
  /* the type scales with the panel, not the viewport: cqw resolves against the
     panel box because .panel-slot / .comic-frame are the containment context */
  /* fallback only — mountScreenLine() overrides this from the measured box width */
  font-size:clamp(6px, 1.1cqw, 18px);
}
.screen-line-text{
  font-family:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  /* Screen-blue rather than white: the plate's own glow is #C7D3DC-ish and pure
     white floats off the glass instead of sitting in it. */
  color:#2B3A45; opacity:.82; letter-spacing:.01em; white-space:nowrap;
  text-shadow:0 0 .5px rgba(43,58,69,.35);
}
/* PRINT — ink on paper, not glass. The block above is lit from behind and says so;
   a return address is pressed into the sheet. Typewriter face, typewriter ink, NO
   glow (ink-coloured type + a glow over painted paper reads as a double exposure),
   and .06em of tracking that mountScreenLine() already paid for in the advance. */
.screen-line[data-medium="print"] .screen-line-text{
  font-family:'Courier Prime', ui-monospace, "Cascadia Mono", Consolas, monospace;
  color:#22242B; opacity:.88; letter-spacing:.06em;
  text-shadow:none;
}
/* HAND — one man's pen, in an account book, under a hanging light. Not glass and not
   a typewriter: `print` is a thing a machine pressed into a sheet, and a ledger kept
   by hand for sixty years is neither. Caveat (--font-hand, loaded by css/ui-paper.css
   and already the corpus's pencil face) at the same ink as print but a shade lighter
   in weight and a shade warmer, because a nib lays less ink than a typebar. NO GLOW,
   for print's reason. Tracking is .02em and mountScreenLine() has already paid for it
   in the .44em advance — change one and you must change the other.

   NOTE FOR A LATER WAVE: this face is PROPORTIONAL, so the box fit is an average and
   not an arithmetic certainty. Boxes for hand entries are measured off the finished
   plate like a cue region (canon/screen-lines.json's own discipline) and verified in
   the runtime, never on paper. */
.screen-line[data-medium="hand"] .screen-line-text{
  font-family:var(--font-hand, 'Caveat', 'Segoe Script', 'Bradley Hand', cursive);
  color:#2A2622; opacity:.86; letter-spacing:.02em;
  text-shadow:none;
}
.screen-line-caret{
  display:inline-block; width:.09em; height:1.05em; margin-left:.22em;
  background:#2B3A45; opacity:.7; vertical-align:baseline;
  animation:caret-blink 1.05s steps(1,end) infinite;
}
@media (prefers-reduced-motion:reduce){ .screen-line-caret{ animation:none; } }

/* --------------------------------------------------------------------------
   A08 TERRITORIES — the proxemic field (grammar v1.7 §21.1)

   The room drawn as social terrain: a presence bubble per body, the border
   where two bubbles meet, a mark where one is crossed, and the furniture
   shaded by whoever is holding it.

   COLOUR LAW, and it is the whole design of this layer. Ownership and
   incursion are POLITICS, not scoring — there is no red and no green here,
   nothing that could be read as right/wrong or good/bad. Everyone is drawn in
   the silver family. People are told apart by HATCH ANGLE plus a name label:
   two channels, because one is never enough (the icon-legibility rule), and
   because a business scene puts five territories on one plate and hue alone
   would collapse at thumbnail scale.

   Everything is positioned by regionCss() in 1600x900 PANEL space and lives
   inside .fx-layer, which is parented to the panel — so a measured field lands
   on painted pixels on a tall plate as well as a wide one.

   REDUCED MOTION: identical timings and identical end states. `.rm` swaps the
   incursion pulse for a steady mark and keeps the same crossfade duration.
   Nothing here gates a state advance on a transition or a frame — the beat's
   hold is a timer in scene-player.
   -------------------------------------------------------------------------- */
.terrain-layer{
  position:absolute; inset:0; z-index:2; pointer-events:none;
  opacity:0; transition:opacity 520ms ease;
  --terr-ink:201,210,212;          /* Gift Silver — the whole layer's one hue */
  --terr-line:rgba(201,210,212,.52);
}
.terrain-layer.in{ opacity:1; }

/* Hatch identity — angle is the channel; luminance never carries meaning. */
.terrain-layer .h0{ --terr-angle:0deg; }
.terrain-layer .h1{ --terr-angle:30deg; }
.terrain-layer .h2{ --terr-angle:60deg; }
.terrain-layer .h3{ --terr-angle:90deg; }
.terrain-layer .h4{ --terr-angle:120deg; }
.terrain-layer .h5{ --terr-angle:150deg; }

/* The presence bubble: a floor-plane contour, not a halo. Wider than it is
   tall and soft at the rim, so it reads as lying ON the floor around a pair of
   feet rather than pasted onto a body. */
.terr-bubble{
  position:absolute; border-radius:50% / 42%;
  border:1px solid var(--terr-line);
  background:
    repeating-linear-gradient(var(--terr-angle, 0deg),
      rgba(var(--terr-ink),.10) 0 1px, transparent 1px 9px);
  box-shadow:inset 0 0 26px 6px rgba(var(--terr-ink),.10);
}
.terr-bubble .terr-tag{
  position:absolute; left:50%; bottom:6%; transform:translateX(-50%);
  font-family:var(--font-ui); font-size:.56rem; letter-spacing:.2em;
  text-transform:uppercase; white-space:nowrap;
  color:var(--hn-silver); opacity:.86;
  text-shadow:0 1px 2px rgba(7,14,18,.85);
}
/* Someone who has just given ground: the contour thins. No alarm, no colour. */
.terr-bubble.retreating{ border-style:dashed; opacity:.72; }

/* The border: exactly the overlap of two bubbles. A seam, drawn once, owned by
   neither of them — a cross-hatch is the only mark in the layer that belongs
   to two people at the same time. */
.terr-border{
  position:absolute;
  background:
    repeating-linear-gradient(45deg, rgba(var(--terr-ink),.16) 0 1px, transparent 1px 6px),
    repeating-linear-gradient(-45deg, rgba(var(--terr-ink),.16) 0 1px, transparent 1px 6px);
  border-left:1px solid rgba(var(--terr-ink),.28);
  border-right:1px solid rgba(var(--terr-ink),.28);
}

/* Object ownership — the furniture itself, shaded in its holder's hatch and
   captioned with the holder's name. The layer literally paints who owns the
   table (design/abilities/08 §UI Treatment). */
.terr-own{
  position:absolute; border:1px solid rgba(var(--terr-ink),.34);
  background:
    repeating-linear-gradient(var(--terr-angle, 0deg),
      rgba(var(--terr-ink),.16) 0 2px, transparent 2px 8px);
}
.terr-own .terr-tag{
  position:absolute; left:0; top:-1.35em;
  font-family:var(--font-ui); font-size:.54rem; letter-spacing:.18em;
  text-transform:uppercase; white-space:nowrap;
  color:var(--hn-rain); text-shadow:0 1px 2px rgba(7,14,18,.85);
}

/* The incursion mark, at the boundary actually crossed. An aperture ring in the
   observation register — the same desaturated-gold family the cue layer uses
   for "something happened here", never an alert. */
.terr-incursion{
  position:absolute; width:44px; height:44px; margin:-22px 0 0 -22px;
  border-radius:50%;
  border:1.5px solid rgba(185,166,124,.78);
  box-shadow:0 0 0 4px rgba(185,166,124,.12);
  animation:terr-incur 2.4s ease-out infinite;
}
.terr-incursion .terr-tag{
  position:absolute; left:50%; top:calc(100% + 4px); transform:translateX(-50%);
  font-family:var(--font-ui); font-size:.52rem; letter-spacing:.18em;
  text-transform:uppercase; white-space:nowrap;
  color:var(--hn-gold); text-shadow:0 1px 2px rgba(7,14,18,.9);
}
@keyframes terr-incur{
  0%   { transform:scale(.82); opacity:.55; }
  22%  { transform:scale(1);   opacity:1; }
  60%  { transform:scale(1.06);opacity:.75; }
  100% { transform:scale(.82); opacity:.55; }
}

/* Ground given up: the outline of where the bubble WAS, held behind it. */
.terr-retreat{
  position:absolute; border-radius:50% / 42%;
  border:1px dashed rgba(var(--terr-ink),.30);
  transform:translate(-1.6%, 1.2%) scale(1.06);
  opacity:.55;
}

/* Touch: initiator and duration, both marked, per the ability sheet. */
.terr-touch{
  position:absolute; display:inline-flex; align-items:center; gap:.4em;
  transform:translate(-50%,-50%);
  padding:.2rem .5rem; border-radius:999px;
  background:rgba(7,14,18,.76);
  border:1px solid rgba(var(--terr-ink),.38);
  font-family:var(--font-ui); font-size:.52rem; letter-spacing:.16em;
  text-transform:uppercase; white-space:nowrap; color:var(--hn-silver);
}

/* The author's line for the beat — bottom-left, clear of the letterbox band. */
.terr-note{
  position:absolute; left:4%; bottom:14%; max-width:40%; margin:0;
  font-family:var(--font-prose); font-size:.82rem; line-height:1.45;
  color:var(--hn-silver); text-shadow:0 1px 3px rgba(7,14,18,.9);
}

/* --- THE STORYBOARD PLAN -------------------------------------------------
   No measured row for this plate, so nothing is drawn on painted pixels: the
   same people, the same events, laid out as a schematic overhead diagram that
   announces itself as one. A bubble in the wrong place would be a lie about
   the room, and where people are is this layer's entire content. */
.terrain-layer.plan{
  background:rgba(7,14,18,.42);   /* the diagram sits ON the plate, not IN it */
}
.terrain-layer.plan .terr-bubble{
  border-style:dashed; border-radius:8px;
  background:repeating-linear-gradient(var(--terr-angle, 0deg),
    rgba(var(--terr-ink),.09) 0 1px, transparent 1px 11px);
  box-shadow:none;
}
.terrain-layer.plan .terr-bubble .terr-tag{
  bottom:auto; top:50%; transform:translate(-50%,-50%);
}
.terrain-layer.plan .terr-own{ border-style:dashed; border-radius:4px; }
.terr-plan-mark{
  position:absolute; right:3%; top:4%;
  font-family:var(--font-ui); font-size:.5rem; letter-spacing:.24em;
  text-transform:uppercase; color:var(--hn-rain); opacity:.75;
  border:1px dashed rgba(var(--terr-ink),.3); border-radius:2px;
  padding:.22rem .5rem;
}

/* --- REDUCED MOTION ------------------------------------------------------
   Same durations, same end states. The pulse becomes a steady mark; the layer
   still crossfades over the same 520ms, because a crossfade IS the
   reduced-motion variant (an instant cut would be a different statement), and
   the beat's hold is a timer either way. */
.terrain-layer.rm .terr-incursion{ animation:none; opacity:1; transform:scale(1); }
.terrain-layer.rm{ transition:opacity 520ms linear; }

/* --------------------------------------------------------------------------
   A09 ECHO — the synchrony thread layer (v1.8, [SYNCHRONY])

   Two bodies moving in the same shape a measurable interval apart: a fine
   thread between the mirrored limbs, an arrowhead at the FOLLOWER's end (the
   arrow points at where the information landed, not at who sent it), a tick
   run reading one tick per 100 ms, and the standing deviation shimmer taken
   twice with the lag as its offset.

   COLOUR LAW, AND IT IS ABSOLUTE. Dust-haze Silver #C7D3DC for threads, Lamp
   Ivory #E8DCC3 for tick runs, Corvin Teal #1E4E56 for the shimmer offset. NO
   RED. NO GREEN. No warm/cool valence coding of any kind — leading is not good
   and following is not bad, and the instant the palette says otherwise the
   ability becomes a dominance meter about a couple dancing. The room's own Neon
   Rose #C9438A is the PLATE's, never the layer's: it is the brightest hue in
   the picture and a layer element drawn in it would read as an alert.

   Threads are told apart by ANCHOR POSITION plus DASH PATTERN — two channels,
   because one collapses at thumbnail scale and a dance floor puts four couples
   on a single plate. Never by hue.

   Everything is positioned in 1600x900 PANEL space inside .fx-layer, which is
   parented to the panel, so a measured anchor lands on painted pixels.

   REDUCED MOTION: identical timings, identical end states. `.rm` draws the
   thread in its end state, the arrowhead placed, and expresses the shimmer
   offset as two different opacities held SIMULTANEOUSLY rather than as a delay.
   The lag is the one thing reduced motion cannot show as motion, and the
   accommodation is the tick run — under `.rm` the length IS the whole
   measurement, and it is exactly as accurate as the animation.
   -------------------------------------------------------------------------- */
.synchrony-layer{
  position:absolute; inset:0; z-index:2; pointer-events:none;
  opacity:0; transition:opacity 520ms ease;
  --sync-ink:199,211,220;        /* Dust-haze Silver #C7D3DC — threads         */
  --sync-tick:232,220,195;       /* Lamp Ivory #E8DCC3 — the tick run          */
  --sync-off:30,78,86;           /* Corvin Teal #1E4E56 — the shimmer offset   */
}
.synchrony-layer.in{ opacity:1; }

/* The thread: a hairline rule rotated onto the picture plane between two
   measured anchors. Transform origin is the left edge, which is the anchor. */
.sync-thread{
  position:absolute; height:0; transform-origin:0 50%;
  border-top:1px solid rgba(var(--sync-ink),.62);
}
/* Dash pattern is the SECOND identity channel. Four patterns before they
   repeat — a plate never carries more than four couples plus the principals. */
.sync-thread.d0{ border-top-style:solid; }
.sync-thread.d1{ border-top-style:dashed; }
.sync-thread.d2{ border-top-style:dotted; }
.sync-thread.d3{ border-top-style:double; border-top-width:3px; }

/* The background field: reduced WEIGHT, never reduced clarity. The strangers'
   arrowheads are the puzzle — decorative ones make it unwinnable. */
.sync-thread.bg{ border-top-color:rgba(var(--sync-ink),.36); }

/* THE UNANSWERED THREAD. Out to where the answering limb would be, and it
   terminates in NOTHING: no arrowhead, no ticks, no dot. It fades along its own
   length rather than ending on a mark. This must NEVER become a hollow outline
   — that is #13 The Null's idiom, three chapters away, and it would imply Sam
   perceives his own absence. He perceives Casey. He does not perceive himself. */
.sync-thread.unanswered{
  border-top-color:transparent;
  background:linear-gradient(90deg,
    rgba(var(--sync-ink),.62) 0%,
    rgba(var(--sync-ink),.30) 55%,
    rgba(var(--sync-ink),0) 100%);
  height:1px;
}

/* The arrowhead, at the FOLLOWER's end, pointing at the follower. One per
   thread, and only when a lead was stated: leads=none draws no arrowhead. */
.sync-arrow{
  position:absolute; width:0; height:0;
  border-left:9px solid rgba(var(--sync-ink),.86);
  border-top:5px solid transparent;
  border-bottom:5px solid transparent;
}
.sync-arrow.bg{ border-left-color:rgba(var(--sync-ink),.52); }

/* The tick run: one tick per 100 ms, so the eye reads DISTANCE AS TIME. Never a
   printed figure — a tick run is a length, and a length is comparable at a
   glance, which a numeral is not. */
.sync-ticks{ position:absolute; inset:0; }
.sync-tick{
  position:absolute; width:1px; height:7px; margin:-3.5px 0 0 0;
  background:rgba(var(--sync-tick),.78);
}
.synchrony-layer .sync-ticks.bg .sync-tick{ background:rgba(var(--sync-tick),.42); }

/* The paired shimmer: the existing deviation treatment, used twice with the lag
   as its delay. No new idiom — and it keeps the lag legible when a body
   occludes the thread. */
.sync-shimmer{
  position:absolute; width:26px; height:26px; margin:-13px 0 0 -13px;
  border-radius:50%;
  border:1px solid rgba(var(--sync-ink),.34);
  box-shadow:inset 0 0 10px 2px rgba(var(--sync-off),.34);
  animation:sync-breathe 2.6s ease-in-out infinite;
}
.sync-shimmer.bg{ opacity:.55; }
@keyframes sync-breathe{
  0%   { opacity:.42; }
  46%  { opacity:.92; }
  100% { opacity:.42; }
}

/* The author's line for the beat — bottom-left, clear of the letterbox band. */
.sync-note{
  position:absolute; left:4%; bottom:14%; max-width:40%; margin:0;
  font-family:var(--font-prose); font-size:.82rem; line-height:1.45;
  color:var(--hn-silver); text-shadow:0 1px 3px rgba(7,14,18,.9);
}

/* --- THE FLOOR PLAN ------------------------------------------------------
   No measured row for this plate, so nothing is drawn on painted pixels: the
   same couples, the same arrowheads, the same tick runs, laid out as a
   schematic that announces itself as one. A thread on the wrong limb is a lie
   about who is following whom, and that is this layer's entire content. */
.synchrony-layer.plan{ background:rgba(7,14,18,.42); }
.synchrony-layer.plan .sync-shimmer{
  border-radius:2px; box-shadow:none; animation:none; opacity:.8;
}
.synchrony-layer.plan .sync-shimmer.lagging{ opacity:.5; }
.sync-plan-mark{
  position:absolute; right:3%; top:4%;
  font-family:var(--font-ui); font-size:.5rem; letter-spacing:.24em;
  text-transform:uppercase; color:var(--hn-rain); opacity:.75;
  border:1px dashed rgba(var(--sync-ink),.3); border-radius:2px;
  padding:.22rem .5rem;
}

/* --- REDUCED MOTION ------------------------------------------------------
   Same durations, same end states. The shimmer's phase offset becomes two
   different opacities held simultaneously instead of a delay; the layer still
   crossfades over the same 520ms, because the crossfade IS the accommodation
   (an instant cut would be a different statement). The tick run is unchanged
   and carries the whole measurement. */
.synchrony-layer.rm .sync-shimmer{ animation:none; opacity:.92; }
.synchrony-layer.rm .sync-shimmer.lagging{ opacity:.5; }
.synchrony-layer.rm{ transition:opacity 520ms linear; }

/* The disburse slip — a faculty arriving mid-scene. Deliberately almost
   nothing: a grant is mechanically silent and its presentation is authored. */
.disburse-slip{
  position:absolute; left:50%; bottom:16%;
  transform:translateX(-50%) translateY(6px);
  z-index:9; max-width:56%; pointer-events:none;
  padding:.7rem 1.1rem; border-radius:2px;
  background:rgba(7,14,18,.82); border:1px solid rgba(201,210,212,.24);
  opacity:0; transition:opacity 240ms ease, transform 240ms ease;
}
.disburse-slip.in{ opacity:1; transform:translateX(-50%) translateY(0); }
.disburse-slip .ds-line{
  margin:0; font-family:var(--font-prose); font-size:.9rem; line-height:1.5;
  color:var(--hn-silver); text-align:center;
}

/* --------------------------------------------------------------------------
   A10 THE MASK — the face layer (grammar v1.9 §23.1)

   Two acts of deliberate darkness end here and the face arrives LABELLED.
   Performed expressions render loud in a gilt frame; leaks render small and
   unframed at panel-edge brightness. Volume is inversely proportional to
   honesty, and the UI says so.

   COLOUR LAW, AND IT IS ABSOLUTE. Gilt is Leaf Brass (--hn-leaf, #8E7A4A) and
   it is NOT Meridian Brass. Leaks are the standing Gift Silver at panel-edge
   value. NO RED. NO GREEN. No warm/cool valence coding of any kind. A gilt
   frame is not an alarm and an unframed chip is not a reward: `shown` is not
   lying and `escaped` is not truth.

   AND THE GILT MUST BE PAPER. 50-ui-redress makes the chrome a commonplace
   book, so a metallic gradient or a specular sheen would be the one glossy
   object in a game made of pencil and card. It is a DOUBLE RULE in dull ink on
   the same card stock, and it reads as "this one has been mounted".

   TRIPLE CHANNEL (20-mechanics §Accessibility): Performance = gilt frame +
   double line + its own suffix word. The double line is the PATTERN channel and
   it is not optional, because colour alone fails at thumbnail scale and fails
   for a third of players.

   NOTHING BELOW THE NECK, EVER. There is no rule in here that touches the lower
   frame, the underglow, the terrain layer or the thread layer. Do not add one.
   -------------------------------------------------------------------------- */

/* --- (shown) — the mount. A thin DOUBLE rule, and THE DOUBLE RULE IS THE
       CHANNEL, so it is preserved exactly and re-inked onto the slip: the
       box's own hairline plus the inner line ::before draws two pixels in.
       No glow, no sheen, no animation, no sound.
   Both rules move ONTO the paper for the reason the side channel did: the
   border now carries the plate, so a 1px border here would eat the safe area.
   The outer line becomes an inset ring at the padding edge and the inner line
   stays the base's own ::before at inset:2px — which, being absolutely
   positioned, is also measured from the padding box. Two ink rules, 2px apart,
   on the paper. Same grammar, same weights. ---------------------------- */
.cue-note.a10-shown {
  border-color: transparent;
  box-shadow: inset 0 0 0 1px var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  position: relative;
}
.cue-note.a10-shown::before {
  content: '';
  position: absolute;
  inset: 2px;
  pointer-events: none;
  border: 1px solid var(--pp-type-faint, rgba(34, 36, 43, 0.34));
  border-radius: 1px;
}
/* the edge facing the region takes the gilt's weight too, so the callout reads
   as one mounted object rather than a paper slip with a brass lid — both rules
   in one declaration, or the second would overwrite the first */
.cue-note.a10-shown.side-right {
  box-shadow: inset 0 0 0 1px var(--pp-type-dim, rgba(34, 36, 43, 0.62)),
              inset 2px 0 0 0 var(--pp-type, #22242b);
}
.cue-note.a10-shown.side-left {
  box-shadow: inset 0 0 0 1px var(--pp-type-dim, rgba(34, 36, 43, 0.62)),
              inset -2px 0 0 0 var(--pp-type, #22242b);
}

/* The card path: the same double rule around the insert. It is a border, not a
   pulse — .cue-insert.pulse's desaturated-gold two-pulse is the standard
   highlight beat and is UNCHANGED here, because A10 does not get its own
   attention grammar. */
.cue-insert.a10-shown {
  outline: 1px solid rgba(142, 122, 74, 0.62);
  outline-offset: 0;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6),
              inset 0 0 0 3px rgba(11, 18, 32, 0),
              inset 0 0 0 4px rgba(142, 122, 74, 0.42);
}

/* --- (escaped) — the leak. NO frame at all, no double line, no glint, at the
       smallest legible size the chip system supports, at panel-edge brightness.
       "Easy to miss unless hunted" is the requirement: a leak that catches the
       eye has inverted the ability.
   SACRED EXCEPTION #1 — THE FRAMELESS LEAK GETS NO PAPER. The base law is NO
   FRAME AT ALL and a slip IS a frame, so the leak takes no plate, no slip, no
   pin, no lift: it stays words on the picture, at the same panel-edge
   brightness, in the typed face. The `.in` opacity below is the engine's and is
   untouched. Do not dock paper onto this state. ------------------------- */
.cue-note.a10-escaped {
  background: none;
  border-image: none;
  border-width: 0;
  border-color: transparent;
  filter: none;
  box-shadow: none;
  padding: 0.4rem 0.55rem;
}
.cue-note.a10-escaped::after { content: none; }   /* no pin on the leak */
.cue-note.a10-escaped.in { opacity: 0.72; }
/* Inert now that the leak carries border-width 0, and kept as the shape of the
   base contract: the leak's region-facing edge is not a channel. */
.cue-note.a10-escaped.side-right,
.cue-note.a10-escaped.side-left {
  border-left-color: rgba(201, 210, 212, 0.12);
  border-right-color: rgba(201, 210, 212, 0.12);
}
.cue-note.a10-escaped .cue-note-kicker,
.cue-note.a10-escaped .cn-subject,
.cue-note.a10-escaped .cn-cue,
.cue-note.a10-escaped .cn-production { color: var(--ov-ink-dim); }
.cue-note.a10-escaped .cue-note-text {
  font-size: 0.8rem;
  color: var(--ov-ink-dim);
  text-shadow: 0 1px 4px rgba(3, 8, 11, 0.9);
}
.cue-insert.a10-escaped { box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5); }
.cue-insert.a10-escaped.in { opacity: 0.82; }

/* --- the suffix. Same face, same size, in parentheses, appended to the chip's
       meaning-free label. THE GAME'S ONLY STANDING EDITORIAL, and it is
       permitted for exactly one reason: IT DESCRIBES PRODUCTION, NOT MEANING.
       "(shown)" does not say false. "(escaped)" does not say true. It never
       becomes (performed), (faked), (genuine), (true) or (real).
       The two words are told apart by the WORDS and by the frame — two channels
       without spending a hue on a verdict. ---------------------------------- */
.cue-note .cn-production { color: var(--pp-type-dim, rgba(34, 36, 43, 0.62)); opacity: 1; }
/* `(shown)` keeps Leaf Brass — the gilt is the mount's own word, and on the
   paper slip it reads as a brass ink rather than the silver-on-glass it was. */
.cue-note.a10-shown .cn-production { color: var(--hn-leaf); }

/* --- the leak's sub-threshold beat. It rides the faint dock unchanged — same
       unread row, same price, same Region Focus spend at the next READ — and
       only sits lower in the brightness range and smaller. It carries NO
       suffix and announces NOTHING: it has not docked. ----------------------- */
.faint-dock.a10-leak { opacity: 0.62; max-width: 22ch; }
.faint-dock.a10-leak .fd-head { font-size: 0.66rem; }
.faint-dock.a10-leak .fd-on { font-size: 0.64rem; }
.faint-dock.a10-leak .fd-where { color: var(--hn-rain); }
.fx-region.fx-leak { opacity: 0.55; }

/* A10's live region: one polite announcement per DOCKED facial chip, signal and
   production only. Never visible. */
.face-live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* --- REDUCED MOTION. Nothing in this layer animates in the first place — the
       frame is drawn in place, the leak is drawn at its end opacity, and the
       Region-Focus reveal is the shipped faint-to-visible path, which is
       already timer-armed. That is the accommodation, and it is why there is no
       prefers-reduced-motion block here: same timers, same end states, on every
       setting. Never gate a state change on transitionend or a single rAF. */

/* --------------------------------------------------------------------------
   A13 THE NULL — the silhouette layer (grammar v1.10 §24)

   The behaviour a body's LOAD predicts, drawn where it is not. Forty minutes of
   a flawless performance without one unrehearsed beat, and in that perfect
   absence the faculty disburses.

   ONE SHAPE, AND IT IS A HOLLOW OUTLINE. The chip's fill is taken away and a
   single hairline stroke is left standing. 20-mechanics' Accessibility rule
   fixes absence as the fourth entry in the triple-channel table — Comfort =
   soft round ring + solid line, Discomfort = angular ring + hatched line, Load =
   square ring + dotted line, Performance = gilt frame + double line, ABSENCE =
   HOLLOW OUTLINE — so the hollowness is the SHAPE channel, the interior ticks
   are the PATTERN channel, and the canon negative label is the TEXT one. Three
   channels, no hue spent on a verdict.

   COLOUR LAW, AND IT IS ABSOLUTE. The standing Gift Silver (--hn-silver) drawn
   as stroke with NO FILL. No red. No green. No warm/cool valence coding of any
   kind. AND NO GILT: the Leaf Brass double rule belongs to A10's `shown` and to
   the Performance polarity below, and an outline is not a performance. `edges`
   is not an accusation and `empty` is not an acquittal — THE ABILITY'S OWN
   FAILURE MODE IS READING CONTROL AS GUILT, and the instant the palette says
   otherwise this layer has committed it on the player's behalf.

   THE OUTLINE IS FREE AND WHAT IS INSIDE IT IS ONE GLYPH. The passive half is
   the shipped faint dock wearing `.a13-outline`; the paid half is the ordinary
   Region Focus resolve. No second economy, for §23.6's reason.
   -------------------------------------------------------------------------- */

/* --- the chip. Fill removed, one hairline stroke left. It is deliberately NOT
       dimmed: an absence is the loudest channel in Act III and it must read at a
       glance — what is withheld is the INTERIOR, never the outline.
   SACRED EXCEPTION #2 — THE HOLLOW GETS NO PAPER. The shape channel here IS
   hollowness: fill removed, one hairline left, undimmed. Papering it would put
   the fill back and DELETE the channel, so the silhouette chip keeps its hollow
   silver outline and its pool-of-shade type, and the paper lane explicitly
   declines to dock a slip on it — no plate, no pin, no lift, no tilt. Do not
   dock paper onto this state. ------------------------------------------- */
.cue-note.a13-null {
  background: none;
  border-image: none;
  border-color: rgba(201, 210, 212, 0.55);
  border-width: 1px;
  border-style: solid;
  padding: 0.5rem 0.8rem;
  filter: none;
  box-shadow: none;                      /* also clears the side inset rule */
  position: relative;
}
.cue-note.a13-null::after { content: none; }   /* no pin on the silhouette */
/* the edge facing the region keeps the Gift's silver and stays hollow with it */
.cue-note.a13-null.side-right { border-left-color: rgba(201, 210, 212, 0.55); }
.cue-note.a13-null.side-left  { border-right-color: rgba(201, 210, 212, 0.55); }
/* The text sits on the picture rather than on a card, so it needs its own
   ground — the pool-of-shade treatment, not a box, which would put the fill
   back and undo the shape channel. Silver, not ink: there is no paper here. */
.cue-note.a13-null .cue-note-kicker,
.cue-note.a13-null .cn-subject,
.cue-note.a13-null .cn-cue,
.cue-note.a13-null .cn-interior,
.cue-note.a13-null .cue-note-text { color: var(--hn-silver); }
.cue-note.a13-null .cue-note-kicker,
.cue-note.a13-null .cue-note-text { text-shadow: 0 1px 4px var(--hn-ink-deep), 0 0 12px var(--hn-ink-deep); }

/* --- the card. Same hollowing at card scale: the outline has to survive the
       4:5 cutaway, not only the chip. `.cue-insert.pulse`'s desaturated-gold
       two-pulse is UNCHANGED — A13 does not get its own attention grammar, for
       the reason A10 did not. ----------------------------------------------- */
.cue-insert.a13-null {
  outline: 1px solid rgba(201, 210, 212, 0.55);
  outline-offset: 0;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
}

/* --- the region, on the wide. The spotlight ring loses its weight and becomes
       the same hairline the chip wears, drawn on the patch of picture the
       behaviour is NOT in. Same ellipse, same two-pulse, same dwell — an absence
       is an observation, not an alarm. ------------------------------------- */
.cue-spotlight.a13-null .vignette-ring {
  stroke: var(--hn-silver);
  stroke-width: 1.5;
}

/* --- THE INTERIOR — the PATTERN channel, and it exists only after the glyph is
       spent. `edges` draws short inward ticks along the stroke: control
       rendered present, a body visibly HELD. `empty` draws nothing at all and
       says so in the one word beside it, because "nothing further" has to be a
       statement rather than a missing element — an empty outline and an unbought
       one are one stroke apart on screen and they are opposite facts.
       NEITHER IS A VERDICT. ------------------------------------------------- */
.cue-note.a13-edges::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* eight short inward ticks on the box's own edge — a repeating hard-stop
     gradient, so it is geometry and not an image, and it holds at any scale */
  background:
    repeating-linear-gradient(to right,
      rgba(201, 210, 212, 0.5) 0 6px, transparent 6px 20px) top / 100% 3px no-repeat,
    repeating-linear-gradient(to right,
      rgba(201, 210, 212, 0.5) 0 6px, transparent 6px 20px) bottom / 100% 3px no-repeat;
}
.cue-insert.a13-edges {
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6),
              inset 0 3px 0 -1px rgba(201, 210, 212, 0.34),
              inset 0 -3px 0 -1px rgba(201, 210, 212, 0.34);
}
.cue-spotlight.a13-edges .vignette-ring { stroke-dasharray: 7 13; }
/* `empty` adds no mark. The absence of ticks IS the pattern channel's other
   value, and the word beside it is what makes it a statement. Do not give it a
   decoration to "balance" the pair: a mark for genuine rest would be the layer
   editorialising about a widow. */

/* --- the interior word. ONE WORD, in the same face and size as the label, in
       the kicker beside it. IT IS A SIGNAL WORD, NOT AN EDITORIAL, and the
       distinction is the reason it is allowed to exist at all: `edges` says a
       body is visibly being held and `empty` says it is not, which are facts
       about what is on the picture in exactly the way "hands still" is. It is
       NOT A10's (shown)/(escaped) suffix and never joins it — that one describes
       how a behaviour got into the room. It never becomes "(controlled)",
       "(suppressed)", "(genuine)" or "(calm)": three verdicts and a meaning. */
.cue-note .cn-interior { color: var(--pp-type-dim, rgba(34, 36, 43, 0.62)); opacity: 1; }
.cue-note.a13-edges .cn-interior { color: var(--hn-silver); }

/* --- THE PASSIVE HALF. The free dock: the outline is on the region for
       nothing, and the dock states who / where / on which words and NOT what is
       inside. It is the opposite treatment to a leak on purpose — an `escaped`
       face must be easy to miss, and an outline must be impossible to miss and
       unreadable until bought. ---------------------------------------------- */
.fx-region.fx-outline {
  border-radius: 0;
  border: 1px solid rgba(201, 210, 212, 0.62);
  box-shadow: 0 0 18px 2px rgba(10, 20, 24, 0.5);
}
.faint-dock.a13-outline .fd-where { color: var(--hn-silver); }

/* --- REDUCED MOTION. Nothing in this layer animates either: the outline is
       drawn in place at its end opacity, the ticks are geometry, and the reveal
       is the shipped faint → visible path, which is already timer-armed. Same
       timers, same end states, on every motion setting — which is why there is
       no prefers-reduced-motion block here, exactly as in A10 above. The
       `.fx-region.fx-outline` rule above deliberately overrides `.fx-faint`'s
       ring but not its entrance, so the shipped reduced-motion rule at the top
       of this file (`.fx-faint, .faint-dock { animation: none; }`) still governs
       it. Never gate a state change on transitionend or a single rAF. */

/* The Performance polarity's own row treatment lives with the rest of the menu
   chrome in css/ui.css (§24.3) — the gilt double rule on a READ option, not a
   stage layer. Nothing about it belongs here, and nothing in THIS layer may
   borrow it: an outline that acquired the gilt would be the UI calling a missing
   gesture a performance, which is this ability's own failure mode drawn on
   screen. `nullProbe().gilded` is empty on every correct frame. */

/* --------------------------------------------------------------------------
   [MIRROR wheel] — THE REFUSAL (grammar v1.9 §23.3)

   The ability wheel, opened by the game over Sam's own reflection. Every entry
   is LIVE AND SELECTABLE — not greyed, because a disabled wheel is the game
   saying "no" and a live wheel that returns nothing is the instrument saying
   "nothing", and those are different sentences.

   NO MARKER OF ANY KIND. No red, no error state, no alert colour, no sound, no
   shake, no shudder on the refusal string. It is not a malfunction. It is a
   reading. And there is no rule below for a "disabled" row, because there is no
   such row: do not add one.
   -------------------------------------------------------------------------- */
.mirror-beat.wheel { cursor: default; touch-action: auto; }

.mirror-wheel {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  min-width: min(20rem, 76vw);
  padding: 1rem 1.25rem;
  background: rgba(7, 14, 18, 0.8);
  border: 1px solid rgba(201, 210, 212, 0.22);
  border-radius: 2px;
  opacity: 0;
  transition: opacity var(--t-fade) ease;
}
.mirror-wheel.in { opacity: 1; }

.mirror-wheel .mw-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.3rem 0.9rem;
  width: 100%;
}
.mirror-wheel .mw-row {
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-left: 1px solid rgba(201, 210, 212, 0.3);
  padding: 0.22rem 0 0.22rem 0.5rem;
  font-family: var(--font-prose);
  font-size: 0.84rem;
  letter-spacing: 0.02em;
  color: var(--text);
  cursor: pointer;
}
.mirror-wheel .mw-row:hover,
.mirror-wheel .mw-row:focus-visible {
  color: var(--hn-white);
  outline: none;
  border-left-color: rgba(201, 210, 212, 0.75);
}

/* The one answer, in one place. It appears once per selection, in the SAME
   place, and it does not accumulate. In the UI's own hand — the same graphite as
   every other piece of chrome, on the same card stock: NOT system text, NOT a
   monospace face, NOT in a box with a border. */
.mirror-wheel .mw-said {
  margin: 0.2rem 0 0;
  min-height: 1.3em;
  font-family: var(--font-ui);
  font-size: 0.68rem;
  letter-spacing: var(--track-ui);
  color: var(--text-dim);
}
/* "Turn off the light" — typed ink words with a graphite underline on hover.
   The wheel itself is NOT touched: every row stays live, nothing is greyed, no
   marker of any kind is added — that is the beat. */
.mirror-wheel .mw-close {
  margin-top: 0.15rem;
  border: 0;
  font-family: var(--font-type);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: none;
  color: var(--ov-ink-dim);
  background-color: transparent;
  background-image: linear-gradient(var(--ov-rule), var(--ov-rule));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--pp-t-lift) var(--pp-ease-paper);
  padding-bottom: 2px;
  cursor: pointer;
}
.mirror-wheel .mw-close:hover,
.mirror-wheel .mw-close:focus-visible {
  color: var(--ov-ink);
  background-size: 100% 1.5px;
}
/* the base suppressed the outline on this control; law 6 says replace it, so
   the keyline is the lane's one saturated element and nothing else */
.mirror-wheel .mw-close:focus-visible {
  outline: 2px solid var(--ov-focus);
  outline-offset: 3px;
}

/* `hold` governs the NOTE SLIP and nothing else — the wheel never times out. */
.mirror-beat.wheel .mirror-note { transition: opacity var(--t-fade) ease; }
.mirror-beat.wheel .mirror-note.spent { opacity: 0.22; }

/* --------------------------------------------------------------------------
   [MIRROR void] — THE LOUDEST NOTHING (grammar v1.12 §20.5)

   The instrument pointed at the one body that returns nothing, and the fourth
   mirror form: ONE full-body hollow outline over the picture, and then the
   scene continues at the same speed.

   IT IS A13'S IDIOM AND IT WEARS A13'S PALETTE — the standing Gift Silver drawn
   as stroke with NO FILL, which is the whole of §24's colour law arriving in a
   beat that has no chip to carry it. No red. No green. NO GILT: the Leaf Brass
   double rule is A10's `shown` and the Performance polarity, a man is not a
   performance, and this is the room where that distinction is the game.

   AND THERE IS NO INTERIOR RULE BELOW, BECAUSE THERE IS NO INTERIOR. §24.2's
   `edges` / `empty` pair is a property of a granular silhouette and is bought
   with a glyph; this outline is free, and a free interior would hand away what
   the next chapter sells. There is also no chip rule, no dock rule, no rail
   rule and no ping: none of those elements exist in this beat. Do not add one.

   NOTHING ANIMATES. The outline is drawn at its end state inside the overlay's
   own fade, it is identical on every visit, and no motion setting changes it —
   which is why there is no prefers-reduced-motion block here.
   -------------------------------------------------------------------------- */
.mirror-beat.void { cursor: pointer; }

/* The plate is a ROOM, not a piece of glass: it keeps a touch more of its own
   light than the mirror's reflection treatment allows, because there is nobody
   composited into it and nothing to read the silver against. */
.mirror-beat.void .mirror-art { filter: saturate(0.3) brightness(0.56) contrast(1.03); }

/* The figure. Centred and height-driven, deliberately NOT positioned from a
   measured field: it is what the instrument returns, not a claim about which
   pixels he is standing on, and it is the same shape every time it is asked
   (js/scene-player.js VOID_FIGURE_PATH carries that ruling in full). */
.void-figure {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 86%;
  width: 100%;
  pointer-events: none;
}
.void-figure .vf-outline {
  fill: none;                        /* hollow is the SHAPE channel — never fill it */
  stroke: var(--hn-silver);
  stroke-width: 1.5;                 /* non-scaling: a hairline at every frame size */
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.92;
}

/* --------------------------------------------------------------------------
   A11 VOICE UNDER VOICE — the waveform ribbon (grammar v1.11 §25.2)

   A hairline under the caption with one glyph per authored departure from the
   speaker's vocal baseline.

   COLOUR LAW, AND IT IS ABSOLUTE: one hue for every mark, and it is the
   standing Gift Silver. NO RED. NO GREEN. No warm/cool valence coding, no
   gilt (that is A10's `shown` and §24.3's Performance row, and a voice is not
   a performance), no alarm state, no intensity ramp, no thickness that means
   "worse". 20-mechanics' triple channel is served by SHAPE (six glyphs),
   PATTERN (hollow vs solid, above vs below the line, single vs doubled) and
   TEXT (the mark's meaning-free word, in the accessible label).

   AND THERE IS NO VERDICT CHANNEL. A mark says what happened to the voice. A
   pitch-lift is arousal, never falsehood; a pause is load, never concealment.
   Nothing here may grow a colour, a count, a score or a highlight of one mark
   over another — that is the edit that makes this a lie detector, on the one
   channel where a lie detector would be most believable and most wrong.
   -------------------------------------------------------------------------- */
.vribbon {
  --vr-ink: var(--hn-silver);
  position: relative;
  margin: 0.42rem 0 0;
  height: 14px;
  pointer-events: none;
}
/* The baseline. A FLAT RIBBON IS STILL A RIBBON — this line is all an
   un-departing voice draws, and it is the identical line an un-portraited
   speaker draws. The two are not distinguished for the player anywhere. */
.vribbon .vr-track {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 0;
  border-top: 1px solid var(--vr-ink);
  opacity: 0.55;
}
.vribbon .vr-mark {
  position: absolute;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  opacity: 0.92;
}

/* lift — a chevron ABOVE the line: the pitch went up. */
.vribbon .vr-lift {
  margin-top: -7px;
  border-top: 1.4px solid var(--vr-ink);
  border-right: 1.4px solid var(--vr-ink);
  transform: rotate(-45deg);
}
/* swallow — the same chevron BELOW the line, pointing down. Orientation is the
   pattern channel; the two are never told apart by colour. */
.vribbon .vr-swallow {
  margin-top: 0;
  border-bottom: 1.4px solid var(--vr-ink);
  border-right: 1.4px solid var(--vr-ink);
  transform: rotate(45deg);
}
/* fracture — the line steps. Two short verticals at different heights: the
   pace broke and did not resume where it left off. */
.vribbon .vr-fracture {
  margin-top: -5px;
  width: 6px;
  height: 10px;
  border-left: 1.4px solid var(--vr-ink);
}
.vribbon .vr-fracture::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 4px;
  width: 0;
  height: 6px;
  border-left: 1.4px solid var(--vr-ink);
}
/* pause — a HOLLOW ring sitting on the line. Hollow is the absence pattern the
   game already uses (§24.1's outline): a micro-pause is a hole in the sound. */
.vribbon .vr-pause {
  margin-top: -3.5px;
  width: 7px;
  height: 7px;
  border: 1.2px solid var(--vr-ink);
  border-radius: 50%;
  background: var(--stage-bg, transparent);
}
/* clear — a doubled vertical: the throat is cleared before the words. */
.vribbon .vr-clear {
  margin-top: -5px;
  width: 5px;
  height: 10px;
  border-left: 1.4px solid var(--vr-ink);
}
.vribbon .vr-clear::after {
  content: "";
  position: absolute;
  left: 3px;
  top: 0;
  width: 0;
  height: 10px;
  border-left: 1.4px solid var(--vr-ink);
}
/* load — a SOLID thickening of the line itself. The one filled mark, and its
   weight is a shape channel, never a severity: it says the support went, not
   that anything is wrong. */
.vribbon .vr-load {
  margin-top: -1.5px;
  width: 12px;
  margin-left: -6px;
  height: 3px;
  background: var(--vr-ink);
  border-radius: 1.5px;
}

/* In the letterbox the caption is centred; the ribbon narrows to the text. */
.lb-caption .vribbon { max-width: 100%; }
/* Inside a bubble the ribbon takes the balloon's own width, under the words. */
.bubble .vribbon { margin: 0.3rem 0 0.1rem; height: 12px; }
/* A07's scrub redraws each line's ribbon on its ghost frame (§25.2). Dimmer,
   because everything in the ghost is. */
.vribbon.in-ghost {
  position: absolute;
  left: 1.4rem;
  right: 1.4rem;
  bottom: 4.1rem;
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   THE RE-LISTEN (v1.11 §25.3) — what one glyph buys.

   The take again, the ribbon large, and the pins struck as TICK-MARKS against
   the real clip clock with a playhead riding it. Pillar law holds inside it as
   it holds inside the A07 ghost: who, where on the line, what happened — never
   what it meant. No verdict line, no summary, no mark drawn louder than
   another, and nothing written to the Record.
   -------------------------------------------------------------------------- */
.voice-layer {
  position: absolute;
  inset: 0;
  z-index: 9;
  background: rgba(7, 14, 18, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
}
.voice-layer[hidden] { display: none; }

.voice-layer .vp-card {
  width: min(760px, 82%);
  padding: 1.1rem 1.4rem 1.5rem;
}
.voice-layer .vp-who {
  font-family: var(--font-ui);
  font-size: 0.64rem;
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  color: var(--hn-silver);
}
.voice-layer .vp-line {
  margin: 0.4rem 0 1.1rem;
  font-family: var(--font-prose);
  font-size: 1.02rem;
  line-height: 1.55;
  color: var(--hn-white);
}
.voice-layer .vp-track {
  position: relative;
  height: 26px;
}
.voice-layer .vp-track::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  border-top: 1px solid var(--hn-silver);
  opacity: 0.55;
}
/* The glyphs are the SAME six shapes as the inline ribbon, scaled up. Reusing
   the classes is deliberate: a mark the player learned under a caption has to
   be the same mark here, or the enlargement teaches a second vocabulary. */
.voice-layer .vp-track .vr-mark {
  position: absolute;
  top: 50%;
  --vr-ink: var(--hn-silver);
}
.voice-layer .vp-head {
  position: absolute;
  top: 2px;
  bottom: 2px;
  width: 0;
  border-left: 1px solid var(--hn-white);
  opacity: 0.6;
}
/* The tick-marks: one labelled pin per departure, in the mark's own
   meaning-free word. The only place the words are drawn rather than spoken to
   a screen reader, and it is what the glyph paid for. */
.voice-layer .vp-pins {
  position: relative;
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0;
  height: 2.4rem;
}
.voice-layer .vp-pin {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  text-align: center;
}
.voice-layer .vp-pin::before {
  content: "";
  display: block;
  margin: 0 auto 3px;
  width: 0;
  height: 7px;
  border-left: 1px solid var(--hn-silver);
  opacity: 0.55;
}
.voice-layer .vp-word {
  font-family: var(--font-ui);
  font-size: 0.54rem;
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  color: var(--hn-silver);
  white-space: nowrap;
}

/* ==========================================================================
   RELOCATED BLOCKS — paper dockings for surfaces whose BASE rules live in
   css/ui.css (the Observation Ledger strip + rows, the polarity chips, the
   loupe tray + rows). css/ui.css is owned by another module and is not this
   file's to edit, so the paper treatment for those four surfaces is folded in
   HERE, at the end of stage.css, and everything below is deliberately out of
   its natural place in this file.

   THE CASCADE PROBLEM, AND THE PROOF THAT THESE RULES WIN
   -------------------------------------------------------------------------
   index.html loads css/stage.css (line 11) BEFORE css/ui.css (line 12), so a
   same-specificity rule down here LOSES to its ui.css counterpart on document
   order. Order cannot be won from this file, so every rule below wins on
   SPECIFICITY instead. Two documented raises do that, and not one rule below
   raises its weight with a bang:

     (a) the `html ` ancestor. The elected candidate's selectors were prefixed
         with `html` plus its lane class; stripping only the CLASS leaves
         `html X`, which adds one type selector — the smallest possible
         increment — and turns every (0,n,m) tie into a (0,n,m+1) win.
     (b) for the ledger rows, the `.ledger-strip` ancestor as well. `.ledger-
         rows` is only ever built inside `.ledger-strip` (js/scene-player.js:
         630-633), and the extra class is what beats ui.css's own
         `.ledger-rows li:last-child` (0,2,1) with `html .ledger-strip
         .ledger-rows li` (0,2,2) — the same outcome the elected shot had.

   Worked specificity, mine vs the ui.css rule each one replaces:
     html .ledger-strip                             (0,1,1) > .ledger-strip                      (0,1,0)
     html .ledger-strip .ledger-rows                (0,2,1) > .ledger-rows                       (0,1,0)
     html .ledger-strip .ledger-rows:empty::after   (0,3,1) > .ledger-rows:empty::after          (0,2,0)
     html .ledger-strip .ledger-rows li             (0,2,2) > .ledger-rows li:last-child         (0,2,1)
     html … .ledger-rows .l-subject/.l-cue/.l-stim  (0,3,1) > .ledger-rows .l-*                  (0,2,0)
     html … .ledger-rows .l-cue.l-null              (0,4,1) > .ledger-rows .l-cue.l-null         (0,3,0)
     html … li.unread-row                           (0,3,2) > .ledger-rows li.unread-row         (0,2,1)
     html … li.unread-row .l-cue                    (0,4,2) > … li.unread-row .l-cue             (0,3,1)
     html … li.unread-row.expired                   (0,4,2) > … li.unread-row.expired            (0,3,1)
     html … li.unread-row.expired .l-cue            (0,5,2) > … li.unread-row.expired .l-cue     (0,4,1)
     html … li.rebaseline-row .l-cue                (0,4,2) > … li.rebaseline-row .l-cue         (0,3,1)
     html … li.ghost-row                            (0,3,2) > .ledger-rows li.ghost-row          (0,2,1)
     html .ledger-strip .pol-chip                   (0,2,1) > .pol-chip                          (0,1,0)
     html .ledger-strip .pol-* i                    (0,2,2) > .pol-* i                           (0,1,1)
     html .loupe-tray                               (0,1,1) > .loupe-tray                        (0,1,0)
     html .lt-row                                   (0,1,1) > .lt-row                            (0,1,0)
     html .lt-row:hover:not(:disabled)              (0,3,1) > .lt-row:hover:not(:disabled)       (0,3,0)
     html .lt-row:focus-visible                     (0,2,1) > .lt-row:focus-visible              (0,2,0)
                                                            > :focus-visible (theme.css)         (0,1,0)
     html .lt-row:disabled                          (0,2,1) > .lt-row:disabled                   (0,2,0)
     html .lt-row.holding                           (0,2,1) > .lt-row.holding                    (0,2,0)
   ========================================================================== */

/* ==========================================================================
   RELOCATED · .ledger-strip — THE RULED PAPER STRIP    (base rules: css/ui.css)
   The Observation Ledger pulls down as a page out of the commonplace book: the
   ruled spread as ground (right page), entries as ruled lines, typed names,
   the stimulus in the prose hand it was always in. Nothing is reordered, no row
   gains or loses a channel.
   ========================================================================== */
html .ledger-strip {
  /* THE RULE LOCK (owner ruling 2026-08-31). The plate is blank now, so the
     ruling is drawn here and the entries are quantized to it: one rule per
     entry, two when the entry carries its stimulus. The mechanism and the
     measured face metrics are documented once on :root in css/ui.css under THE
     RULE LOCK; every --rp-* read below carries a literal fallback so a build
     without that sheet still gets a legible ruled strip.

     `background-attachment: local` on the ruling and NOT on the plate, and that
     asymmetry is the point: the strip scrolls (max-height 42vh, overflow-y
     auto), so a ruling fixed to the box would hold still while the writing slid
     over it. Tiled with `repeat-y` rather than sized to the box for the same
     reason — a band sized to the visible height would scroll away and leave the
     entries below it unruled. The pitch therefore rides `background-size`, not
     the gradient, which also keeps --ov-rules free of any var() that would have
     to resolve before a pitch existed (see the trap noted in ui.css).

     The phase is `.ledger-rows`' own padding-top, because that is where the
     first entry's box starts and the shim puts baselines a whole --rp below it. */
  --rp: 1.6rem;
  --rp-pad-t: 0.2rem;
  --rp-k: var(--rp-k-type, 0.44);
  --ov-rules: linear-gradient(to bottom,
    var(--rp-ink, rgba(43, 58, 85, 0.28)) 0,
    var(--rp-ink, rgba(43, 58, 85, 0.28)) var(--rp-w, 1px),
    rgba(0, 0, 0, 0) var(--rp-w, 1px));
  background:
    var(--ov-rules) 0 calc(var(--rp-pad-t) + var(--rp-drop, 0.1rem)) / 100% var(--rp) repeat-y local,
    var(--ov-ruled) right center / cover no-repeat,
    var(--pp-ledger, #e8dfc9);
  border: 0;
  border-radius: 0;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
}
html .ledger-strip .ledger-rows { padding: var(--rp-pad-t) 0; }
/* THE ENTRY IS THE RULE. Its `border-bottom` was a drawn ruling standing in for
   a page that had none; the page prints its own now, one --rp-drop under the
   entry's own baseline, so the border comes off — a border a pixel above a
   printed rule reads as a smudge, not as two lines.
   ALIGNMENT MOVES FROM `baseline` TO `flex-end` AND THE BASELINES STILL AGREE,
   because every child below is exactly one rule tall and each is shimmed onto
   that rule by its own face's metric. Flex baseline alignment would instead
   make the entry as tall as its tallest ascent plus its deepest drop — half a
   pixel over a rule per entry, which is a rule of drift every fifty entries.
   The row gap goes to zero for the same reason; the column gap is untouched. */
html .ledger-strip .ledger-rows li {
  align-items: flex-end;
  line-height: var(--rp);
  gap: 0 0.7rem;
  padding: 0 1.1rem;
  border-bottom: 0;
  color: var(--pp-type, #22242b);
}
html .ledger-strip .ledger-rows .l-subject,
html .ledger-strip .ledger-rows .l-cue,
html .ledger-strip .ledger-rows .l-stim,
html .ledger-strip .pol-chip {
  position: relative;
  top: calc((var(--rp) - var(--rp-k) * 1em) / 2);
}
html .ledger-strip .ledger-rows .l-subject {
  font-family: var(--font-type);
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}
html .ledger-strip .ledger-rows .l-cue {
  font-family: var(--font-type);
  letter-spacing: var(--track-type, 0.06em);
  color: var(--pp-type, #22242b);
}
/* the quoted stimulus is the one prose line in the entry, and it has its own
   metric; `flex-basis: 100%` already puts it on its own flex line, which is now
   its own rule */
html .ledger-strip .ledger-rows .l-stim { --rp-k: var(--rp-k-prose, 0.698); }
/* The silhouette's negative label keeps its HOLLOW shape — outline, no fill —
   and the outline has to keep HUGGING THE WORD. It is a direct child of the
   entry, so flexbox blockifies it: at the entry's own line-height its box would
   be a whole rule tall and the ring would read as an empty field around a short
   word, and its 1px border would push the entry two pixels past its rule. So it
   takes a tight line box of its own, hangs from the entry's TOP, and states its
   own offset. It still hangs from the entry's bottom edge with everything else,
   so the offset is just how far its own box drops BELOW its baseline —
       1px border + (1.35em − (A+D)em)/2 + D·em  =  1px + (1.35 − k) x 0.5em
   — pushed back down so the baseline lands on the rule. The ring then dips a
   few pixels past the printed line, which is what a box drawn round a word on
   ruled paper does. Shape, ink and absence of fill are untouched; only the box
   around them is. */
html .ledger-strip .ledger-rows .l-cue.l-null {
  border-color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
  background: none;
  line-height: 1.35;
  top: calc(1px + (1.35 - var(--rp-k)) * 0.5em);
}
html .ledger-strip .ledger-rows .l-stim { color: var(--pp-type-dim, rgba(34, 36, 43, 0.62)); }
/* "seen, unread": the gold margin rule becomes a pencil stroke in the margin;
   spent/expired goes faint exactly as the base's hollowing does. */
/* `padding-left` is restated here because the entry's own padding above is now
   (0,2,2) and would otherwise beat the base's `li.unread-row` (0,2,1) and
   straighten this row's indent out. The pencil stroke keeps its margin. */
html .ledger-strip .ledger-rows li.unread-row {
  border-left: 2px solid var(--pp-graphite, #3a3f45);
  padding-left: 0.5rem;
}
html .ledger-strip .ledger-rows li.unread-row .l-cue { color: var(--pp-type, #22242b); }
html .ledger-strip .ledger-rows li.unread-row.expired {
  border-left-color: var(--pp-type-faint, rgba(34, 36, 43, 0.34));
}
html .ledger-strip .ledger-rows li.unread-row.expired .l-cue {
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}
html .ledger-strip .ledger-rows li.rebaseline-row .l-cue {
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}
html .ledger-strip .ledger-rows li.ghost-row { color: var(--pp-type-dim, rgba(34, 36, 43, 0.62)); }
/* the empty line is marginalia, so it is written in the hand — and it is
   written ON the first rule like everything else on the strip */
html .ledger-strip .ledger-rows:empty::after {
  font-family: var(--font-hand);
  font-size: 1.15rem;
  font-style: normal;
  line-height: var(--rp);
  position: relative;
  top: calc((var(--rp) - var(--rp-k-hand, 0.66) * 1em) / 2);
  padding: 0 1.1rem;
  color: var(--pp-graphite, #3a3f45);
}

/* ==========================================================================
   RELOCATED · .pol-chip — INK MARKS                    (base rules: css/ui.css)
   SHAPE + LABEL (+ pattern) are the channels and all three survive: round ring,
   rotated hatched square, dotted square, tick. Only the mark colour moves from
   Gift Silver to typewriter ink, because the chip now sits on paper. Scoped to
   the ledger so nothing else can inherit it.
   ========================================================================== */
/* `align-items: baseline` inside the chip is what puts the MARK on the rule:
   the `i` carries no text, so flexbox synthesises its baseline from its bottom
   border edge and the mark comes to rest on the same line as the word beside
   it. Centred (the base's value) it straddled the rule instead. Shape, label
   and pattern are all untouched — only where the pair sits has moved. */
html .ledger-strip .pol-chip {
  font-family: var(--font-type);
  align-items: baseline;
  color: var(--pp-type-dim, rgba(34, 36, 43, 0.62));
}
html .ledger-strip .pol-comfort i { border-color: var(--pp-type, #22242b); }
html .ledger-strip .pol-discomfort i {
  border-color: var(--pp-type, #22242b);
  background: repeating-linear-gradient(45deg,
    var(--pp-type, #22242b) 0 1px, transparent 1px 3px);
}
html .ledger-strip .pol-neutral i { border-color: var(--pp-type, #22242b); }
html .ledger-strip .pol-noted i {
  border-left-color: var(--pp-type, #22242b);
  border-bottom-color: var(--pp-type, #22242b);
}

/* ==========================================================================
   RELOCATED · .loupe-tray — THE CARD TRAY              (base rules: css/ui.css)
   The room, listed, on a small blank card. Under `locked` the tray still lists
   the room and still refuses without a word — no refusal text, no icon, no
   colour is added here, ever.

   Sliced, not stretched, for the same reason the slip is: card-face.webp paints
   only x[102..538] of 640 and y[51..366] of 420, so `100% 100%` put every tray
   row outside the painted card on all four sides. Sliced, the card's margins
   and curl render at a constant size and the rows sit ON the card by
   construction.
   ========================================================================== */
/* THE TRAY IS NOT RULED, ON PURPOSE. It is a small blank INDEX CARD — the plate
   is card-face.webp, not a page out of the ledger — and index cards are not
   ruled paper. What it takes from the rule lock is the RHYTHM: its rows are set
   on the same --rp the strip beside it uses and baseline-locked to it, so the
   two objects on screen at the same time share one measure even though only one
   of them is printed with lines. */
html .loupe-tray {
  --rp: 1.6rem;
  --rp-k: var(--rp-k-type, 0.44);
  /* the tray is content-box by default (there is no global border-box in this
     repo), so without this the 16px side borders would add 32px to its
     `width: min(280px, calc(100% - 2rem))` and overflow the frame */
  box-sizing: border-box;
  background: linear-gradient(var(--pp-card, #ede6d6), var(--pp-card, #ede6d6)) padding-box;
  border-style: solid;
  border-width: var(--ov-card-edge);
  border-color: transparent;
  border-image: var(--ov-tray) var(--ov-card-slice) fill stretch;
  border-radius: 0;
  box-shadow: none;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.55));
  padding: 0.2rem 0;
}
html .lt-row {
  font-family: var(--font-type);
  font-size: 0.72rem;
  letter-spacing: var(--track-type, 0.06em);
  /* one row, one measure. The shim is `position: relative; top:` and never
     padding, so the row's box stays exactly --rp tall and the underline below
     still lands where the writing ends. */
  line-height: var(--rp);
  position: relative;
  top: calc((var(--rp) - var(--rp-k) * 1em) / 2);
  padding: 0 0.8rem;
  color: var(--pp-type, #22242b);
  /* the graphite underline, .paper-underline's technique inlined */
  background-color: transparent;
  background-image: linear-gradient(var(--pp-graphite, #3a3f45), var(--pp-graphite, #3a3f45));
  background-repeat: no-repeat;
  background-position: 0.8rem 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--pp-t-lift) var(--pp-ease-paper);
}
/* THE UNDERLINE STACK IS RESTATED IN EVERY STATE RULE, ON PURPOSE. ui.css's own
   state rules paint with the `background:` SHORTHAND, which resets
   background-image to `none`, background-repeat to `repeat` and
   background-position to `0% 0%`. Those resets arrive at (0,3,0)/(0,2,0) —
   higher than `html .lt-row`'s (0,1,1) — so declaring the stack once on the
   base rule is NOT enough: on hover the graphite line would vanish, and on a
   held row it would tile. Restating the four longhands inside each state rule
   puts them at a specificity that beats the shorthand instead. */
html .lt-row:hover:not(:disabled),
html .lt-row:focus-visible {
  background-color: transparent;
  background-image: linear-gradient(var(--pp-graphite, #3a3f45), var(--pp-graphite, #3a3f45));
  background-repeat: no-repeat;
  background-position: 0.8rem 100%;
  background-size: calc(100% - 1.6rem) 1.5px;
}
html .lt-row.holding {
  background-color: transparent;
  background-image: linear-gradient(var(--pp-graphite, #3a3f45), var(--pp-graphite, #3a3f45));
  background-repeat: no-repeat;
  background-position: 0.8rem 100%;
  background-size: calc(100% - 1.6rem) 2.5px;
  color: var(--pp-type, #22242b);
}
html .lt-row:disabled {
  color: var(--pp-type-faint, rgba(34, 36, 43, 0.34));
  opacity: 1;                      /* the fade is now in the ink, not the box */
}
/* law 6 — the base tray had no ring of its own; this one has to have one */
html .lt-row:focus-visible {
  outline: 2px solid var(--ov-focus);
  outline-offset: -2px;
}

/* ==========================================================================
   REDUCED MOTION — the paper lane's companions.
   The only motion this lane adds is the slip's settle (a transform TRANSITION,
   never a keyframe, and nothing anywhere gates state on it). Under reduce the
   slip is drawn already landed: same rotation, same place, no travel — the END
   state, visibly. The two underline transitions collapse to an instant swap,
   which still lands the underline.
   `html .lt-row` keeps its ancestor here for the same reason as above: ui.css's
   `.lt-row { transition: background … }` (0,1,0) loads later, so (0,1,1) is
   what turns it off.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .cue-note:not(.on-card),
  .cue-note:not(.on-card):not(.in) {
    transform: translateY(-50%) rotate(-0.5deg);
    transition: opacity var(--t-fade) ease;
  }
  .cue-note.side-left:not(.on-card),
  .cue-note.side-left:not(.on-card):not(.in) {
    transform: translateY(-50%) rotate(0.5deg);
  }
  html .lt-row,
  .mirror-wheel .mw-close { transition: none; }
}

/* ==========================================================================
   THE TWO SACRED EXCEPTIONS, FLATTENED — last in the file on purpose.
   `a10-escaped` (the leak: no frame at all) and `a13-null` (the absence: one
   hairline, hollow, undimmed) are the two chips this lane deliberately does NOT
   dock paper onto, so they must not inherit the slip's rotation or its settle
   either — a tilted box of words with nothing under it is a slip that failed to
   load.

   POSITION IS LOAD-BEARING, NOT COSMETIC. `.cue-note.a13-null:not(.on-card)
   :not(.in)` and the reduced-motion block's `.cue-note.side-left:not(.on-card)
   :not(.in)` are BOTH (0,4,0): a silhouette on the left side under reduce
   matches both, and only source order settles it. The same tie exists between
   the kicker reset below and `.cue-note.side-left:not(.on-card)
   .cue-note-kicker`. So this block sits AFTER the reduced-motion companions
   above, exactly as it did in the elected file. Do not move it up.
   ========================================================================== */
.cue-note.a10-escaped:not(.on-card),
.cue-note.a10-escaped:not(.on-card):not(.in),
.cue-note.a13-null:not(.on-card),
.cue-note.a13-null:not(.on-card):not(.in) {
  transform: translateY(-50%);
}

/* …and they carry no pin either (`content: none` on both states), so they must
   not pay for one. Without this the leak and the silhouette would wear a 26px
   indent reserving space for a head that is not there — and on the silhouette
   that is not merely untidy: A13's chip is the one that must read at a glance,
   and an unexplained gutter is a mark on a chip whose whole point is that
   nothing has been added to it. */
.cue-note.a10-escaped:not(.on-card) .cue-note-kicker,
.cue-note.a13-null:not(.on-card) .cue-note-kicker {
  padding-left: 0;
  padding-right: 0;
}

/* Explicit presentation packages: clean source plates remain untouched. */
.presentation-composition{position:absolute;height:90%;aspect-ratio:1784/1168;max-width:98%;left:50%;top:48%;transform:translate(-50%,-50%);background:#111b1d;overflow:hidden}
.presentation-half{position:absolute;top:0;height:100%;overflow:hidden}
.presentation-art{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;max-width:none;pointer-events:none;user-select:none}
.presentation-sequence{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
.presentation-state{position:absolute;inset:0;background:#111b1d}
.presentation-sequence [hidden],.presentation-composition [hidden]{display:none!important}
.presentation-motion{position:absolute;overflow:hidden;pointer-events:none;mask-image:radial-gradient(ellipse at center,#000 46%,transparent 74%);-webkit-mask-image:radial-gradient(ellipse at center,#000 46%,transparent 74%)}
.presentation-motion .presentation-art{inset:auto;will-change:transform}
.presentation-note{position:absolute;left:3%;right:3%;bottom:3%;padding:.65rem .85rem;background:#f3eddf;color:#183d41;font:500 clamp(12px,1.5vw,18px)/1.4 var(--font-ui,sans-serif);box-shadow:0 2px 10px #0006}
.presentation-state>.presentation-art{object-fit:contain}
.presentation-sequence.reduced{display:grid;grid-template-columns:repeat(var(--presentation-columns,2),minmax(0,1fr));grid-template-rows:repeat(var(--presentation-rows,1),minmax(0,1fr)) auto;gap:8px;background:#14252a;padding:8px;box-sizing:border-box}
.presentation-sequence.reduced .presentation-state{position:relative;inset:auto;min-width:0;min-height:0;display:flex;flex-direction:column}
.presentation-sequence.reduced .presentation-art{position:relative;inset:auto;min-height:0;flex:1;object-fit:contain}
.presentation-state-label{position:relative;flex:none;background:#f3eddf;color:#183d41;padding:.35rem;font:500 clamp(11px,1.1vw,15px)/1.3 var(--font-ui,sans-serif);overflow-wrap:anywhere}
.presentation-sequence.reduced .presentation-note{position:relative;grid-column:1/-1;left:auto;right:auto;bottom:auto;padding:.5rem .65rem;margin:0;box-shadow:none}
@media(max-width:640px) and (orientation:portrait){
  .presentation-sequence.reduced[data-layout="landscape-pair"]{--presentation-columns:1!important;--presentation-rows:2!important}
}
.presentation-sequence.replay{background:#14252a}
.skin-presentation{position:absolute;inset:0;pointer-events:none;z-index:3;background:radial-gradient(ellipse at 50% 50%,transparent 35%,color-mix(in srgb,var(--skin-palette) 32%,transparent) 95%);mix-blend-mode:color}
.skin-offer{background:radial-gradient(ellipse at 50% 55%,color-mix(in srgb,var(--skin-palette) 20%,transparent) 0%,transparent 68%);mix-blend-mode:screen}
.skin-borrowed{background:color-mix(in srgb,var(--skin-palette) 21%,transparent)}
.skin-residue{background:linear-gradient(110deg,transparent 35%,color-mix(in srgb,var(--skin-palette) 15%,transparent) 100%)}
.playback-paused .sync-shimmer{animation-play-state:paused}
@media(max-width:640px){.presentation-note{font-size:12px;padding:.45rem}.presentation-sequence.reduced{gap:4px;padding:4px}.presentation-state-label{font-size:11px;padding:.2rem}}

.presentation-replay-host{position:absolute!important;inset:3% auto 12% 50%!important;width:auto!important;height:85%!important;aspect-ratio:880/1168;transform:translateX(-50%)}
.presentation-replay-host .ghost-onset{z-index:4;top:0;bottom:auto;background:#f3eddf;color:#183d41;padding:.4rem}
.presentation-replay-close{position:absolute;bottom:2%;left:50%;transform:translateX(-50%);padding:.6rem 1.5rem;background:#f3eddf;color:#183d41;border:1px solid #688486;font:inherit}

/* A static evidence sequence needs reading space on a narrow screen. The
   ordinary 16:9 comic thumbnail would leave almost no room for four captions. */
@media(max-width:640px){
  .stage-frame:has(.presentation-sequence.reduced){aspect-ratio:auto;height:calc(100dvh - var(--hb-h,54px));width:100%;max-height:none;margin:var(--hb-h,54px) auto 0}
  .stage-frame:has(.presentation-sequence.reduced) .comic-page{inset:0;width:100%;padding:0;transform:none!important}
  .stage-frame:has(.presentation-sequence.reduced) .comic-frame.read{display:none}
  .stage-frame:has(.presentation-sequence.reduced) .comic-frame:has(.presentation-sequence.reduced){position:absolute;inset:8px 8px 31%;width:auto!important;height:auto!important;aspect-ratio:auto!important;max-width:none;max-height:none;flex:none;transform:none!important}
  .stage-frame:has(.presentation-sequence.reduced) .panel-slot:has(.presentation-sequence.reduced){inset:8px 8px 31%}
  .stage-frame:has(.presentation-sequence.reduced) .lb-caption{left:5%!important;right:5%!important;width:auto!important;top:auto!important;bottom:3%!important;transform:none!important;max-height:25%;overflow:auto}
  .stage-frame:has(.presentation-sequence.reduced) .presentation-state-label{font-size:13px;line-height:1.35}
  .stage-frame:has(.presentation-sequence.reduced) .presentation-note{font-size:13px;line-height:1.4}
}

/* Keep a cue's portrait visible while its physical description has enough
   width to read. The phone's short 16:9 stage must not size its text column. */
/* A spoken sequence inherits its real preceding panel's comic host. Reserve
   that host above a separate caption region, for both moving cuts and the
   reduced-motion evidence sheet. A timed plate must not collapse the stage
   back to a 16:9 strip or inherit the desktop's translated comic frame. */
@media(max-width:640px) and (orientation:portrait){
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading){
    --phone-sequence-art-height:min(52dvh,calc(100dvh - var(--hb-h,44px) - 200px));
    width:100%;height:calc(100dvh - var(--hb-h,44px));max-height:none;
    aspect-ratio:auto;margin:var(--hb-h,44px) auto 0;
  }
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .comic-page{
    inset:0;width:100%;padding:0;display:block;transform:none!important;
  }
  .stage-frame:has(.presentation-sequence[data-aspect="wide"]:not(.reduced):not(.replay)):not(.cue-reading):not(.cue-insert-reading){
    --phone-sequence-art-height:min(52dvh,calc(100dvh - var(--hb-h,44px) - 200px),var(--phone-sequence-wide-height,52dvh));
  }
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) .panel-art,
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) .panel-fill,
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) .comic-art{visibility:hidden;}
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .comic-page > .comic-frame:not(:has(.presentation-sequence)),
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .panel-slot:not(:has(.presentation-sequence)){visibility:hidden;}
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .comic-page > .comic-frame.read{visibility:hidden;}
  .stage-frame:not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .comic-page > .comic-frame:has(.presentation-sequence:not(.replay)),
  .stage-frame:not(.cue-reading):not(.cue-insert-reading) > .panel-stack > .panel-slot:has(.presentation-sequence:not(.replay)),
  .stage-frame:not(.cue-reading):not(.cue-insert-reading) > .bubble-layer > .presentation-sequence:not(.replay){
    position:absolute;inset:38px 36px auto 18px;margin:0;
    width:auto!important;height:var(--phone-sequence-art-height)!important;
    aspect-ratio:auto!important;max-width:none;max-height:none;flex:none;transform:none!important;
  }
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .lb-caption{
    inset:calc(var(--phone-sequence-art-height) + 54px) 36px 24px 18px!important;
    width:auto!important;height:auto;max-height:none;margin:0;padding:0;
    transform:none!important;justify-content:flex-start;overflow:auto;
    pointer-events:auto;overscroll-behavior:contain;touch-action:pan-y;
  }
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .lb-caption .lb-text{
    flex:none;margin:0;font-size:calc(16px * var(--reading-scale,1));line-height:1.55;
  }
  .stage-frame:has(.presentation-sequence:not(.replay)):not(.cue-reading):not(.cue-insert-reading) > .lb-band{height:0;}
}

@media(max-width:640px){
  .cue-insert{top:12px;width:min(80vw,40dvh);height:auto;transform:translate(-50%,0) scale(.965)}
  .cue-insert.in{transform:translate(-50%,0) scale(1)}
  .cue-note.on-card{top:calc(100% + 8px);left:50%;right:auto;width:min(90vw,24rem);transform:translateX(-50%);border-width:1px;padding:.65rem .85rem;box-shadow:0 4px 12px rgba(0,0,0,.42)}
}

/* In-place observations keep the original plate and spotlight. On a phone
   their paper note belongs beneath the evidence, not inside a 90px column. */
@media(max-width:640px){
  .stage-frame.cue-reading{aspect-ratio:auto;height:calc(100dvh - var(--hb-h,54px));width:100%;max-height:none;margin:var(--hb-h,54px) auto 0}
  .cue-reading-layout{position:absolute;inset:8px 12px 34px;z-index:6;display:grid;grid-template-rows:minmax(0,1fr) auto auto;gap:12px;background:var(--hn-ink-deep);overflow:hidden}
  .cue-reading-evidence{min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
  .cue-reading-evidence .cue-reading-carrier{position:relative!important;inset:auto!important;margin:0!important;transform:none!important;width:var(--cue-reading-width)!important;height:var(--cue-reading-height)!important;max-width:none!important;max-height:none!important;flex:none;aspect-ratio:auto!important}
  .stage-frame.cue-reading .cue-note.cue-reading-note{position:relative;inset:auto!important;transform:none!important;width:100%;max-width:none!important;max-height:34dvh;overflow:auto;pointer-events:auto;box-sizing:border-box;border-image:none;border:1px solid var(--pp-rule,#688486);border-radius:0;padding:.75rem .9rem;background:var(--pp-card,#ede6d6);filter:none;box-shadow:none}
  .stage-frame.cue-reading .cue-note.cue-reading-note .cue-note-kicker{padding:0;line-height:1.35}
  .stage-frame.cue-reading .cue-note.cue-reading-note .cue-note-text{font-size:max(14px,.875rem);line-height:1.45}
  .stage-frame.cue-reading .cue-note.cue-reading-note::before,.stage-frame.cue-reading .cue-note.cue-reading-note::after,.cue-reading-carrier .cue-note-link{display:none}
  .cue-reading-dialogue{min-width:0;max-height:22dvh;overflow:auto}
  .cue-reading-dialogue[hidden]{display:none}
  .cue-reading-dialogue>.bubble,.cue-reading-dialogue>.lb-caption,.cue-reading-dialogue>.db{position:relative!important;inset:auto!important;width:100%!important;height:auto!important;max-width:none!important;max-height:none!important;margin:0!important;transform:none!important;box-sizing:border-box;opacity:1}
  .stage-frame.cue-reading>.lb-band{display:none}
  .cue-reading-dialogue>.bubble::before,.cue-reading-dialogue>.bubble::after{display:none}
  .stage-frame.cue-reading>.cue-skip-hint{z-index:7;bottom:8px;top:auto}
  /* The same subject/cue is already on the reading sheet. Its duplicate
     pickup toast must not cover that sheet while the observation is held. */
  body:has(.stage-frame.cue-reading) #codex-drawer .cx-pings{visibility:hidden}
}

/* Insert observations: the actual evidence, full observation and current
   spoken line each keep their own area. Reading never paints over anatomy. */
.cue-insert-art{position:absolute;inset:0;overflow:hidden}
.cue-insert.full-image .cue-insert-art>img,
.cue-insert.full-image .cue-insert-art>video{object-fit:contain;}
.stage-frame.cue-insert-reading{aspect-ratio:auto;height:calc(100dvh - var(--hb-h,54px));width:100%;max-height:none;margin:var(--hb-h,54px) auto 0}
.cue-reading-layout.cue-insert-layout{position:absolute;inset:8px 12px 34px;z-index:6;display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,1fr);grid-template-rows:minmax(0,1fr) minmax(0,1fr);gap:14px 24px;background:var(--hn-ink-deep);overflow:hidden}
.cue-insert-layout>.cue-reading-evidence{grid-column:1;grid-row:1 / 3;min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
.cue-insert-layout .cue-reading-carrier{position:relative!important;inset:auto!important;margin:0!important;transform:none!important;width:var(--cue-reading-width)!important;height:var(--cue-reading-height)!important;max-width:none!important;max-height:none!important;flex:none;aspect-ratio:4 / 5!important}
.cue-insert-layout>.cue-reading-info{grid-column:2;grid-row:1;align-self:end;max-height:100%;min-height:0;overflow:auto;overscroll-behavior:contain;pointer-events:auto}
.stage-frame.cue-insert-reading .cue-note.cue-reading-note{position:relative;inset:auto!important;transform:none!important;width:100%;max-width:none!important;max-height:none;box-sizing:border-box;border:1px solid var(--pp-rule,#688486);border-image:none;border-radius:0;padding:.8rem 1rem;background:var(--pp-card,#ede6d6);filter:none;box-shadow:none;overflow:visible}
.cue-insert-reading .cue-note-kicker{padding:0;line-height:1.35}
.cue-insert-reading .cue-note-text{font-size:calc(max(14px,.95rem) * var(--reading-scale,1));line-height:1.45}
.cue-insert-reading .cue-note::before,.cue-insert-reading .cue-note::after{display:none}
.cue-insert-layout>.cue-reading-dialogue{grid-column:2;grid-row:2;align-self:start;min-width:0;max-height:100%;overflow:auto;overscroll-behavior:contain;pointer-events:auto}
.cue-insert-layout>.cue-reading-dialogue[hidden]{display:none}
.cue-insert-layout .cue-reading-dialogue>.bubble,.cue-insert-layout .cue-reading-dialogue>.lb-caption,.cue-insert-layout .cue-reading-dialogue>.db{position:relative!important;inset:auto!important;width:100%!important;height:auto!important;max-width:none!important;max-height:none!important;margin:0!important;transform:none!important;box-sizing:border-box;opacity:1;filter:none}
.cue-insert-layout .cue-reading-dialogue>.bubble::before,.cue-insert-layout .cue-reading-dialogue>.bubble::after{display:none}
.cue-insert-layout .cue-reading-info>.cue-echo,.cue-insert-layout .cue-reading-info>.weather-chip,.cue-insert-layout .cue-reading-info>.needle-chip{position:relative;inset:auto;transform:none;max-width:100%;width:auto;margin:.6rem 0 0;box-sizing:border-box}
.cue-insert-layout .cue-reading-info:focus-visible,.cue-insert-layout .cue-reading-dialogue:focus-visible{outline:2px solid var(--hn-silver,#c7d3dc);outline-offset:-2px}
.stage-frame.cue-insert-reading>.lb-band{display:none}
.stage-frame.cue-insert-reading>.cue-skip-hint{z-index:7;bottom:8px;top:auto}
body:has(.stage-frame.cue-insert-reading) #codex-drawer .cx-pings{visibility:hidden}
/* Reserve a top strip for the pause chit in either reading mode. Its old
   letterbox clearance otherwise lands halfway down a landscape note. */
.stage-frame.cue-reading>.cue-reading-layout{top:34px}
body.playback-paused .stage-frame.cue-reading::after{top:6px}
@media(max-width:640px) and (orientation:portrait){
 .cue-reading-layout.cue-insert-layout{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(150px,1fr) auto auto;gap:10px;inset:8px 12px 30px}
 .cue-insert-layout>.cue-reading-evidence{grid-column:1;grid-row:1}
 .cue-insert-layout>.cue-reading-info{grid-column:1;grid-row:2;align-self:stretch;max-height:30dvh}
 .cue-insert-layout>.cue-reading-dialogue{grid-column:1;grid-row:3;align-self:stretch;max-height:22dvh}
 .stage-frame.cue-insert-reading .cue-note.cue-reading-note{padding:.65rem .8rem}
}
@media(max-height:440px) and (orientation:landscape){
 .cue-reading-layout.cue-insert-layout{grid-template-columns:minmax(160px,.8fr) minmax(0,1.2fr);gap:10px 16px;inset:6px 10px 28px}
 .stage-frame.cue-insert-reading .cue-note.cue-reading-note{padding:.5rem .7rem}
}

/* Desktop observation sheets must clear the closed book's visible fore-edge.
   Keep both text tracks shrinkable so enlarged text wraps inside the sheet;
   each reading area already scrolls vertically when its content needs room. */
@media (min-width:641px) {
  body:not(.bare) .cue-reading-layout.cue-insert-layout {
    right:calc(var(--cxb-peek,22px) + 16px + env(safe-area-inset-right,0px));
  }
  .cue-reading-layout.cue-insert-layout {
    grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);
  }
  .cue-insert-layout .cue-reading-dialogue .lb-text {
    font-size:calc(clamp(16px,1.32vw,20px) * var(--reading-scale,1));
  }
  .cue-insert-layout > .cue-reading-info,
  .cue-insert-layout > .cue-reading-dialogue {
    min-width:0;
    overflow-wrap:anywhere;
  }
}
