/* CRAYZ Story Studio — shared player/maker styles.
   Grandma bar: tap · read · choose. Portrait = phone column;
   landscape = laptop stage. C64/Amiga registers. */
/* ---- THE CRAYZ DESK tokens: the booth's system, one palette for the app.
   Surfaces are a 4-step violet ramp; text is warm parchment; gold has
   exactly three states (idle · hover · active). Maker-only values (crew
   gradients, curtain reds) live in maker.css so they can never ship. ---- */
:root {
  /* surfaces */
  --bg:#161320; --bg-glow:#262032; --card:#221c2e; --edge:#372c40;
  --control:#2a2336; --line:#443850; --hover:#39304a; --sunken:#1c1724; --void:#0d0a13;
  /* text */
  --ink:#e6dcc8; --ink-soft:#cfc4b0; --dim:#9a8c78; --dimmest:#6f6458; --on-gold:#2a1c08;
  /* gold */
  --gold:#d8b06a; --gold-deep:#c09048; --gold-top:#d8b070; --gold-bot:#b8904a;
  --gold-bright:#e0b060; --bronze:#5c4a34; --bronze-deep:#6a5030; --brass:#8a6a34;
  /* the "on" fill for a selected pill/button — was only ever referenced as a
     var(--gold-fill, linear-gradient(...)) fallback, never actually defined,
     so anything that read it bare (.set-seg button.on) got no background at
     all: --on-gold's dark bronze text on the sunken surface underneath,
     unreadable in dark mode and merely gold-less in light mode. */
  --gold-fill: linear-gradient(var(--gold-top), var(--gold-bot));
  /* choice grammar (GREEN=progress · RED=dwell) + registers */
  --green:#7fae8e; --green-deep:#5f8d70; --red:#cf6a52; --red-deep:#93392b;
  --orange:#d8874a; --blue:#5b8dd6; --steel:#6f9fb0; --steel-deep:#547e91;
  /* the golden mean governs the box system: the frame is φ times the reading
     column, and φ times the action bar under it. Slot sizes derive the same
     way, so every box in the hall is in proportion at any viewport. */
  --phi: 1.618;
  /* arc identity (timeline strips) */
  --arc-0:#d8b06a; --arc-1:#6f9fb0; --arc-2:#b8543f; --arc-3:#7fae8e;
  --arc-4:#9a7fc0; --arc-5:#d8874a; --arc-6:#5fa9a0;
  /* type — no web fonts: offline-first, and the identity is in the
     tracking + palette, not the typeface. The story voice stays Courier. */
  --font-body:"Courier New", ui-monospace, monospace;
  --font-display:Futura, "Century Gothic", "Josefin Sans", "Avenir Next", "Trebuchet MS", sans-serif;
  --font-mono:ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-ui:system-ui, -apple-system, "Segoe UI Emoji", sans-serif;
  /* shape */
  --r-card:12px; --r-ctl:8px; --r-pill:999px; --r-chip:3px;
  --shadow-card:0 2px 10px rgba(0,0,0,.25); --track:.22em;
  /* the two values that cannot survive a light surface as literals: a hover
     "lift" is white on a dark room and must become a SHADE on a light one,
     and the corner chrome that floats over the artwork has to read against
     whatever is behind it. Everything else in this file that is written as a
     literal rgba is a scrim OVER THE PICTURE — see the note below. */
  --lift: rgba(255,255,255,.03);
  --chrome-fill: rgba(13,10,19,.72);
}

/* ---- LIGHT (S92) ------------------------------------------------------
   Eivind: *"can we also add a light mode to our story player? … if someone
   wants to read/play in light mode."* A reading surface, so the register is
   parchment and desk rather than "the dark one, inverted": warm paper, ink
   that is brown-black rather than pure black, and a gold pulled down into
   the range where it can actually be read on paper (the dark palette's
   #d8b06a is a highlight colour — on parchment it is a smear).

   WHAT DOES NOT CHANGE, and this is the load-bearing part: every scrim that
   sits OVER THE ART stays exactly as dark as it was. The caption gradient,
   the theatre wash, the cover's ramp, the text shadows — a photograph does
   not get lighter because the app did, and white text on a picture needs the
   same dark bed either way. Light mode is the ROOM, not the frame.

   The class lands on <body> in the shipped player and on #stage in the maker
   (js/player.js applySkin) — tokens inherit, so one rule dresses the player
   in both places while the workshop's own chrome, which lives outside
   #stage, is untouched by construction. */
body.skin-light, #stage.skin-light {
  --bg:#e7dfcd; --bg-glow:#f2ebda; --card:#f5efe1; --edge:#c9bda3;
  --control:#eee7d6; --line:#c0b298; --hover:#e0d7c1; --sunken:#dcd3be; --void:#f8f4ea;
  --ink:#2b2418; --ink-soft:#463b2a; --dim:#6d6049; --dimmest:#8a7c63; --on-gold:#2a1c08;
  --gold:#8a6520; --gold-deep:#6d4e17; --gold-top:#c99c47; --gold-bot:#a67a2a;
  --gold-bright:#956d23; --bronze:#7d6742; --bronze-deep:#6b5734; --brass:#7a5c2c;
  --gold-fill: linear-gradient(var(--gold-top), var(--gold-bot));
  /* the choice grammar has to keep MEANING at 4.5:1 on paper, so GREEN and
     RED darken rather than merely shifting hue */
  --green:#3d7a55; --green-deep:#2f6242; --red:#a83a22; --red-deep:#822a17;
  --orange:#a35c1c; --blue:#2f5fa8; --steel:#3f6f80; --steel-deep:#345b69;
  --lift: rgba(43,36,24,.055);
  --chrome-fill: rgba(255,255,255,.8);
  --shadow-card:0 2px 10px rgba(58,46,24,.14);
}
/* colour inherits as a COMPUTED value, so re-pointing --ink on #stage does not
   re-colour text that already inherited the maker body's pale night ink — the
   card read #e6dcc8 on #f5efe1 (1.2:1). Re-resolve it where the skin lands. */
#stage.skin-light { color: var(--ink); }

/* THE FRAME KEEPS THE NIGHT PALETTE (S92). Two places put UI ON TOP OF THE
   ARTWORK — the choices when they float over the panel (theatre and cap
   registers) and the cover — and both are built as pale text on a dark glass
   plate. Flip the tokens under them and `color: var(--ink)` becomes dark
   brown on a dark pane: the light mode would have shipped with its own
   choices unreadable, in exactly the two registers a reader spends most time
   in.
   Restating the dark values on those two subtrees is what makes the rest of
   the light palette free — no over-art rule needs to know the skin exists,
   and the next control someone drops over a picture inherits the right
   answer without being told. Scope is deliberate: `#panel > #choices` is
   over the art, `#stage > #choices` is on the page, and only the first is
   in here. */
body.skin-light #panel, #stage.skin-light #panel,
body.skin-light .full, #stage.skin-light .full {
  --ink:#e6dcc8; --ink-soft:#cfc4b0; --dim:#9a8c78; --dimmest:#6f6458;
  --gold:#d8b06a; --gold-deep:#c09048; --gold-top:#d8b070; --gold-bot:#b8904a;
  --gold-bright:#e0b060; --bronze:#5c4a34; --bronze-deep:#6a5030; --brass:#8a6a34;
  --green:#7fae8e; --green-deep:#5f8d70; --red:#cf6a52; --red-deep:#93392b;
  --orange:#d8874a; --blue:#5b8dd6; --steel:#6f9fb0; --steel-deep:#547e91;
  --card:#221c2e; --edge:#372c40; --control:#2a2336; --line:#443850;
  --hover:#39304a; --sunken:#1c1724; --void:#0d0a13;
  --lift: rgba(255,255,255,.03);
}
/* a story's own backdrop (S92, meta.skin.chrome). The image itself is set
   inline by js/player.js applySkin — this is only how it is laid: covering,
   centred, and FIXED so it stays put while a long page scrolls over it. A
   backdrop that scrolls is a very long picture; a backdrop that holds still
   is a room you are reading in. */
body.has-chrome, #stage.has-chrome {
  background-size: cover; background-position: center; background-repeat: no-repeat;
  background-attachment: fixed;
}
* { box-sizing: border-box; margin: 0; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body { background: var(--bg); color: var(--ink);
       font-family: "Courier New", ui-monospace, monospace; }
#stage { max-width: 880px; margin: 0 auto; min-height: 100%; position: relative;
         display: flex; flex-direction: column; padding: 10px; gap: 10px; }
/* [hidden] must actually hide. An id selector setting `display` outranks the
   browser's own `[hidden] { display: none }` (1,0,0 beats 0,1,0), so every
   element we toggle by the hidden ATTRIBUTE needs saying twice. Without this
   the maker's LIBRARY had the whole stage rendering underneath it. */
#stage[hidden], #panel[hidden], #card[hidden], #choices[hidden] { display: none; }
#panel { position: relative; border: 3px solid var(--line); border-radius: 10px;
         overflow: hidden; background: #000; height: 52vh; flex: none;
         image-rendering: pixelated; user-select: none; }
@media (orientation: landscape) and (min-width: 700px) {
  #panel { height: 58vh; }
  #stage { padding: 18px; }
}
#panel img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
/* ---- FRAME TEST: does this art survive a different aspect / crop? ----
   orientation sets the frame, fit says crop-to-fill vs show-the-whole-thing,
   zoom shrinks the frame so you can see it as a thumbnail. All opt-in via
   body classes, so nothing changes for a plain player. */
/* height drives the box and aspect-ratio derives the width — the reverse
   (width:100% + max-height) silently clamps and the ratio never applies */
body.ori-land #panel, body.ori-port #panel, body.ori-sq #panel {
  height: calc((100vh - 250px) * var(--zoom, 1));
  width: auto; max-width: 100%; margin: 0 auto; flex: none;
}
body.ori-land #panel { aspect-ratio: 3 / 2; }
body.ori-port #panel { aspect-ratio: 2 / 3; }
body.ori-sq   #panel { aspect-ratio: 1 / 1; }
body.fit-whole #panel img { object-fit: contain; background: #000; }
/* THEATER: play mode with every tool panel closed — the image owns the screen */
body.theater #stage { min-height: calc(100vh - 56px); justify-content: center; max-width: 1600px; }
body.theater #panel { max-height: calc(100vh - 210px); }
/* CRT whisper + panel entrance — the film feel */
#panel::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: repeating-linear-gradient(0deg, transparent 0 2px, rgba(0,0,0,.13) 2px 3px); }
/* EMULATION → CRT screen (off), player.js's #setCrt. One rule covers every
   skin's own #panel::after too (skins/TREASURE.css etc.) — the extra class
   here always outweighs their plain #id specificity. */
body.crt-off #panel::after { display: none; }
#panel img, #panel .ph { animation: panelIn .3s ease-out both; }
/* THE CINE REEL (S119) — stills flipped fast enough to read as motion.
   Every frame is stacked in the panel and one wears .on; the flip is a style
   change and nothing more. The panelIn fade above MUST NOT apply: a .3s
   ease-in on a 160ms frame cross-dissolves the burst into mush, which is
   exactly the soup a hard cut exists to avoid. A reel cuts. */
#panel img.cine-frame { position: absolute; inset: 0; animation: none;
  visibility: hidden; }
#panel img.cine-frame.on { visibility: visible; }
/* the choice waits for the reel to land. Not `hidden` — the buttons keep
   their box so the page does not reflow when they arrive, and the reveal is
   the same choiceIn the rest of the studio uses. */
#choices.cine-hold { opacity: 0; pointer-events: none; }
@keyframes panelIn { from { opacity: 0; } }
.ph { width: 100%; height: 100%; display: flex; align-items: center;
      justify-content: center; text-align: center; padding: 14px;
      letter-spacing: 3px; color: #ffffffcc; font-weight: bold;
      text-shadow: 0 2px 8px #000c; font-size: clamp(14px, 3.4vw, 24px); }
/* an authored overlay shares the weather's layer (S241, js/weather.js) and is
   appended after it on every render, so a prop hung in the frame stays in front
   of the rain falling past it */
.overlay { position: absolute; image-rendering: pixelated; z-index: 1; }
#tapHint { position: absolute; right: 12px; bottom: 10px; color: #fffc;
           font-size: 13px; animation: pulse 1.4s infinite; pointer-events: none;
           text-shadow: 0 1px 4px #000; }
@keyframes pulse { 50% { opacity: .25; } }
#card { background: var(--card); border: 2px solid var(--line); border-radius: 10px;
        padding: 14px 16px; flex: none; }
#speaker { color: var(--gold); font-size: 12px; letter-spacing: 2px;
           text-transform: uppercase; margin-bottom: 6px; }
#text { font-size: clamp(15px, 2.6vw, 18px); line-height: 1.5; white-space: pre-wrap;
        animation: cardIn .24s ease-out both; }
#text em { color: var(--gold); font-style: normal; }
@keyframes cardIn { from { opacity: 0; transform: translateY(4px); } }
#aside { color: var(--dim); font-size: 13px; font-style: italic; margin-top: 8px;
         animation: cardIn .4s ease-out .25s both; }
#aside:empty { display: none; }
#choices { display: flex; flex-direction: column; gap: 8px; }
/* choices follow the text (S28): #choices lives INSIDE #card, after the
   prose — options read as the page's own last lines, like ink. The caption
   card is pointer-events:none so taps fall through to the panel; the choices
   within it must win their clicks back. */
#card #choices { margin-top: 14px; }
#card #choices:empty { display: none; }
#stage.cap #card #choices { pointer-events: auto; }
/* options are PART of the story (S28, the inky register): quiet text lines
   after the prose, all kinds rendered EQUAL — per the studio lesson
   "GREEN/RED is grammar, not chrome". kind (green/red/orange/blue) stays on
   the DOM as branch grammar; the maker's reel and editors color by it. */
#choices button { min-height: 44px; cursor: pointer; text-align: left;
  font: inherit; font-size: clamp(14px, 2.4vw, 17px); padding: 8px 12px;
  background: transparent; color: var(--ink);
  border: 0; border-left: 2px solid var(--line); border-radius: 0; }
#choices button::before { content: '▸  '; color: var(--gold); }
/* THE SCUMM GATE (js/gate.js): what the pictures gave up, and the case's count */
#choices .finds-line { color: var(--gold); font-size: .85em; letter-spacing: .04em; padding: 4px 12px; }
#choices button:hover { border-left-color: var(--gold); background: var(--lift); }
#choices button:active { transform: translateY(1px); }
#choices > button { animation: choiceIn .22s ease-out backwards; }
#choices > button:nth-child(2) { animation-delay: .07s; }
#choices > button:nth-child(3) { animation-delay: .14s; }
@keyframes choiceIn { from { opacity: 0; transform: translateY(6px); } }
/* A PAUSE AND AN ORNAMENT (S215). Eivind: "in our text panel, let there be a
   subtle pause and ornament between the text and the choices." On the page
   (the card register only — the theatre's choices float on the picture and
   have their scrim) the prose lands, a breath, then a small rule with a
   diamond fades in, then the options arrive under it. The ornament is the
   card's own ::before, so nothing in the markup changes, and the pause is a
   longer choiceIn delay — held off in the maker, which redraws on every
   keystroke and would stutter on it. */
#card #choices:not(:empty)::before { content: '\25C6'; align-self: center; width: clamp(90px, 22%, 160px); margin: 4px 0 2px;
  text-align: center; font-size: 8px; line-height: 1; color: rgba(216,176,106,.7); letter-spacing: 0;
  background: linear-gradient(to right, rgba(216,176,106,0), rgba(216,176,106,.45)) left center / calc(50% - 12px) 1px no-repeat,
              linear-gradient(to left, rgba(216,176,106,0), rgba(216,176,106,.45)) right center / calc(50% - 12px) 1px no-repeat;
  animation: cardIn .5s ease-out .35s both; }
body:not(.maker) #card #choices > button { animation-delay: .7s; }
body:not(.maker) #card #choices > button:nth-child(3) { animation-delay: .77s; }
body:not(.maker) #card #choices > button:nth-child(4) { animation-delay: .84s; }
body:not(.maker) #card #choices > button:nth-child(n+5) { animation-delay: .91s; }
@media (prefers-reduced-motion: reduce) { #card #choices:not(:empty)::before, #card #choices > button { animation: none; } }
#choices .menu { animation: cardIn .35s ease-out both; }
@media (prefers-reduced-motion: reduce) {
  #panel img, #panel .ph, #text, #aside, #choices > button, #choices .menu, #tapHint {
    animation: none !important; }
}
/* the no-tray fallback: when a story doesn't opt into the tray, this is the
   only place carried items show at all — plain text, tucked in the stage's
   corner. Was a child of the footer #hud (now removed); kept its old inline
   styling (dim, 12px) since #stage is `position: relative` already. */
#satchel { position: absolute; left: 10px; bottom: 8px; z-index: 3;
  color: var(--dim); font-size: 12px; pointer-events: none; }
/* title + fate cards */
.full { position: fixed; inset: 0; background: var(--bg); display: flex;
        align-items: center; justify-content: center; padding: 20px; z-index: 10; }
.full[hidden] { display: none; }
.menu { max-width: 420px; width: 100%; background: var(--card);
        border: 3px solid var(--line); border-radius: 12px; padding: 22px;
        display: flex; flex-direction: column; gap: 12px; text-align: center; }
.menu h1 { font-size: clamp(18px, 4vw, 24px); color: var(--gold); letter-spacing: 1px; }
.menu .sub { color: var(--dim); font-size: 12px; }
.menu button { min-height: 50px; font: inherit; font-size: 16px; cursor: pointer;
               border-radius: 10px; border: 2px solid var(--gold);
               background: var(--control); color: var(--gold); }
.menu label { display: flex; justify-content: space-between; align-items: center;
              color: var(--dim); font-size: 13px; }
.menu input[type=text] { width: 110px; background: var(--control); color: var(--ink);
    border: 1px solid var(--line); border-radius: 6px; padding: 4px 6px; font: inherit; }
table.fate { margin: 0 auto; color: var(--ink); font-size: 14px; border-spacing: 12px 4px; }
table.fate td:first-child { color: var(--dim); text-align: right; }

/* --- CINEMA SKIN (v0.7) --- */

/* THE MATTE BELONGS TO THE PICTURE (S75).
   It used to be two absolutely-positioned black bars across the top and
   bottom of #stage at z-index 5 — laid OVER whatever happened to be there.
   In the v0.7 stage that was only ever the frame, so it read as letterboxing.
   In the theatre hall it spans a two-column grid, so it was cutting a black
   line across the top of the artwork AND across the prose beside it: not a
   matte, an occlusion, and visibly superimposed rather than part of the UI.

   Letterboxing is a property of an IMAGE — bars are what you get when a wide
   picture sits in a narrower frame. So it lives on the frame now, as its own
   border. box-sizing means the border eats into the picture exactly the way a
   real matte does, the frame's outer geometry never moves, and nothing is
   drawn on top of anything. */
/* THE CINEMA SKIN IS GONE (S92). It lived here: a 14px letterbox, animated
   film grain, a vignette, a 22-second 6% Ken Burns drift and a half-second
   crossfade, all behind one switch on the cover. Eivind: *"it only adds small
   letterboxes and ken burns that hardly do anything cinematic, but still
   annoyingly resets every page."*

   He is right about the arithmetic. A 6% zoom over 22 seconds is 0.27% a
   second — under the threshold of noticing — on a page most people leave in
   eight. What everyone DID notice was the restart, because the animation
   began again on every render, and in the maker that meant every keystroke.
   A picture that holds still is not a lesser version of one that drifts; it
   is the panel of a comic strip, which is what these games are.

   The crossfade survives, ungated: it is how one picture becomes another,
   not a filter — and since S92 it only runs when the picture actually
   CHANGES (js/player.js render), so nothing fades on a keystroke any more. */
@keyframes panelOut { from { opacity: 1; } }
#panel img.panel-out { position: absolute; inset: 0; opacity: 0;
  animation: panelOut .32s ease-in both; }
/* LIVE 3D (S282) — the scene room's rig over the still, sized by js/live3d.js
   to crop the way the still does. Under the sky (z 1); the panel takes the
   pointer, so the canvas never has to. */
#panel canvas.live3d { position: absolute; z-index: 0; pointer-events: none;
  opacity: 0; transition: opacity .45s ease-out; }
#panel canvas.live3d.on { opacity: 1; }
/* EFFECT ZONES (S283) — the rain behind the window, the sparks off the hearth,
   on the still (js/fx/panel-zones.js). Native size, blown up pixelated and
   cropped like the still by the script; its light ADDS to the picture. Under
   the sky, and under the live 3D canvas that draws the same zones itself. */
#panel canvas.zones-2d { position: absolute; z-index: 0; pointer-events: none;
  image-rendering: pixelated; mix-blend-mode: plus-lighter; }

/* arc title card — a chapter announcing itself. It used to be gated on the
   cinema skin; S92 kept it, because it is story furniture rather than a
   filter. Reuses the existing cardIn keyframe (line 41). */
/* THE ACT CARD. It was parked in S92 because it "covers the frame and EATS
   THE NEXT TAP", which in a tap-to-continue game silently costs a turn at
   every chapter boundary. The fix is not to gate it — it is `pointer-events:
   none`: the card is a CAPTION, not a dialog. It cannot be tapped, so it
   cannot swallow one, and it leaves on its own. */
.arc-card { position: absolute; inset: 0; z-index: 6; display: flex; align-items: center;
  justify-content: center; text-align: center; padding: 20px; background: rgba(10,9,16,.62);
  animation: cardIn .4s ease-out both; pointer-events: none; }
.arc-card h2 { color: var(--gold); font-size: clamp(18px, 4.6vw, 28px); letter-spacing: 3px;
  text-transform: uppercase; border-top: 1px solid var(--gold); border-bottom: 1px solid var(--gold);
  padding: 12px 22px; }
.arc-card.out { animation: arcCardOut .35s ease-in both; }
@keyframes arcCardOut { to { opacity: 0; } }

/* reduced motion — selectors mirror the rules above so specificity ties
   resolve correctly (this block must stay last: equal specificity, later wins) */
@media (prefers-reduced-motion: reduce) {
  #panel img.panel-out, .arc-card, .arc-card.out {
    animation: none !important; }
}

/* THE STAGE (Phase 3) — cards, beats, the held moment.
   The two combatants face each other as EQUALS: one row, equal widths, a
   clash mark between them. Stacked or offset cards read as a list of two
   things; side by side reads as a contest, which is what it is.
   Choices keep the story's own left-aligned grammar — a centred button in a
   game whose every other option is a left-aligned line looks like a different
   app. */
.stagebox { display: flex; flex-direction: column; gap: 10px; align-items: stretch;
  padding: 10px 2px; cursor: pointer; }
.st-title { color: var(--gold); letter-spacing: 3px; font-size: 13px; text-align: center; }
.st-cards { display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px;
  align-items: stretch; justify-items: stretch; }
.st-cards.solo { grid-template-columns: 1fr; }
/* the clash: a struck mark between the two, not decoration — it is where the
   beat lands, and it pulses as each one resolves */
.st-clash { align-self: center; color: var(--gold-deep); font: 15px/1 monospace;
  letter-spacing: 1px; opacity: .55; padding: 0 2px; }
.st-cards.strike .st-clash { animation: st-clash .5s ease-out; }
@keyframes st-clash { 40% { opacity: 1; transform: scale(1.5); color: var(--gold); } }
.stagecard { background: var(--card); border: 2px solid var(--bronze); border-radius: 10px;
  padding: 10px 12px; font-size: 12px; display: flex; flex-direction: column; gap: 3px;
  transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease; }
.stagecard.hero { border-color: var(--gold); }
/* a card that just lost the beat takes the hit; one that won leans in */
.stagecard.hurt { animation: sc-hurt .45s ease-out; }
.stagecard.won  { animation: sc-won .45s ease-out; }
@keyframes sc-hurt { 30% { transform: translateX(5px); border-color: var(--red);
  box-shadow: 0 0 14px rgba(207,106,82,.35); } }
@keyframes sc-won  { 30% { transform: translateY(-5px); box-shadow: 0 0 16px rgba(216,176,106,.3); } }
.st-cards.dealt .stagecard { animation: sc-deal .5s ease-out backwards; }
.st-cards.dealt .stagecard:nth-child(3) { animation-delay: .18s; }
@keyframes sc-deal { from { transform: translateY(-26px) scale(.9); opacity: 0; } }
.sc-name { color: var(--gold); letter-spacing: 2px; margin-bottom: 4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sc-stat { display: flex; justify-content: space-between; gap: 10px; color: var(--dim); }
.sc-pips { color: var(--gold); letter-spacing: 1px; }
/* a spent pip stays visible but dimmed — you can see what the contest cost */
.sc-pips .spent { color: var(--bronze); opacity: .55; }
.sc-text { color: var(--dim); font-style: italic; margin-top: 5px; }
.st-gm { color: var(--ink); }
.st-result { color: var(--gold); letter-spacing: 2px; }
.st-qte { color: var(--gold); font-size: 22px; letter-spacing: 4px; text-align: center;
  animation: st-flash .45s steps(2) infinite; }
@keyframes st-flash { 50% { opacity: .25; } }
/* ---- THE DRAMA REGISTER — a contest a reader can follow ----------------
   Two faces, and one thing moving between them. No stat rows, because in an
   authored story the numbers are not a decision the reader made and cannot
   act on; who is winning is. A face DIMS and desaturates as its side gives
   ground, so you read the fight off the portraits themselves. */
.st-cards.st-drama { grid-template-columns: 1fr auto 1fr; align-items: center; }
.stageface { display: flex; flex-direction: column; align-items: center; gap: 6px;
  --hurt: 0; }
.stageface .sf-pic { position: relative; width: min(112px, 26vw); aspect-ratio: 1;
  border: 2px solid var(--bronze); border-radius: 10px; overflow: hidden;
  background: var(--sunken); display: flex; align-items: center; justify-content: center;
  transition: filter .45s ease, border-color .45s ease, opacity .45s ease; }
.stageface.hero .sf-pic { border-color: var(--gold); }
.stageface .sf-pic img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
.stageface .sf-ph { color: var(--dim); font: 600 20px/1 monospace; }
/* giving ground shows on the face: it greys and pulls back */
.stageface[style*="--hurt"] .sf-pic {
  filter: grayscale(calc(var(--hurt) * .85)) brightness(calc(1 - var(--hurt) * .35));
  opacity: calc(1 - var(--hurt) * .25);
}
.stageface .sf-name { color: var(--ink-soft); font: 11px/1.4 monospace;
  letter-spacing: .1em; text-align: center; max-width: 13ch; }
.st-drama .st-clash { font-size: 20px; opacity: .7; }

/* THE GROUND — one marker, moving. The old row of "6 v 4" tallies never read
   as a match because nothing carried between beats; a single track that gets
   shoved one way or the other is a contest at a glance. In an RPG this is
   where a real HP bar binds — same shape, different pool. */
.st-ground { display: flex; align-items: center; gap: 10px; margin: 2px 0 4px; }
.sg-end { color: var(--dim); font: 10px/1.4 monospace; letter-spacing: .12em;
  text-transform: uppercase; white-space: nowrap; max-width: 9em;
  overflow: hidden; text-overflow: ellipsis; }
.sg-track { position: relative; flex: 1 1 auto; height: 8px; border-radius: 4px;
  background: linear-gradient(90deg, rgba(127,174,142,.22), rgba(0,0,0,.25) 50%, rgba(207,106,82,.22));
  border: 1px solid var(--line); }
.sg-track::before { content: ''; position: absolute; left: 50%; top: -3px; bottom: -3px;
  width: 1px; background: var(--edge); }
.sg-track > i { position: absolute; top: 50%; width: 13px; height: 13px; margin: -7px 0 0 -7px;
  border-radius: 50%; background: var(--gold); box-shadow: 0 0 9px rgba(216,176,106,.5);
  transition: left .5s cubic-bezier(.4,0,.2,1); }
.st-ground.up .sg-track > i { background: var(--green); box-shadow: 0 0 9px rgba(127,174,142,.5); }
.st-ground.down .sg-track > i { background: var(--red); box-shadow: 0 0 9px rgba(207,106,82,.5); }

.st-beats { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
.st-beat { display: flex; align-items: baseline; gap: 8px; }
.st-beat .st-mark { font-size: 11px; }
.st-beat.won .st-mark { color: var(--green); }
.st-beat.lost .st-mark { color: var(--red); }
.st-beat .st-score { margin-left: auto; color: var(--dimmest); font: 11px/1 monospace; }
/* POOLS AS BARS. A bar reads as something that can RUN OUT; pips read as a
   rating. That is why a turn-based fight needs bars and a stat sheet does
   not — and why the same card serves both once it can draw either. */
.sc-bar { display: grid; grid-template-columns: 26px 1fr auto; align-items: center;
  gap: 7px; margin-top: 4px; }
.sc-bl { color: var(--dim); font: 9px/1 monospace; letter-spacing: .1em; }
.sc-btrack { height: 7px; border-radius: 4px; background: var(--sunken);
  border: 1px solid var(--line); overflow: hidden; }
.sc-btrack > i { display: block; height: 100%; transition: width .5s cubic-bezier(.4,0,.2,1); }
.sc-bar.hp .sc-btrack > i { background: linear-gradient(90deg, var(--red-deep), var(--red)); }
.sc-bar.mp .sc-btrack > i { background: linear-gradient(90deg, var(--steel-deep), var(--steel)); }
.sc-bn { color: var(--dimmest); font: 9px/1 monospace; }
/* cost / power, for a card battler */
.sc-badge { float: right; display: inline-flex; gap: 4px; }
.sc-badge b, .sc-badge i { font: 10px/1 monospace; font-style: normal; padding: 2px 5px;
  border-radius: 3px; }
.sc-badge b { background: var(--steel-deep); color: var(--ink); }
.sc-badge i { background: var(--bronze-deep); color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .sc-btrack > i { transition: none; } }

/* RESOLVE on the card: what the contest has cost this side so far */
.sc-res { display: flex; justify-content: space-between; gap: 10px; margin-top: 5px;
  padding-top: 5px; border-top: 1px solid var(--edge); color: var(--dim); }
@media (prefers-reduced-motion: reduce) { .sg-track > i { transition: none; } }
.st-hint { color: var(--dim); font-size: 11px; letter-spacing: 2px; }
/* choices here obey the same grammar as every other choice in the game */
.stagebox button { font: inherit; font-size: clamp(14px, 2.4vw, 17px); min-height: 44px;
  padding: 8px 12px; text-align: left; cursor: pointer;
  background: transparent; color: var(--ink);
  border: 0; border-left: 2px solid var(--line); border-radius: 0; }
.stagebox button::before { content: '▸  '; color: var(--gold); }
.stagebox button:hover { border-left-color: var(--gold); background: var(--lift); }
.stagebox button.green { border-left-color: var(--green); }
.stagebox button.red { border-left-color: var(--red); }
@media (prefers-reduced-motion: reduce) {
  .st-cards.dealt .stagecard, .st-qte,
  .stagecard.hurt, .stagecard.won, .st-cards.strike .st-clash { animation: none; }
}

/* ---- CINEMA-CAPTION register (S24) ------------------------------------
   meta.present:"caption" + a short page ⇒ the text rides in the frame as a
   film caption instead of a card below it, so the image is full-bleed and
   free to move (Ken Burns has nothing to fight). Long pages auto-fall back
   to the card — the class is recomputed per page in player.js:render().
   No new DOM: #card IS the caption, pulled up over the image by a negative
   margin and made click-through so tap-to-continue still lands on #panel. */
#stage.cap #card {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 6;
  border: 0; border-radius: 0; pointer-events: none;
  padding: 54px 22px 16px; text-align: center;
  background: linear-gradient(to bottom,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.45) 34%, rgba(0,0,0,.82) 70%, rgba(0,0,0,.92) 100%);
}
#stage.cap #speaker {
  display: inline-block; border-bottom: 1px solid var(--gold);
  padding-bottom: 4px; margin-bottom: 8px;
}
#stage.cap #text {
  max-width: 34em; margin: 0 auto; text-shadow: 0 2px 8px #000, 0 0 2px #000;
}
#stage.cap #aside { max-width: 34em; margin: 8px auto 0; text-shadow: 0 2px 6px #000; }
/* the frame gets the room the card gave up */
#stage.cap #panel { height: 64vh; }
body.theater #stage.cap #panel { height: calc(100vh - 150px); }
@media (orientation: landscape) and (min-width: 700px) { #stage.cap #panel { height: 68vh; } }

/* ---- THE STACKED COLUMN, ALSO CONTAINED (S69) -------------------------
   The theatre hall below promises "the page never grows" — but only from
   900px landscape up. Everywhere else the column was free to run past the
   viewport, so on an iPad in portrait a long beat pushed the picture off
   the top of the screen and the whole game scrolled: 800x900 overflowed by
   134px, 834x1112 by 32. A story game must never make you scroll to see
   the thing you are reading about.
   Same contract as the hall, one row lower: the stage is exactly the
   viewport, the furniture holds still, and the PROSE owns the overflow.
   Scoped off the maker, whose player is a row inside a workspace and takes
   its height from the grid (body.ws-play, handled in the hall block). */
/* ONE NUMBER FOR THE ROW ABOVE THE HALL (S293). S208 gave the player a
   masthead and the hall became "the viewport LESS that row" — written as a
   literal 84px in the height AND again as the 74 inside --frame-h. Then S225
   framed the player in the house and hid .pl-head, but `.pl-head + #stage`
   goes on matching a display:none sibling, so the hall kept subtracting a row
   nobody draws: measured at 1440×900 housed, 72px of dead cloth under the
   picture, and a frame 47px shorter than the hall could carry. The allowance
   is a variable now, set in one place and read by both, so hiding the row is
   enough to give the hall its height back. */
body:not(.maker) #stage { --pl-head-h: 0px; height: calc(100dvh - var(--pl-head-h)); min-height: 0; overflow: hidden; }
/* under the player's masthead (S208) the hall is the viewport LESS that row */
body:not(.maker) .pl-head + #stage { --pl-head-h: 84px; }
/* …unless the house drew the row instead (S225): then there is nothing above
   the hall and the hall is the whole frame */
html.st-housed body:not(.maker) #stage { --pl-head-h: 0px; }
/* The prose is the ONE flexible row: it takes what is left and scrolls
   inside itself. Everything else is furniture and holds its size — without
   the explicit flex:none the frame and the bar shrink to make room for a
   long beat, which is the same drift by another route. */
/* shrink factor 4: the prose gives ground FIRST, because it scrolls and the
   picture does not. A comic strip that loses its picture to make room for
   text has the priority backwards. */
body:not(.maker) #card { flex: 1 4 auto; min-height: 8vh; overflow-y: auto; scrollbar-width: thin; }
body:not(.maker) #choices, body:not(.maker) #tray { flex: none; }
/* On a phone the fixed 52vh frame plus the action bar plus two choices is
   already taller than the screen, so the frame must be allowed to give
   ground — it crops, which this layout has always accepted, rather than
   climbing out of view. It shrinks before the prose does (basis 52vh vs the
   card's content height) and never below a third of the screen. */
body:not(.maker) #panel { flex: 0 1 auto; min-height: 30vh; }
/* the caption register floats over the frame — it has no row to shrink */
body:not(.maker) #stage.cap #card { flex: none; min-height: auto; overflow: visible; }

/* ---- THE THEATRE HALL (S27) ------------------------------------------
   A graphic novel told on a stage: the frame LEFT, the prose RIGHT, the
   choices under the image. The hall is fixed furniture — panel, choices
   and hud never reflow and the page never grows. The prose column owns
   its own overflow, fading out with a ▾ more mark so a long beat is
   obviously long instead of silently cut.
   Wide landscape only; portrait keeps the stacked column. */
@media (min-width: 900px) and (orientation: landscape) {
  body:not(.maker) #stage, body.ws-play #stage {
    display: grid;
    /* golden columns. NOT calc(var(--phi) * 1fr): flexible lengths are
       illegal inside calc(), and the whole declaration dies silently. */
    grid-template-columns: minmax(0, 1.618fr) minmax(300px, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    align-content: start; gap: 12px 22px;
    max-width: 1600px; padding: 18px 20px;
    /* The frame FILLS its column — it used to be aspect-locked and so ended
       short of the prose, leaving a dead strip between picture and page. Now
       the box is width-full and height-golden, and the art crops to fit. */
    /* frame·φ = frame + frame/φ, so the frame is the hall over φ — plus the
       10px of slack the hall was tuned with at S27. --pl-head-h defaults to
       the masthead's 84 for body.ws-play, which never sets it. */
    --frame-h: min(calc((100dvh - var(--pl-head-h, 84px) + 10px) / var(--phi)), 62dvh);
  }
  body:not(.maker) #stage { height: calc(100dvh - var(--pl-head-h)); }
  /* the maker's row 3 is already minmax(0,1fr) — take all of it */
  body.ws-play #stage { height: 100%; min-height: 0; padding: 6px 8px 4px; }
  body.ws-play #stage { --frame-h: min(calc((100dvh - 108px) / var(--phi)), 60dvh); }

  /* the frame: aspect-locked, and sized by whichever runs out first —
     the column's width or the hall's height. No crop surprises. */
  /* ONE golden frame height governs the left column: frame + bar + chrome
     fills the hall exactly, because frame·φ = frame + frame/φ. Height drives
     the box and aspect-ratio derives the width (style.css:56) — the
     width-driven form silently overflowed the bar off-screen. */
  body:not(.maker) #stage > #panel, body.ws-play #stage > #panel {
    grid-column: 1; grid-row: 1; flex: none; margin: 0;
    height: var(--frame-h); width: 100%; aspect-ratio: auto;
  }
  /* THE ACTION BAR (S29) takes grid row 2, column 1 — the slot under the
     frame. Golden: the frame is φ times the bar's height. */
  body:not(.maker) #stage > #tray, body.ws-play #stage > #tray {
    grid-column: 1; grid-row: 2; align-self: stretch;
    /* DOWN TO THE EDGE. The bar stopped where its content stopped, leaving a
       band of nothing between the inventory and the bottom of the screen —
       which read as the layout having given up rather than as breathing room. */
    min-height: 0; height: 100%; overflow: hidden;
  }
  /* the reading column — the only thing in the hall that moves */
  /* the reading column keeps its full height beside both frame and bar —
     these are story books, after all */
  body:not(.maker) #stage > #card, body.ws-play #stage > #card {
    grid-column: 2; grid-row: 1 / span 2;
    overflow-y: auto; min-height: 0; scrollbar-width: thin; padding: 16px 18px;
  }
  #card.more {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 52px), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 52px), transparent);
  }
  #moreHint {
    position: absolute; right: 26px; bottom: 22px; z-index: 7;
    color: var(--gold); font-size: 11px; letter-spacing: .2em;
    pointer-events: none; animation: pulse 1.8s infinite;
  }
  #moreHint[hidden] { display: none; }
}
@media (prefers-reduced-motion: reduce) { #moreHint { animation: none; } }

/* ---- THE ACTION BAR (S29) ---------------------------------------------
   The genre switch under the frame: cast heads (who is in the scene, even
   when the art doesn't show them) · items · the 3×3 verb grid. Geometry is
   golden — slot sizes derive from --slot by φ, so heads, items and verbs
   stay in proportion. Items show GHOST slots for what you haven't found:
   the gaps are the invitation to a second playthrough. */
/* ---- THE ACTION PANEL (S29) — the studio's shop window ------------------
   GAME CONTENT ONLY: no mute, no menu, no toggles. Chrome lives in the
   header. Two columns: the STAGE on the left (who you are / what you can do)
   and the RAIL on the right (what you carry). Sections never fight for space
   because they are in different columns. Genre comes from meta.tray. */
.tray { --slot: 54px; display: flex; flex-direction: row; gap: 20px;
  padding: 12px 2px 0; min-height: 0; align-items: stretch; }
.tray[hidden] { display: none; }
.tray .tr-main { flex: 1 1 auto; min-width: 0; display: flex; gap: 12px; align-items: stretch; }
.tray .tr-side { flex: 0 0 auto; width: min(380px, 40%); display: flex;
  flex-direction: column; gap: 8px; align-items: stretch; min-width: 0; min-height: 0;
  justify-content: flex-end; }
.tray .tr-line { color: var(--gold); font: 12px/1.4 monospace; letter-spacing: .12em;
  text-transform: uppercase; min-height: 1.2em; width: 100%; text-align: right; flex: none; }
.tray .tr-group { display: flex; flex-direction: column; gap: 5px; align-items: stretch;
  width: 100%; min-height: 0; }
/* the satchel is ONE row, sitting at the foot of the rail */
.tray .tr-g-items { flex: none; }
.tray .tr-group[hidden] { display: none; }
.tray .tr-cap { color: var(--dimmest); font: 10px/1 monospace; letter-spacing: .18em;
  text-transform: uppercase; flex: none; }

/* CHARACTERS — always the left stage, filling the height, never clipped.
   Heads are square (matching the source art, so no face is cropped) and
   capped in WIDTH by headcount: a scene is usually two, sometimes more. */
.tray .tr-cast { display: flex; gap: 10px; flex: 1 1 auto; min-width: 0;
  align-items: stretch; overflow: visible; }
.tray .tr-cast.empty { display: none; }
/* THE CAST FILLS THE BAR — but the bar is only ever what the picture left
   over. The graphic takes its share first (--frame-h); the heads then use all
   of the remainder, so they are as large as they can be without ever
   competing with the screen. No cap: the layout above decides the size. */
.tray .tr-head { position: relative; height: 100%; aspect-ratio: 1;
  border: 2px solid var(--line); border-radius: 8px; background: var(--sunken);
  overflow: hidden; cursor: pointer; display: flex; align-items: center;
  justify-content: center; flex: 0 1 auto; min-width: 0;
  opacity: .68; transition: opacity .18s ease, border-color .18s ease; }
.tray .tr-cast.n-few   .tr-head { max-width: 46%; }
.tray .tr-cast.n-three .tr-head { max-width: 31%; }
.tray .tr-cast.n-many  .tr-head { max-width: 18.5%; }
.tray .tr-head:hover { opacity: .92; }
/* who is speaking is obvious at a glance; the rest are present but quiet */
.tray .tr-head.speaking { opacity: 1; border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold) inset; }
/* the source portraits are SQUARE and the full-height slot is taller than
   wide, so `cover` crops the sides — on a bust that eats the shoulders and
   crowds the face. Anchoring to the top keeps the head where it belongs and
   spends the crop on the chest instead. */
.tray .tr-head img { width: 100%; height: 100%; object-fit: cover;
  object-position: 50% 18%; image-rendering: pixelated; }
/* THE NAME WRAPS BEFORE IT TRUNCATES (2026-09-04, Eivind: "the characters
   are truncated [on mobile]. we dont want that"). A single `nowrap` line
   fit "Jim" on the small squares mobile's tight height budget produces, but
   ellipsized anyone with a longer name ("Squire Trelawney") to a stub. Two
   lines, clamped, read the whole name on every card size; only a name that
   still overflows TWO lines is ever cut, which no cast member's currently
   is. */
.tray .tr-name { position: absolute; left: 0; right: 0; bottom: 0; text-align: center;
  font: 11px/1.3 monospace; letter-spacing: .06em; color: var(--ink);
  background: rgba(0,0,0,.72); padding: 2px 2px;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; text-overflow: ellipsis; }
/* PARTY — the RPG register: cards, always lit, stats on the face */
.tray .tr-party .tr-head { opacity: 1; border-color: var(--bronze); }
.tray .tr-party .tr-head.speaking { border-color: var(--gold); }
.tray .tr-stats { position: absolute; top: 0; left: 0; right: 0;
  font: 10px/1.6 monospace; letter-spacing: .04em; text-align: center;
  color: var(--gold); background: rgba(0,0,0,.66); padding: 2px 1px; }

/* INVENTORY / SPELLS — the rail */
/* ONE ROW of big slots. Four fit; a fifth scrolls in from the right rather
   than shrinking everything — carried things stay legible at any count. */
.tray .tr-items { display: grid; grid-auto-flow: column;
  grid-auto-columns: calc((100% - 3 * 10px) / 4);
  gap: 10px; overflow-x: auto; overflow-y: hidden; flex: none;
  scroll-snap-type: x proximity; scrollbar-width: thin;
  padding-bottom: 2px; }
.tray .tr-items > * { scroll-snap-align: start; }
.tray .tr-items::-webkit-scrollbar { height: 5px; }
.tray .tr-items::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }
.tray .tr-spells { grid-auto-columns: calc((100% - 3 * 10px) / 4); }
.tray .tr-item { width: 100%; aspect-ratio: 1; border: 2px solid var(--line);
  border-radius: 10px; background: var(--sunken); cursor: pointer; overflow: hidden;
  display: flex; align-items: center; justify-content: center; }
.tray .tr-item img { width: 100%; height: 100%; object-fit: contain;
  image-rendering: pixelated; padding: 6px; }
.tray .tr-item:hover { border-color: var(--gold); }
.tray .tr-item.ghost { border-style: dashed; opacity: .35; cursor: default;
  background: repeating-linear-gradient(45deg, transparent 0 5px, var(--lift) 5px 10px); }
.tray .tr-item.held { border-color: var(--gold); opacity: 1;
  box-shadow: 0 0 0 1px var(--gold) inset, 0 0 10px rgba(216,176,106,.25); }
.tray.holding .tr-item:not(.held) { opacity: .55; }
.tray.holding .tr-head { border-style: dashed; }
.tray .ph { font: 600 15px/1 monospace; letter-spacing: .04em;
  color: hsl(var(--h, 40) 40% 82%); background: hsl(var(--h, 40) 24% 22%); }

/* SCUMM REGISTER — the verbs ARE the interface here, so they take the whole
   stage: nine big targets, no hunting, thumb-sized on any screen. */
.tray .tr-verbs { display: grid; grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 1fr; gap: 8px; flex: 1 1 auto; min-width: 0;
  /* the board measures itself, so a verb can be sized by the room it has */
  container-type: inline-size; }
/* A VERB IS ONE LINE (S92). "Pick up", "Look at" and "Talk to" broke across
   two on an iPad mini — a two-word verb reading as two things, in the one
   register where the verbs ARE the interface.
   The old size was `1.5vw`: the WINDOW's width, which says nothing about how
   much room this button has. Nine verbs in three columns means a chip is
   about a third of the board, and in landscape that board is narrow while the
   window is wide — so the type grew exactly where the space shrank. `cqi` is
   the board's own inline size, so a verb is sized by its chip.
   `nowrap` is the guarantee; the cqi clamp is what stops that guarantee from
   turning a wrap into an overflow. Both are needed, and neither is provable
   on CI: the fonts here are Futura and Courier New — present on the iPad,
   absent on a Linux box — so a headless run measures a narrower face than
   the one anybody actually reads. */
.tray .tr-verb { font: inherit; font-size: clamp(11px, 4.6cqi, 19px); letter-spacing: .03em;
  cursor: pointer; padding: 6px 9px; text-align: left; color: var(--ink-soft);
  background: var(--control); border: 1px solid var(--line); border-radius: 7px;
  display: flex; align-items: center; white-space: nowrap;
  transition: color .12s ease, border-color .12s ease; }
.tray .tr-verb:hover { color: var(--ink); border-color: var(--dim); }
.tray .tr-verb.on { color: var(--on-gold); border-color: var(--gold-bot);
  background: var(--gold-fill, linear-gradient(var(--gold-top), var(--gold-bot))); font-weight: bold; }

/* ---- THE MAIN MENU (S65) --------------------------------------------
   A 200px dropdown in a corner is where settings go when nobody has decided
   they matter. This comes to the FRONT, takes the room it needs, and each
   switch says what it does — because "ambience" names a thing without
   telling you whether you want it on. */
#setScrim {
  position: fixed; inset: 0; z-index: 60; background: rgba(6, 5, 12, .74);
  backdrop-filter: blur(3px); animation: scrimIn .16s ease-out;
}
#setScrim[hidden] { display: none; }
@keyframes scrimIn { from { opacity: 0; } }

/* A BOX IN THE MIDDLE, NOT A SIDE SHEET (Eivind). The sheet form (S92) traded
   a centred landing for motion continuity from the ☰ button — right call at
   four groups. A fifth (EMULATION) made that trade worse: a narrow column
   pinned to one edge has no room to grow, and Eivind asked directly for a
   centred box with space for the new controls alongside everything already
   there. So: fixed box, centred, scales/fades in instead of sliding from the
   edge — the ☰ still opens and closes it, it just no longer pretends to be
   the edge itself. */
/* UNDER ITS ORB, IN THE STUDIO MENU'S SHAPE (S210). Eivind: "settings menu
   should reflect the main menu, can we optimise settings better perhaps."
   The studio menu (js/studio-menu.js) hangs under the top-left orb as a
   narrow card of row-cards; this is its twin under the top-right orb — the
   same card, the same gold rule, the same 14px radius, the same width, the
   same 78px top — so the two menus read as one system, one on each side.
   The centred box (above, Eivind's earlier call) gave the sheet room for
   EMULATION's eight cards; at this width they run three across and the
   sheet scrolls, which a settings sheet may do and a menu of rooms need not. */
#setPanel {
  position: fixed; z-index: 61; top: 78px; right: 14px; left: auto; transform: none;
  width: min(420px, calc(100vw - 28px)); max-height: calc(100vh - 94px);   /* S284: wider, so every group holds one row */
  overflow-y: auto; scrollbar-width: thin;
  background: linear-gradient(200deg, var(--card), var(--sunken) 62%);
  border: 1px solid var(--gold-bot); border-radius: 14px;
  padding: 14px 12px 12px;
  box-shadow: 0 30px 80px rgba(0,0,0,.62), 0 1px 0 rgba(0,0,0,.6),
              inset 0 1px 0 rgba(184,150,63,.10);
  animation: menuIn .22s cubic-bezier(.2,.9,.3,1.02);
}
/* THE BOX GROWS WITH THE ROOM (Eivind: use the space on iPad and desktop).
   560px is a phone-honest width — on anything wider it reads as a strip
   floating in the middle of a hall. Same two breakpoints the rest of this
   file already uses for "tablet" and "desktop" (700px landscape's hall
   layout, 900px's golden-column stage) rather than a third number invented
   just for this box. Not orientation-gated like those: a portrait iPad is
   already 768-834px wide and just as able to hold a wider sheet as its
   landscape rotation is — width is what the box needs, not aspect. */
/* The centred descriptions this sheet showed in the maker were NOT inherited
   text-align — that was a first guess, and measuring killed it (every
   ancestor computed `start`). The cause was `#makerbar button`, 1-0-1,
   beating `.set-seg button`, 0-1-1, because S66 had parked the whole sheet
   inside the masthead: the rows were being handed `display:inline-flex;
   align-items:center`, so each label centred itself in its own button. Fixed
   at the root in maker.html — the ribbon adopts the BUTTON, the sheet goes
   back to <body> — so there is nothing to correct here. */
#setPanel[hidden] { display: none; }
/* the base transform re-centres every frame — animating `transform` replaces
   the whole value, so the centring has to ride along with the scale or the
   box would jump to the top-left corner for the animation's duration */
@keyframes menuIn { from { transform: translateY(-6px); opacity: .2; } }
/* the groups arrive just behind the box, in reading order — the box is
   the movement, this is only the settling */
#setPanel > * { animation: menuRow .26s both cubic-bezier(.2,.9,.3,1); }
#setPanel > *:nth-child(2) { animation-delay: .04s; }
#setPanel > *:nth-child(3) { animation-delay: .07s; }
#setPanel > *:nth-child(4) { animation-delay: .10s; }
#setPanel > *:nth-child(5) { animation-delay: .13s; }
#setPanel > *:nth-child(6) { animation-delay: .16s; }
@keyframes menuRow { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  #setPanel, #setScrim, #setPanel > * { animation: none; }
}

/* the title alone since S92 — the ✕ went back to being the ☰ (js/player.js).
   The right padding keeps MENU clear of the button now sitting over it. */
/* THE ONE ? (S115): help, top right of the head, clear of the ☰ that sits
   outside the panel. Toggles #setHelpText, a few lines under the head. */
#setHelp { margin-left: auto; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--line);
  background: none; color: var(--dim); font: inherit; font-size: 14px; cursor: pointer; flex: none; }
#setHelp:hover, #setHelp.on { border-color: var(--gold); color: var(--gold); }
#setHelpText { font-size: 12px; line-height: 1.5; color: var(--ink-soft); margin: -6px 0 18px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--sunken); }
#setHelpText b { color: var(--bronze); font-family: var(--font-display); font-size: 10px; letter-spacing: .12em; margin-right: 4px; }
#setHelpText[hidden] { display: none; }
.set-head { display: flex; align-items: center;
  position: sticky; top: -14px; z-index: 1; margin: -14px -12px 10px; padding: 14px 12px 10px 12px;
  background: linear-gradient(180deg, var(--card) 72%, transparent);
  border-bottom: 1px solid var(--edge); }
.set-head b { color: var(--gold); font-family: var(--font-display);
  font-size: 15px; letter-spacing: .3em; font-weight: normal; }

.set-group { margin-bottom: 12px; }
/* ONE GUTTER DOWN THE SHEET (S92). A row's mark — a checkbox in SOUND, an
   icon plate in TOOLS — reserves the SAME 34px column, so every label in the
   menu starts at one x and a group title sits on the rows' own left edge
   rather than 12px outside it. Measured before: four edges at 21 · 33 · 65 ·
   79px. The plate is the widest mark, so it sets the number; the checkbox
   centres itself in the same slot instead of being grown to fill it. */
.set-gtitle { color: var(--bronze); font-family: var(--font-display); font-size: 10px;
  letter-spacing: .28em; margin-bottom: 6px; padding-left: 13px; }

/* a row is a big target with a REASON on it, not a bare checkbox and a noun */
.set-row {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 8px 12px; border-radius: 10px; margin-bottom: 5px;
  background: var(--sunken); border: 1px solid var(--edge);
  transition: border-color .13s ease, background .13s ease;
}
/* a .set-row inside .set-seg (SOUND's three, laid out in a row like
   everything else now) gets its spacing from the grid's own gap — its own
   margin-bottom would just add a stray gap under the last one too */
.set-seg .set-row { margin-bottom: 0; }
/* a row that FOLLOWS a card grid (CRT under EMULATION) sits outside the grid,
   so its gap has to be stated — without it the row butts against the cards */
.set-seg + .set-row { margin-top: 8px; }
.set-row:hover { border-color: var(--bronze); background: var(--hover); }
.set-row input[type=checkbox] { width: 20px; height: 20px; accent-color: var(--gold); flex: none; }
.set-row b { flex: 1; min-width: 0; color: var(--ink); font-weight: normal; font-size: 12px; letter-spacing: .04em; }
.set-row:has(input:checked) { border-color: var(--gold-bot); }
.set-row:has(input:checked) b { color: var(--gold); }

/* ROWS, NOT COLUMNS (Eivind). A handful of short options read as a menu you
   scan left-to-right, not a form you scroll through — three or four cards
   side by side, wrapping only when the panel is too narrow to hold them.
   auto-fit collapses any track past the last real button, so a 3-option
   group (HOW IT LOOKS, EMULATION) sits as 3 even columns and a 4-option one
   (VERB POSITION) sits as 4, off the SAME rule — no per-group column count
   to keep in sync as options come and go.
   The MAX here must stay `1fr`, not a length — auto-fit counts columns
   using the min bound when the max is flexible, but switches to counting by
   the MAX bound the moment it's a fixed length (measured: minmax(112px,
   220px) in a 308px row collapsed to a single 220px column instead of two,
   because 2×220 "wouldn't fit" even though 2×112 plainly does). The actual
   220px cap the comment below still promises lives on the grid ITEMS
   instead, where max-width doesn't confuse the column count. */
/* align-items:stretch is grid's own default — stated explicitly because a
   row's buttons are FLEX containers in their own right (centering the label
   against its hint icon), and a flex-container-as-grid-item is exactly the
   shape WebKit has a known history of stretching inconsistently: Safari can
   leave one item at its own content height while its row-mates correctly
   fill the row, so a row with mixed 1-line/2-line labels reads jagged on
   iPad even when Chromium renders the identical CSS perfectly even. */
/* 96px minimum: EMULATION's four-a-row still shares one row inside the 440px sheet (skin-probe measures it) */
.set-seg { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 6px; align-items: stretch; }
/* two big cards, never three, for a two-fold choice (THE PUZZLES) */
.set-seg.set-two { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
/* on a phone the sheet is ~400px: EMULATION's eight go two across (not 3+1+3+1); SOUND's three stay one row */
@media (max-width: 560px) { .set-seg { grid-template-columns: repeat(2, 1fr); } .set-sound .set-seg { grid-template-columns: repeat(3, 1fr); } }
/* a sub-choice sits a step in from its parent, on the same gutter */
.set-sub { margin-top: 10px; padding-left: 14px; border-left: 2px solid var(--edge); }
.set-sub[hidden] { display: none; }
/* the 220px CAP matters once the box itself grows on iPad/desktop: without
   one, a 3-item row splits an 820px panel into ~260px cards for a label as
   short as "Native" — technically full, visually empty. Capped, a short row
   stays a comfortable size and leaves quiet margin instead of straining to
   fill the box; a 4-item row (VERB POSITION) still uses the extra width
   fully long before any card gets near the cap.
   height:100% backs up align-items:stretch rather than replacing it — belt
   and suspenders for the same WebKit case above, since a flex item's own
   `auto` height can otherwise win the argument against its grid row. */
/* LINED UP (S111, Eivind: "lined up all should be"). The old 220px cap on
   each card left every 3-option row short of the right edge while the
   4-option row and the CRT row reached it — four different right edges
   down one sheet. Cards fill their row; a row is as wide as the box. */
.set-seg > * { height: 100%; box-sizing: border-box; min-width: 0; }
/* LABELS, NOT PARAGRAPHS (Eivind: keep the menu simplistic and
   self-explanatory — a hint icon instead of a sentence on every option). One
   line: the name, then whatever room is left pushes the (i) to the trailing
   edge, same shape as a .set-row's checkbox-label-hint. */
.set-seg button {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  text-align: left; cursor: pointer;
  padding: 8px 12px; border-radius: 10px;
  background: var(--sunken); border: 1px solid var(--edge); font: inherit;
  transition: border-color .13s ease, background .13s ease;
}
/* THE TOOLS ARE ICONS, NOT ROWS (Eivind: "let the icons do the talk").
   Mark on top, name underneath, centred — a dock, not a paragraph list. No
   hint icon here; the "why" text rides as `title`/`aria-label` on the
   button itself instead (set in maker.html) — an icon is the whole point. */
.set-tools .set-seg button {
  flex-direction: column; justify-content: center;
  gap: 8px; text-align: center; padding: 14px 8px;
}
.set-ico {
  flex: none; width: 44px; height: 44px; display: grid; place-items: center;
  border-radius: 11px; background: var(--control); border: 1px solid var(--line);
  color: var(--bronze);
}
.set-ico svg { display: block; width: 24px; height: 24px;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round; }
.set-tools .set-seg button:hover .set-ico { color: var(--gold); border-color: var(--bronze); }
.set-tools .set-seg button.on .set-ico { color: var(--on-gold); border-color: var(--gold-bot);
  background: rgba(0,0,0,.14); }
.set-seg button:hover { border-color: var(--bronze); background: var(--hover); }
.set-seg button b { min-width: 0; color: var(--ink-soft); font-weight: normal; font-size: 12px; letter-spacing: .04em; }
.set-seg button.on { border-color: var(--gold-bot); background: var(--gold-fill); }
.set-seg button.on b { color: var(--on-gold); }

/* THE HINT (i) — every description that used to sit under a label as its
   own line now lives here instead: a small mark, native title on hover,
   aria-label for a screen reader. Dim until you reach for it, the same
   quiet register as .set-gtitle, so it reads as "more, if you want it"
   rather than competing with the label for attention. */

.set-big {
  width: 100%; padding: 10px; border-radius: 10px; cursor: pointer; font: inherit;
  letter-spacing: .2em; font-family: var(--font-display); font-size: 12px;
  color: var(--gold); background: var(--control); border: 1px solid var(--bronze);
}
.set-big:hover { background: var(--hover); border-color: var(--gold); }

/* PILLS (S314, Eivind: "pill buttons for the graphic and music modes, plus
   also the other settings. all should be easily visible without any
   dropdowns"). Every option is on the sheet: a row of pills that wraps when
   the sheet is narrow, rather than a dropdown that hides five of six. Two
   kinds, told apart at a glance:
   - a ONE-OF-SEVERAL (graphics, sound chip, puzzles, 3D, camera) is a row of
     pills with the chosen one gilt, like the plan slider's thumb;
   - a SWITCH (music, the room, effects, CRT, weather) is a pill with a lamp
     in it — lit gold when on. Its checkbox is still the control, only hidden
     from sight, so it keeps its keyboard, its label and every probe. */
.set-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.set-pills + .set-pills { margin-top: 6px; }
.set-pills > button, .set-pills > .set-row { position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; margin: 0;
  min-height: 30px; padding: 5px 11px; box-sizing: border-box; border-radius: 999px; cursor: pointer; font: inherit;
  background: var(--sunken); border: 1px solid var(--edge); transition: border-color .13s ease, background .13s ease; }
.set-pills > button:hover, .set-pills > .set-row:hover { border-color: var(--bronze); background: var(--hover); }
.set-pills b { flex: none; color: var(--ink-soft); font-weight: normal; font-size: 12px; letter-spacing: .03em; white-space: nowrap; }
.set-pills > button.on { border-color: var(--gold-bot); background: var(--gold-fill); }
.set-pills > button.on b { color: var(--on-gold); }
.set-pills > button:focus-visible, .set-pills > .set-row:has(input:focus-visible) { outline: 2px solid var(--gold); outline-offset: 2px; }
.set-pills > .set-row input[type=checkbox] { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.set-pills > .set-row::before { content: ''; flex: none; width: 7px; height: 7px; border-radius: 50%; border: 1px solid var(--dim); }
.set-pills > .set-row:has(input:checked) { border-color: var(--gold-bot); }
.set-pills > .set-row:has(input:checked)::before { border-color: var(--gold); background: var(--gold); box-shadow: 0 0 6px var(--gold); }
.set-pills > .set-row:has(input:checked) b { color: var(--gold); }
.set-hint { margin: 6px 0 0 13px; font: 11px/1.4 var(--font-mono); color: var(--dimmest); }

/* nothing behind the menu scrolls while it is open */
body.menu-open { overflow: hidden; }

/* THE CODEX (S46) — what you have found, and the shape of what you have not.
   Sits under the fate card because that is the moment the question lands. */
.codex { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--edge); text-align: left; }
.codex h2 { font: bold 12px/1.4 var(--font-display); letter-spacing: .22em;
  text-transform: uppercase; color: var(--gold); }
.cx-score { color: var(--ink-soft); font-family: var(--font-mono); font-size: 12px; margin: 5px 0 12px; }
.cx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 8px; }
.cx-e { padding: 7px 9px; border: 1px solid var(--edge); border-radius: 6px;
  background: var(--sunken); min-width: 0; }
.cx-e b { display: block; font-weight: normal; color: var(--dimmest);
  font-size: 12px; letter-spacing: .1em; }
.cx-e span { display: block; margin-top: 3px; color: var(--dimmest);
  font-size: 11px; line-height: 1.45; font-style: italic; }
.cx-e.has { border-color: var(--bronze); }
.cx-e.has b { color: var(--gold); letter-spacing: .04em; }
.cx-e.has span { color: var(--ink-soft); font-style: normal; }
.cx-tail { margin-top: 12px; color: var(--dim); font-size: 12px; line-height: 1.5; }
.cx-tail.cx-done { color: var(--gold); }

/* THE POLYGON STAGE (S50) — a vector scene fills the frame exactly as a
   picture would, so every register (theatre, caption, Ken Burns) still works
   around it without knowing the difference. */
#panel canvas.vec { position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; z-index: 1; }

/* THE TRIANGLE (S55) — three ways to meet a beat, side by side and equal.
   Equal width on purpose: a rock-paper-scissors choice where one option looks
   more important is not a rock-paper-scissors choice. */
.st-throws { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 10px; }
.st-throw { font: inherit; cursor: pointer; padding: 10px 12px; text-align: left;
  display: flex; flex-direction: column; gap: 4px;
  background: var(--control); border: 1px solid var(--line); border-radius: 8px;
  transition: border-color .12s ease, background .12s ease; }
.st-throw:hover { border-color: var(--gold); background: var(--hover); }
.st-throw b { color: var(--gold); font: bold 13px/1.2 var(--font-display); letter-spacing: .14em; }
.st-throw span { color: var(--dim); font-size: 11px; line-height: 1.4; }
/* the throws already made — the sequence is the game, not the single pick */
.st-thrown { display: flex; gap: 6px; margin-top: 6px; }
.st-t { font: 10px/1 var(--font-mono); letter-spacing: .1em; color: var(--dimmest);
  border: 1px dashed var(--line); border-radius: 4px; padding: 4px 7px; }
.st-of { color: var(--dimmest); font: 11px/1 var(--font-mono); letter-spacing: .08em; }
/* and the read, in the beat log */
.st-vs { font: 10px/1 var(--font-mono); letter-spacing: .06em; color: var(--dimmest);
  border-radius: 4px; padding: 2px 5px; border: 1px solid var(--line); }
.st-vs.won { color: var(--gold); border-color: var(--bronze); }
.st-vs.lost { color: var(--red); border-color: var(--red-deep); }
@media (max-width: 640px) { .st-throws { grid-template-columns: 1fr; } }

/* THE STAGE REGISTER (S54) — the verbs take the floor.
   The nine verbs stand where the cast heads normally do and run the full main
   width up to the rail, so the inventory and the description keep their side
   untouched. This is the SCUMM board as it was actually played: big targets,
   always visible, nothing to hunt for. */
.tray .tr-verbs.stage { flex: 1 1 auto; min-width: 0; }
.tray.verbs-staged .tr-cast { display: none; }
.tray.verbs-staged .tr-main { align-items: stretch; }
/* at this size the verbs can afford to be read rather than scanned */
.tray.verbs-staged .tr-verbs.stage .tr-verb { font-size: clamp(11px, 5cqi, 22px); padding: 8px 11px; }
.tray .tr-verbs.stage[hidden] { display: none; }

/* THE SHARED SLOT (S49). When an answer takes the floor from the verb board
   it should read as an ARRIVAL, not a swap — and it should look tappable,
   because tapping it hands the slot straight back. */
.tray .tr-read.sentence b { color: var(--gold); letter-spacing: .1em; text-transform: uppercase; }
/* the body under a sentence is the held object's own description — normal
   text, same as when it was not a sentence (Eivind, S112: "weird that the
   description becomes italic") */
.tray.verbs-auto .tr-read.has { cursor: pointer; }
.tray.verbs-auto .tr-verbs.rail { animation: verbsIn .18s ease-out; }
@keyframes verbsIn { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .tray.verbs-auto .tr-verbs.rail { animation: none; } }

/* THE COMMAND PANEL (S46) — the battle register.
   Not the verb grid: a verb builds a sentence, a command IS the move. Two
   columns of wide targets with the icon leading, so the row reads as a menu
   you scan rather than a vocabulary you compose. GREEN/RED still carries its
   meaning (advance / hold) where a stance declares one. */
.tray .tr-cmd { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px; align-content: start; min-width: 0; }
.tray .tr-cmd.n-few { grid-template-columns: minmax(0, 1fr); }
.tray .tr-cmd-btn {
  font: inherit; font-size: clamp(11px, 1.15vw, 15px); letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer; padding: 8px 10px; min-height: 42px;
  display: flex; align-items: center; gap: 8px; text-align: left;
  color: var(--ink-soft); background: var(--control);
  border: 1px solid var(--line); border-radius: 7px;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.tray .tr-cmd-btn:hover { color: var(--ink); border-color: var(--dim); background: var(--hover); }
.tray .tr-cmd-btn .tc-i { flex: none; font-size: 1.25em; line-height: 1; opacity: .9; }
.tray .tr-cmd-btn .tc-l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the studio grammar survives into the fight: green advances, red holds */
.tray .tr-cmd-btn.green { border-color: var(--green-edge, #3c6b45); }
.tray .tr-cmd-btn.green:hover { color: var(--green-lit, #9fe8b0); }
.tray .tr-cmd-btn.red { border-color: var(--red-deep); }
.tray .tr-cmd-btn.red:hover { color: var(--red-lit, #ffb3a7); }
/* LOCKED is shown, never hidden — a visible gap is what invites the next run */
.tray .tr-cmd-btn.locked { color: var(--dimmest); border-style: dashed; cursor: help; }
.tray .tr-cmd-btn.locked:hover { color: var(--dim); background: none; }
.tray .tr-cmd-btn.back { grid-column: 1 / -1; color: var(--dim); }
.tray .tr-cmd-none { grid-column: 1 / -1; color: var(--dimmest);
  font-size: 12px; padding: 8px 2px; }

/* portrait / narrow: the bar sits under the card, laid out in one row */
@media (max-width: 899px), (orientation: portrait) {
  .tray { --slot: 44px; flex-direction: column; gap: 10px; }
  .tray .tr-side { width: 100%; align-items: stretch; }
  .tray .tr-line, .tray .tr-group { text-align: left; align-items: flex-start; }
  .tray .tr-items { justify-content: flex-start; }
  /* THE REAL BUG (2026-09-04): `flex: none` here made `.tr-cast` a
     content-sized box instead of sharing `.tr-main`'s actual width (374px
     on a real phone) — a lone portrait shrank to match its own height
     (120px square) because nothing told it to claim the row. The base rule
     (`flex: 1 1 auto`, line 575) already does this correctly on iPad and
     desktop; mobile was overriding it away for reasons lost to time, and
     that — not the two-line name — was why "Jim Hawkins" had ~50px to
     write itself into. Dropping the override lets it size like every
     other breakpoint. The height floor still moved 64 → 78px, so a
     two-line name has the room to use. */
  .tray .tr-cast { height: clamp(78px, 15vh, 120px); }
  .tray .tr-verbs { min-height: 160px; }
}

/* ---- HOT ZONES + THE HELD ITEM (S30) ----------------------------------
   Zones are invisible: an adventure asks you to LOOK, not to hunt lit-up
   buttons. They answer the pointer with a cursor and a whisper of an edge
   on hover, and they light up while you are holding something — so a held
   item shows you where it might go without giving the puzzle away. */
#panel .hz { position: absolute; z-index: 3; padding: 0; margin: 0;
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; transition: border-color .16s ease, background .16s ease; }
#panel .hz:hover { border-color: rgba(216,176,106,.55); background: rgba(216,176,106,.07); }
#panel .hz:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
body.holding-item #panel .hz { border-color: rgba(216,176,106,.28); }
body.holding-item #panel .hz:hover { border-color: var(--gold); background: rgba(216,176,106,.14); }
@media (prefers-reduced-motion: reduce) { #panel .hz { transition: none; } }

/* the item in hand — SCUMM's "use X with ___" made visible */
.tray .tr-item.held { border-color: var(--gold); opacity: 1;
  box-shadow: 0 0 0 1px var(--gold) inset, 0 0 10px rgba(216,176,106,.25); }
.tray.holding .tr-item:not(.held) { opacity: .55; }
.tray.holding .tr-head { border-style: dashed; }
.tray .tr-item[draggable="true"] { -webkit-user-drag: element; }

/* ---- THE MENU BUTTON (S33, and only the button since S92) --------------
   Top right, tucked under the curtain's edge. Play mode shows no chrome at
   all until you reach for it: one small ☰, and the sheet comes in from the
   edge it sits on.

   WHAT USED TO BE HERE WAS OVERRIDING THE MENU. S33's drawer styled
   `#settings #setPanel` — TWO ids — so when S65 replaced the drawer with a
   real menu, every property the two blocks shared silently kept S33's
   answer: the small card background, the 10px radius, the 12px padding, the
   thin shadow, and `cardIn` instead of the menu's own entrance. The same
   went for `#settings label`, which flattened `.set-row` back to a checkbox
   on one line. Nobody wrote a broken menu; two ids beat one and the newer
   design lost. It is the recurring class of bug in this file (S25/S26), and
   the fix is always the same: delete the older selector rather than out-
   specify it. */
#settings { position: fixed; top: 14px; right: 14px; z-index: 45;
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
/* OPEN, IT OUTRANKS ITS OWN SHEET (S92). The sheet is z-index 61 at the right
   edge — the very corner the button lives in — so the ☰ was buried the moment
   it did its job, which is what forced the second ✕ into the header. Raised
   above it, the one button stays exactly where you pressed it and simply
   wears the other mark. The box is unchanged in both states on purpose: the
   ✕ must land ON the ☰, not merely near it. */
#settings.open { z-index: 62; }
/* and above the sheet INSIDE that new stacking context too: in the shipped
   player the panel is still this button's own sibling, so it must clear the
   sheet's own 61 — merely being positioned is not enough, a plain z-index:1
   still loses to it. (In the maker the sheet lives on <body>, so the number
   here does nothing and the wrapper's 62 is what counts.) */
#settings.open #setBtn { position: relative; z-index: 62; }
/* THE ORB (S206). Eivind: "unified menus in top left and right. one for the
   story studio rooms, and one for the player itself. make them nice unified."
   This is the top-RIGHT orb, the player's own menu, and it ships with every
   game. The top-LEFT orb is the studio's (js/studio-menu.js, never shipped).
   The two are the same object in two corners — 56×56, 14px in, the same
   ring, the mark centred at its own fixed size — and the numbers are spelled out in both files
   on purpose: an export never loads studio-menu.js, a lab page never loads
   this file. studio-menu-probe measures the two against each other.
   S257: 56, not 48. The masthead is 84px BECAUSE of the orb (studio-header.css
   says so: the orb plus its inset top and bottom), and the orb had quietly
   been 48 since the mark took it over — so the row was built for a logo a
   size larger than the one in it, and Eivind read the logo as small. Both
   corners move together or they are no longer one object; the inset is
   (84 - 56) / 2 = 14, the same number as the side inset, which is why the
   corner looks square.
   THE MARK IS DRAWN, NOT TYPED (S73, once maker-only): centring the TEXT ☰
   centres its line box, and the bars sit wherever the font decides. A
   hard-stop gradient gives three 2px rules with 4px between, identical in
   every font and crisp at any DPR; open, the same box wears a drawn ✕. The
   ☰/✕ text js/player.js writes stays in the DOM for the accessible name at
   font-size 0. */
#settings #setBtn { box-sizing: border-box; width: 56px; height: 56px; padding: 0; position: relative; font-size: 0;
  color: var(--gold); background: rgba(20,16,10,.85); backdrop-filter: blur(6px);
  border: 1.5px solid rgba(216,176,106,.55); border-radius: 50%; box-shadow: 0 6px 18px rgba(0,0,0,.45);
  cursor: pointer; opacity: .88; transition: opacity .18s ease, border-color .18s ease; }
#settings #setBtn::before { content: ''; position: absolute; inset: 0; margin: auto; width: 22px; height: 14px;
  background: linear-gradient(currentColor 0 2px, transparent 2px 6px) 0 0 / 100% 6px repeat; }
#settings.open #setBtn::before { width: 20px; height: 20px; transform: rotate(45deg);
  background: linear-gradient(currentColor, currentColor) center / 100% 2px no-repeat,
              linear-gradient(currentColor, currentColor) center / 2px 100% no-repeat; }
#settings:hover #setBtn, #settings.open #setBtn, #settings #setBtn:focus-visible { opacity: 1; color: var(--gold-bright); border-color: var(--gold);
  box-shadow: 0 6px 22px rgba(0,0,0,.55); }

/* ---- THE PLAYER'S MASTHEAD (S208) ----------------------------------------
   Eivind: "in story studio player we have no masthead. so the menu and
   settings icons are protruding onto story panels. can we make a new header
   in the player, stating what it is and what story you are playing, and we
   will be able to show ads there, like unlock story studio pro."
   The same row every studio room has (studio-header.css): 84px tall — the
   orb's 56 plus 14 above and below — the brand at 18px, everything after it
   at 12px, an 84px gutter at the right for the player's orb, and one at the
   left only when the studio's orb is there (html.st-orb, set by
   js/studio-menu.js — an export has no studio orb and no gutter). The AD
   SLOT at the right is a small card; its first tenant is the Pro line. The
   theatre register owns the whole screen, so the masthead steps out there. */
.pl-head { height: 84px; box-sizing: border-box; display: flex; align-items: center; gap: 14px; flex-wrap: nowrap;
  padding: 8px 84px 8px 22px; background: var(--sunken); border-bottom: 1px solid var(--edge); position: relative; z-index: 5; }
html.st-orb .pl-head { padding-left: 84px; }
.pl-brand { font-family: var(--font-display); letter-spacing: .28em; color: var(--gold); font-size: 18px; white-space: nowrap; }
.pl-room { color: var(--dim); font-size: 12px; letter-spacing: .14em; text-transform: uppercase; font-family: var(--font-display); white-space: nowrap; }
.pl-story { color: var(--ink-soft); font-family: var(--font-mono); font-size: 12px; border-left: 1px solid var(--line); padding-left: 14px;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-story:empty { display: none; }
.pl-ad { margin-left: auto; flex: none; display: flex; flex-direction: column; gap: 3px; text-decoration: none; text-align: right;
  padding: 7px 14px; border: 1px solid var(--gold-deep); border-radius: 10px; background: var(--lift); }
.pl-ad b { font-family: var(--font-display); font-size: 11px; letter-spacing: .18em; color: var(--gold); font-weight: normal; }
.pl-ad span { font-family: var(--font-mono); font-size: 10px; color: var(--dim); }
.pl-ad:hover { border-color: var(--gold); background: rgba(216,176,106,.08); }
.pl-ad:hover span { color: var(--ink-soft); }
body.theatre-on .pl-head { display: none; }
@media (max-width: 900px) { .pl-ad span { display: none; } .pl-room { display: none; } }
@media (max-width: 560px) { .pl-ad { display: none; } .pl-story { border-left: 0; padding-left: 0; } }

/* ---- THE CURTAIN (S34) — the house lights go down ----------------------
   Velvet parts once at the top of a session. The palette lives here rather
   than in maker.css because this is the PRODUCT's opening, not studio
   furniture: the side-curtains and crew badges stay maker-only, but the
   ceremony ships with the game.

   The easing is the whole trick. A drawn curtain is heavy — it leans into
   the pull, travels, and is eased to rest by its own weight. Linear motion
   reads as a web animation; this reads as cloth. */
:root {
  --velvet-dark:#2a0910; --velvet-mid:#43111a; --velvet-lit:#5e1822;
  --velvet-edge:#8a6a34;
}
#curtain { position: fixed; inset: 0; z-index: 90; pointer-events: none; overflow: hidden; }
#curtain .cu-panel {
  position: absolute; top: 0; bottom: 0; width: 51%;
  /* the folds: uneven on purpose — even stripes look like a barcode */
  background:
    linear-gradient(90deg, rgba(0,0,0,.55), rgba(0,0,0,0) 18%, rgba(0,0,0,0) 82%, rgba(0,0,0,.55)),
    repeating-linear-gradient(90deg,
      var(--velvet-dark) 0 9px, var(--velvet-mid) 15px 26px,
      var(--velvet-lit) 32px 38px, var(--velvet-mid) 44px 54px);
  /* THE FOLD, NOT A SCALE (S236, Eivind: "old style zoom in and out" — every
     room but the house). scaleX on the whole panel was this room's stand-in
     for house.css's `.folds` background-size trick — it read as the panel
     shrinking, i.e. a zoom, not cloth gathering. Same fix: the pattern's own
     tile narrows as the panel travels, the panel itself only ever slides. */
  background-size: auto, 54px 100%;
  will-change: transform, background-size;
}
/* the inner edge: a brass catch-light, and the shadow it throws on the screen */
#curtain .cu-panel > i {
  position: absolute; top: 0; bottom: 0; width: 46px; pointer-events: none;
}
#curtain .cu-l { left: 0; }
#curtain .cu-r { right: 0; }
#curtain .cu-l > i { right: -46px;
  background: linear-gradient(90deg, rgba(0,0,0,.55), rgba(0,0,0,0));
  border-left: 2px solid var(--velvet-edge); }
#curtain .cu-r > i { left: -46px;
  background: linear-gradient(270deg, rgba(0,0,0,.55), rgba(0,0,0,0));
  border-right: 2px solid var(--velvet-edge); }
/* the pelmet, which lifts a beat after the cloth starts moving */
#curtain .cu-valance {
  position: absolute; top: 0; left: 0; right: 0; height: 46px;
  background: linear-gradient(var(--velvet-lit), var(--velvet-dark));
  border-bottom: 2px solid var(--velvet-edge);
  box-shadow: 0 6px 20px rgba(0,0,0,.6);
}

/* THE INTERMISSION (S123): the cloth comes IN over a scene already playing.
   The same travel, run backwards and faster — a curtain closes quicker than
   it opens, because closing is the pull and opening is the release. Built
   already-open, so `.shut` is the arriving state and `.open` then parts it
   for real. */
#curtain.shut .cu-l { animation: cuShutL 1.1s cubic-bezier(.45,.05,.35,1) both; }
#curtain.shut .cu-r { animation: cuShutR 1.1s cubic-bezier(.45,.05,.35,1) both; }
#curtain.shut .cu-valance { animation: cuDrop 1.1s cubic-bezier(.45,.05,.35,1) both; }
/* SPRING AT STOP (Eivind, 2026-09-19): the last 15% overshoots past rest and
   settles back — the cloth's own weight catching itself, not a hard stop.
   The 0→85% span keeps its rule-level easing; the overshoot swaps in its own
   timing-function at the 85% stop, which governs only that final stretch. */
@keyframes cuShutL {
  0%   { transform: translateX(-101%); background-size: auto, 38px 100%; }
  85%  { transform: translateX(3%); background-size: auto, 58px 100%; animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateX(0); background-size: auto, 54px 100%; }
}
@keyframes cuShutR {
  0%   { transform: translateX(101%); background-size: auto, 38px 100%; }
  85%  { transform: translateX(-3%); background-size: auto, 58px 100%; animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateX(0); background-size: auto, 54px 100%; }
}
@keyframes cuDrop {
  0%   { transform: translateY(-100%); }
  85%  { transform: translateY(3%); animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateY(0); }
}
/* the act title, on the cloth — it only exists while the house is dark */
#curtain .cu-act {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 24px; z-index: 2;
  font: clamp(15px, 2.6vw, 26px)/1.5 var(--font-display); letter-spacing: .34em;
  text-transform: uppercase; color: var(--gold);
  text-shadow: 0 2px 18px rgba(0,0,0,.9);
  opacity: 0; animation: cuActIn .7s .95s ease-out both; }
#curtain.open .cu-act { animation: cuActOut .45s ease-in both; }
@keyframes cuActIn  { from { opacity: 0; letter-spacing: .5em; } to { opacity: 1; letter-spacing: .34em; } }
@keyframes cuActOut { to { opacity: 0; } }

#curtain.open .cu-l { animation: cuPartL 1.7s cubic-bezier(.62,.02,.24,1) forwards; }
#curtain.open .cu-r { animation: cuPartR 1.7s cubic-bezier(.62,.02,.24,1) forwards; }
#curtain.open .cu-valance { animation: cuLift 1.35s .18s cubic-bezier(.6,0,.3,1) forwards; }

/* travel AND gather: the fabric bunches toward the wings as it goes, so the
   folds compress rather than the panel simply sliding away */
@keyframes cuPartL {
  0%   { transform: translateX(0); background-size: auto, 54px 100%; }
  85%  { transform: translateX(-106%); background-size: auto, 34px 100%; animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateX(-101%); background-size: auto, 38px 100%; }
}
@keyframes cuPartR {
  0%   { transform: translateX(0); background-size: auto, 54px 100%; }
  85%  { transform: translateX(106%); background-size: auto, 34px 100%; animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateX(101%); background-size: auto, 38px 100%; }
}
@keyframes cuLift {
  0%   { transform: translateY(0); }
  85%  { transform: translateY(-108%); animation-timing-function: cubic-bezier(.34,1.4,.64,1); }
  100% { transform: translateY(-100%); }
}

@media (prefers-reduced-motion: reduce) { #curtain { display: none; } }

/* THE READOUT — examine text lives beside the thing examined, filling the
   rail between the status line and the satchel. Poking at the world must
   never shove the story's prose aside: two voices, two places. */
.tray .tr-read { flex: 1 1 auto; min-height: 4.5em; overflow-y: auto; scrollbar-width: thin;
  display: flex; flex-direction: column; gap: 6px; padding: 2px 0 6px;
  color: var(--ink-soft); font-size: clamp(12px, 1.15vw, 14px); line-height: 1.55; }
.tray .tr-read:not(.has) { visibility: hidden; }
.tray .tr-read.in { animation: cardIn .22s ease-out both; }
.tray .tr-read b { color: var(--gold); font: 11px/1.4 monospace; letter-spacing: .16em;
  text-transform: uppercase; font-weight: normal; }
.tray .tr-read em { color: var(--gold); font-style: normal; }
@media (prefers-reduced-motion: reduce) { .tray .tr-read.in { animation: none; } }

/* CHOICES FLOW WITH THE PROSE (S28). Pinning them to the card's foot was
   tried and reverted: it put a rule and a gap between the story and its own
   options, which made them read as UI rather than as the page's last lines.
   They are part of the text — that is the inky register, and the whole point. */

/* the fate card's optional full tally — a completionist's drawer, shut by
   default so the verdict is not buried under a column of zeroes */
.fate-all { margin-top: 10px; }
.fate-all summary { cursor: pointer; color: var(--dim); font-size: 12px;
  letter-spacing: .12em; text-transform: uppercase; }
.fate-all summary:hover { color: var(--gold); }
.sub.carried { color: var(--gold-deep); }

/* ---- THEATRE (S38) — the audiobook register ---------------------------
   The picture is the whole screen and the VOICE carries the text. Nothing
   else is drawn: no cast, no satchel, no prose column, no title. Only the
   choices, as stylised buttons over the image, and whatever timed moment the
   scene calls for. A story written for listening should look like one being
   watched — so this is the register where a scene can also run a mini-game
   inside the frame, with the whole viewport to play in. */
#stage.theatre { max-width: none; padding: 0; margin: 0; height: 100vh;
  display: block; gap: 0; }
/* the `body` prefix is not decoration: the hall's rule is
   `body:not(.maker) #stage > #panel` at (2,1,1), and a bare
   `#stage.theatre > #panel` is only (2,1,0) — it would lose and the screen
   would keep the hall's height. Matching specificity and coming later wins. */
body #stage.theatre > #panel {
  position: fixed; inset: 0; width: 100vw; height: 100vh;
  border: 0; border-radius: 0; margin: 0; z-index: 1; aspect-ratio: auto;
}
body #stage.theatre > #panel img { width: 100%; height: 100%; object-fit: cover; }
body #stage.theatre > #card, body #stage.theatre > #tray { display: none; }
body.theatre-on #settings { z-index: 12; }
/* THEATRE is the frame owning the whole screen, so it takes no matte — the
   picture IS the page there, and bars would be cropping it for nothing.
   (The stage-level mattes this used to switch off are retired; S75 moved the
   letterbox onto #panel's own border, and S92 removed the letterbox with the
   rest of the cinema skin — so there is nothing left here to switch off.) */

/* the choices: floating over the picture, unmistakably the thing to press.
   A scrim under them keeps them legible on any plate without dimming the art
   above — the picture stays the point. */
#panel > #choices {
  position: absolute; left: 50%; bottom: 6%; transform: translateX(-50%);
  z-index: 8; width: min(680px, 86vw); display: flex; flex-direction: column;
  gap: 10px; padding: 0; margin: 0; border: 0;
}
#panel > #choices::before {
  content: ''; position: absolute; left: -8vw; right: -8vw; top: -40px; bottom: -8vh;
  background: radial-gradient(120% 100% at 50% 100%, rgba(0,0,0,.72), rgba(0,0,0,0) 72%);
  pointer-events: none; z-index: -1;
}
#panel > #choices button {
  font: inherit; font-size: clamp(15px, 1.7vw, 20px); letter-spacing: .04em;
  min-height: 58px; padding: 14px 22px; text-align: center; cursor: pointer;
  color: var(--ink); border-radius: 10px; border: 1px solid rgba(216,176,106,.45);
  background: linear-gradient(rgba(28,23,36,.82), rgba(13,10,19,.88));
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  box-shadow: 0 6px 24px rgba(0,0,0,.45);
  transition: border-color .16s ease, background .16s ease, transform .12s ease;
}
#panel > #choices button::before { content: none; }
#panel > #choices button:hover {
  border-color: var(--gold); background: linear-gradient(rgba(42,35,54,.9), rgba(20,16,28,.92));
}
#panel > #choices button:active { transform: translateY(1px); }
#panel > #choices button.green { border-color: rgba(127,174,142,.6); }
#panel > #choices button.red { border-color: rgba(207,106,82,.6); }
/* the stage/quick boxes get the same treatment when they mount here */
#panel > #choices .stagebox, #panel > #choices .menu {
  background: linear-gradient(rgba(28,23,36,.86), rgba(13,10,19,.9));
  border: 1px solid rgba(216,176,106,.3); border-radius: 12px; padding: 14px 16px;
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
@media (prefers-reduced-motion: reduce) { #panel > #choices button { transition: none; } }

/* THE SCUMM PANEL in the rail (S39) — the nine verbs taking the description's
   slot above the satchel. Compact where the SCUMM register's wall is huge:
   here they are a tool you opted into, not the whole interface. */
/* the verbs sit ABOVE the satchel and take only what they need; the
   description keeps the space above them. Picking a verb and reading what it
   answered are two halves of one action — neither should evict the other. */
.tray .tr-verbs.rail { grid-auto-rows: auto; gap: 4px; flex: none;
  align-content: start; }
.tray .tr-verbs.rail[hidden] { display: none; }
.tray .tr-verbs.rail .tr-verb { font-size: 12px; padding: 7px 8px; min-height: 0;
  background: transparent; border: 1px solid var(--line); color: var(--dim); }
.tray .tr-verbs.rail .tr-verb:hover { color: var(--ink); border-color: var(--dim); }
.tray .tr-verbs.rail .tr-verb.on { color: var(--gold); border-color: var(--gold);
  background: rgba(201,164,92,.08); font-weight: normal; }
.tray .tr-read[hidden] { display: none; }

/* ---- REGISTER VARIANTS (S40) -------------------------------------------
   The same panel, dressed for what the scene needs. Each of these is one
   preset's worth of difference — no new components, only proportions. */
/* a fuller company: four across still readable */
.tray .tr-cast.n-four .tr-head { max-width: 24%; }
/* CREW — everyone in the scene; the rail steps back to give them the room */
.tray.slim-rail .tr-side { width: min(220px, 26%); }
.tray.slim-rail .tr-items { grid-auto-columns: calc((100% - 2 * 10px) / 3); }
/* DUEL — two people and nothing else, so they get the whole bar */
.tray.no-rail .tr-side { display: none; }
.tray.no-rail .tr-main { justify-content: center; gap: 6vw; }
.tray.no-rail .tr-cast.n-few .tr-head { max-width: 34%; }
/* SATCHEL — the puzzle register: a deep bag, faces small */
.tray.big-bag .tr-side { width: min(460px, 46%); }
.tray.big-bag .tr-items { grid-auto-flow: row; grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 1fr; overflow: visible; flex: 1 1 auto; }
.tray.big-bag .tr-cast .tr-head { max-height: clamp(80px, 12vh, 120px); }
.tray.big-bag .tr-read { flex: none; min-height: 3em; }

/* THE VERB PANEL REPORTS, IT DOES NOT COMMAND (S41).
   Nothing is lit until you point at something; then the verb a click would
   actually use lights up. `.hint` is that live readout — softer than `.on`,
   which means a verb you deliberately pinned. The panel teaches its own
   vocabulary without ever requiring you to use it. */
.tray .tr-verb.hint { color: var(--gold-bright); border-color: var(--gold-deep);
  background: rgba(201,164,92,.05); }
.tray .tr-verbs.rail .tr-verb.hint { color: var(--gold-bright);
  border-color: var(--gold-deep); background: rgba(201,164,92,.05); }
/* a pinned verb stays firmly on until it is spent */
.tray .tr-verb.on { box-shadow: 0 0 0 1px var(--gold-bot) inset; }

/* ---- THE ARENA (S42) — the duel register's takeover ---------------------
   When a contest runs in the `duel` register it owns the action bar outright:
   the satchel and the description stand down, and the whole width goes to the
   one thing happening. A fight should not share a shelf with your cheese. */
#tray.arena { display: block; padding: 6px 2px 0; }
#tray.arena .stagebox { max-width: 100%; gap: 12px; }
#tray.arena .st-title { font-size: 14px; letter-spacing: 4px; }
#tray.arena .st-cards { align-items: stretch; }
#tray.arena .st-qte { font-size: clamp(24px, 4vw, 40px); letter-spacing: 6px; padding: 6px 0; }
/* the odds made visible: a gang crowds the frame, one against several */
.st-gang { display: flex; gap: 8px; align-items: stretch; min-width: 0; }
.st-gang .stagecard, .st-gang .stageface { flex: 1 1 0; min-width: 0; }
.st-gang.many .stagecard { padding: 8px 9px; font-size: 11px; }
.st-gang.many .sc-text { display: none; }   /* no room for banter three-up */
.st-gang.many .sc-name { font-size: 11px; letter-spacing: 1px; }
#tray.arena .st-cards { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr); }

/* ============================================================
   THE COVER (S74) — the jacket, not a settings dialog
   ============================================================
   The title screen was a 420px box with a 24px heading and three
   checkboxes in it. It was also the first thing anybody ever saw.

   A serif, because this is a book. The stack is all system faces — the
   studio ships offline and a webfont is a download that can fail — but
   Iowan Old Style and Palatino are genuine book faces and are present on
   the machines this runs on. */
:root { --font-title: "Iowan Old Style", "Palatino Linotype", Palatino,
                      "Book Antiqua", Georgia, "Times New Roman", serif; }

/* [hidden] MUST ACTUALLY HIDE. `.full[hidden]{display:none}` and
   `.full.cover{display:block}` are BOTH specificity (0,2,0), and this rule
   is later in the file — so it won, the cover never left, and it sat over
   the game swallowing every click. "I cannot start the story" was this one
   line. Same trap the maker's rooms hit: any element toggled by the hidden
   ATTRIBUTE needs saying twice. */
.full.cover { padding: 0; display: block; overflow: hidden; }
.full.cover[hidden] { display: none; }

/* THE PLATE, full bleed. object-fit:cover and a slow drift — a still image
   with the faintest motion reads as "waiting", a frozen one as "broken". */
.cv-plate { position: absolute; inset: 0; overflow: hidden; }
.cv-plate img, .cv-grad { position: absolute; inset: 0; width: 100%; height: 100%; }
.cv-plate img { object-fit: cover; image-rendering: pixelated; }

/* KEN BURNS, INTENDED (S122). The drift was 44 s of 1.06 → 1.13 on
   `alternate` — which means half of every cycle is zooming OUT, and at 7 %
   over three quarters of a minute nobody ever saw it move. Eivind asked for
   "slight intentional ken burns", and intentional is the operative word: a
   single image now drifts INWARD only, 1.0 → 1.10 over 26 s, and comes back
   out over the same time rather than snapping. Still slight; no longer
   invisible. The S74 reasoning is untouched — the faintest motion reads as
   "waiting", a frozen frame reads as "broken". */
.cv-plate:not(.cv-many) img,
.cv-plate:not(.cv-many) .cv-grad {
  animation: cvDrift 26s ease-in-out infinite alternate; transform-origin: 50% 44%; }
@keyframes cvDrift { from { transform: scale(1); } to { transform: scale(1.1) translate3d(-1%, -1.2%, 0); } }

/* THE ROTATING JACKET (S122, Eivind: "maybe the cover page will show several
   of the graphics rotating, so we get a feel for the world"). N layers on one
   cycle, each delayed by its own slot — no timer, and nothing to tear down,
   because an animation on a display:none element is not running.

   The KEYFRAMES are generated per cover (js/cover.js), because a slot is
   1/n of the cycle and keyframe percentages cannot be computed in CSS. The
   first draft wrote them for three plates and called the rest "graceful
   degradation"; it was not — at five plates two layers are visible at once.
   Everything that does NOT depend on n stays here. */
.cv-many .cv-slide {
  opacity: 0;
  transform-origin: 50% 44%;
  animation-duration: var(--cv-cycle);
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}
/* the fade is linear, the drift eases out — a cross-fade that eases reads as
   a stutter, a zoom that does not reads as a mechanism */
.cv-many .cv-slide { animation-timing-function: linear, ease-out; }
@media (prefers-reduced-motion: reduce) {
  /* the rotation stays — it is the CONTENT, several pictures of the world —
     but the drift goes, which is the part that is motion for its own sake */
  .cv-many .cv-slide { animation-name: var(--cv-show); }
}
.cv-grad::after { content: ''; position: absolute; inset: 0; opacity: .14;
  background: repeating-linear-gradient(45deg, transparent 0 9px, #000 9px 10px); }
.cv-bare { background: linear-gradient(160deg, #1a1726, #0d0b14); }
/* dark where the words are, clear where the picture is worth seeing */
.cv-veil { position: absolute; inset: 0; background:
  linear-gradient(to bottom, rgba(8,7,14,.72) 0%, rgba(8,7,14,.28) 26%,
                  rgba(8,7,14,.42) 56%, rgba(8,7,14,.93) 100%); }

/* THE TYPE — the point of the whole screen.
   Its own scrim, not the page veil. The veil is tuned to keep the PICTURE
   worth looking at, so it is deliberately thin across the middle — which is
   exactly where a tagline lands, and on the Benbow plate that put the line
   straight over the inn's hanging sign. Type that competes with detail loses.
   An ellipse behind the words costs the image almost nothing and means the
   cover works over any plate, including ones nobody has drawn yet. */
.cv-type { position: relative; z-index: 2; text-align: center;
  padding: clamp(30px, 7vh, 74px) clamp(20px, 6vw, 70px) clamp(20px, 4vh, 40px); }
.cv-type::before {
  content: ''; position: absolute; inset: -6% -4% -14%; z-index: -1;
  background: radial-gradient(ellipse 62% 58% at 50% 42%,
    rgba(8,7,14,.86) 0%, rgba(8,7,14,.7) 42%, rgba(8,7,14,.34) 68%, rgba(8,7,14,0) 100%);
}
.cv-imprint { font: 10px/1 var(--font-display); letter-spacing: .46em;
  color: var(--bronze); text-transform: uppercase;
  text-shadow: 0 1px 8px #000; margin-bottom: clamp(14px, 3vh, 26px);
  /* the tracking pushes the wordmark right by one gap; pull it back so it
     reads as centred rather than as nearly centred */
  text-indent: .46em; }
.cv-title { margin: 0; font-family: var(--font-title); font-weight: normal;
  color: var(--gold); display: flex; flex-direction: column; gap: .18em;
  text-shadow: 0 2px 18px rgba(0,0,0,.8), 0 0 2px rgba(0,0,0,.9); }
/* TRACKING TIGHTENS AS THE SIZE GROWS — the one rule of display setting that
   a single fixed letter-spacing always breaks. --cv-scale steps the whole
   thing down for a long title rather than letting it wrap anywhere it likes. */
.cv-l { font-size: calc(clamp(30px, 7.4vw, 74px) * var(--cv-scale, 1));
  line-height: 1.03; letter-spacing: .03em; }
.cv-l.cv-sub { font-size: calc(clamp(15px, 2.5vw, 27px) * var(--cv-scale, 1));
  letter-spacing: .2em; color: var(--gold-deep); text-transform: uppercase;
  font-family: var(--font-display); opacity: .92; }
/* the rule is drawn to the measure of the words above it, never the page */
.cv-rule { width: clamp(70px, 16vw, 190px); height: 1px; margin: clamp(16px, 3.4vh, 30px) auto 0;
  background: linear-gradient(90deg, transparent, var(--bronze) 22%, var(--gold) 50%, var(--bronze) 78%, transparent); }
/* THE ACT LINE (S123) — where the story starts, set like a playbill rather
   than like blurb: small, tracked wide, the same register the act cards use
   at every later curtain, so the cover and the boundaries speak one language.
   `.cv-tag` is kept for anything that still sets a tagline; the COVER no
   longer prints one. */
.cv-act { margin: clamp(14px, 2.6vh, 24px) auto 0;
  font: clamp(11px, 1.25vw, 14px)/1.5 var(--font-display); letter-spacing: .34em;
  text-transform: uppercase; color: var(--gold-deep); text-indent: .34em;
  text-shadow: 0 1px 10px rgba(0,0,0,.9); }
.cv-tag { max-width: 34ch; margin: clamp(14px, 2.6vh, 24px) auto 0;
  font: italic clamp(13px, 1.6vw, 18px)/1.65 var(--font-title);
  color: var(--ink-soft); text-shadow: 0 1px 10px rgba(0,0,0,.9); }

/* THE ESTABLISHING CAPTION (S117) — the line a film lays over the wide shot
   before anybody speaks. S123, Eivind: "i want it to say somewhere deep in
   the caribbean... only slight, as the main sub text. we keep it intentional,
   looking like subtitles." So it is set as a SUBTITLE and not as display
   type: smaller, plainer, sitting low in the frame where a subtitle sits,
   with the modest letter-spacing of one rather than the wide tracking of a
   title. It is the cover's only sub text now — the blurb is gone. It fades
   in rather than being there, because a caption that has always been on
   screen is a label. */
.cv-card { position: absolute; left: 0; right: 0; top: 72%; z-index: 2;
  text-align: center; padding: clamp(8px, 1.2vh, 14px) clamp(20px, 6vw, 70px);
  font: clamp(12px, 1.45vw, 17px)/1.5 var(--font-display);
  letter-spacing: .04em; color: rgba(246,242,232,.92);
  text-shadow: 0 2px 12px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.9);
  animation: cvCard 2.6s ease-out .5s both; }
@keyframes cvCard { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .cv-card { animation: none; } }
/* NO BAND (S214, Eivind: "that shadow that stretches across the screen, let
   us remove that again"): the caption used to sit on a full-width dark
   gradient; the text-shadow above is all the legibility it gets now. */

/* THE CONTROLS get out of the way */
.cv-foot { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  padding: 0 clamp(18px, 5vw, 60px) clamp(20px, 4vh, 40px);
  display: flex; flex-direction: column; align-items: center; gap: 12px; }
.cv-acts { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.cv-go { min-height: 52px; padding: 0 clamp(22px, 4vw, 42px); cursor: pointer;
  font: 13px/1 var(--font-display); letter-spacing: .24em;
  border-radius: 10px; border: 1px solid var(--gold-bot);
  background: var(--gold-fill, linear-gradient(var(--gold-top), var(--gold-bot)));
  color: var(--on-gold); box-shadow: 0 4px 16px rgba(0,0,0,.5); }
.cv-go.cv-second { background: rgba(13,10,19,.66); color: var(--gold);
  border-color: var(--bronze); box-shadow: none; }
.cv-go:hover { filter: brightness(1.08); }
.cv-cog { min-height: 52px; padding: 0 16px; cursor: pointer;
  font: 10px/1 var(--font-mono); letter-spacing: .18em;
  background: rgba(13,10,19,.5); color: var(--dim);
  border: 1px solid var(--line); border-radius: 10px; }
.cv-cog:hover, .cv-cog.on { color: var(--gold); border-color: var(--bronze); }

.cv-opts { display: flex; flex-wrap: wrap; gap: 8px 18px; justify-content: center;
  background: rgba(13,10,19,.82); border: 1px solid var(--edge); border-radius: 10px;
  padding: 12px 16px; backdrop-filter: blur(3px); }
.cv-opts[hidden] { display: none; }
.cv-opts label { display: flex; align-items: baseline; gap: 8px; color: var(--dim); font-size: 12px; }
.cv-opts b { font: 10px/1 var(--font-display); letter-spacing: .18em; color: var(--ink-soft); }
.cv-opts i { font-style: normal; font-size: 10.5px; color: var(--dimmest); }
.cv-opts input[type=text] { width: 92px; background: var(--control); color: var(--ink);
  border: 1px solid var(--line); border-radius: 6px; padding: 4px 6px; font: 12px var(--font-mono); }
.cv-source { max-width: 74ch; text-align: center; color: var(--dimmest);
  font: 10px/1.5 var(--font-mono); letter-spacing: .04em; }

@media (prefers-reduced-motion: reduce) { .cv-plate:not(.cv-many) img { animation: none; transform: scale(1.04); } }
/* short landscape (a laptop in a hall): the type gives ground before the
   buttons do — an unreachable BEGIN is worse than a smaller title */
@media (max-height: 560px) {
  .cv-type { padding-top: clamp(14px, 3vh, 26px); }
  .cv-l { font-size: calc(clamp(24px, 5vw, 44px) * var(--cv-scale, 1)); }
  .cv-tag, .cv-card { display: none; }
}

/* ============================================================
   ASK — the non-blocking prompt() stand-in (js/ask.js). Shared here
   (not maker.css) because worlds.html uses it too. Floats beside the
   element that opened it instead of freezing the page — a native
   prompt()/confirm() blocks the main thread, which starves this
   studio's look-ahead music scheduler (js/clock.js) exactly the way a
   backgrounded tab does, only harder, and reads as the score randomly
   pausing and resuming.
   ============================================================ */
.ask-pop {
  position: fixed; z-index: 80; display: flex; align-items: center; gap: 8px;
  background: var(--card); border: 1px solid var(--gold); border-radius: var(--r-ctl);
  box-shadow: var(--shadow-card); padding: 8px 9px; max-width: min(320px, 90vw);
  animation: askIn .16s ease-out both;
}
@keyframes askIn { from { opacity: 0; transform: translateY(-4px); } }
.ask-pop label { font-size: 11px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ask-pop input {
  font: inherit; font-size: 12px; width: 140px; min-width: 0;
  background: var(--sunken); border: 1px solid var(--line); border-radius: var(--r-ctl);
  color: var(--ink); padding: 4px 7px;
}
.ask-pop input:focus { outline: none; border-color: var(--gold); }
.ask-btns { display: flex; gap: 4px; flex: 0 0 auto; }
.ask-pop button {
  font: inherit; font-size: 12px; line-height: 1; cursor: pointer; width: 24px; height: 24px;
  border-radius: var(--r-ctl); border: 1px solid var(--edge); background: var(--control); color: var(--ink-soft);
}
.ask-pop .ask-ok:hover { border-color: var(--gold); color: var(--gold); }
.ask-pop .ask-x:hover { border-color: var(--red); color: var(--red); }
@media (prefers-reduced-motion: reduce) { .ask-pop { animation: none; } }

/* ---- HOUSED (S225) — a page framed inside house.html: the valance is the
   masthead, so the page's own masthead and the player's head stand down;
   the room's orbs (#setBtn) stay, and sit under the valance, clickable. ---- */
html.st-housed header.st-top, html.st-housed .pl-head { display: none; }
/* the framed player's own settings orb keeps its box (its sheet hangs from it) but the valance's orb is the one you see */
html.st-housed #settings #setBtn { visibility: hidden; }
/* …and its sheet hangs under the valance's right orb: 14px in from the stage's right edge, just under the valance (the frame starts there) */
html.st-housed #setPanel { left: auto !important; right: 14px !important; top: 14px !important; max-height: calc(100vh - 30px); }
/* ONE GRID FOR THE ROOMS (S229, Eivind: "make a better fit for all rooms, so
   we are in the same grid and on line always"): inside the house every maker
   room starts 24px in and 24px down, and a long room (SETS) scrolls inside
   its own box instead of the page, as CAST already does. */
html.st-housed body.maker #library, html.st-housed body.maker #castpage, html.st-housed body.maker #musicdesk { margin: 0 !important; max-width: none !important; }
/* S240: …and not by trapping it in the card either. This clipped SETS to the
   viewport and made the card the page's only scroller, so the wheel only
   worked while the pointer was over it — the same dead gutter CAST had. The
   room scrolls as a page now, from anywhere in it. */
html.st-housed body.maker #library { box-sizing: border-box; }
html.st-housed body { padding-top: 12px !important; }
/* …but not the player's. Its hall is sized to the frame exactly, so 12px at
   the top is 12px of dead cloth at the bottom (S293). The player is the only
   room with a .pl-head — deck, bench, gallery and the shop keep their 12px. */
html.st-housed body:not(.maker):has(.pl-head) { padding-top: 0 !important; }
html.st-housed body.maker #stage { margin-top: 12px !important; }   /* WRITE/SHOOT's stage sat 12px above the other rooms' boxes; now all four start at one line */
html.st-housed .st-bar { padding-left: 18px; }

/* THE 3D GLASSES ROW (S240) — the hint under the label says why the switch is
   doing nothing when a plate has no baked depth map beside it. A switch that
   silently does nothing is worse than one that explains itself. */
#setGlassesRow { flex-wrap: wrap; }
#setGlassesHint { flex-basis: 100%; margin-left: 30px; font-style: normal;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--dimmest); }

/* ── THE SPYGLASS AND THE INSERT (S312, js/detail-view.js) ──────────────
   The glass: the frame dark round one bright circle, a brass rim whose
   inner ring fills as the glass settles on a detail (--spy-p). Over Live 3D
   the circle is a hole onto the live canvas; over a still, the still
   magnified. The insert: a close-up framed over the dimmed plate. Both sit
   over the hot zones (z 3): while either is up, nothing else is clickable. */
#panel .spy { position: absolute; inset: 0; z-index: 6; cursor: crosshair; overflow: hidden; }
/* the dark round the circle is the circle's own shadow, spread past any frame */
#panel .spy-glass { position: absolute; left: 50%; top: 50%; width: min(62vmin, 74%); aspect-ratio: 1;
  transform: translate(-50%, -50%); border-radius: 50%; overflow: hidden;
  box-shadow: 0 0 0 200vmax rgba(4,4,6,.94); }
#panel .spy:not(.live) .spy-glass { background: #000; }
#panel .spy-flat { position: absolute; inset: 0; background-repeat: no-repeat; }
#panel .spy.live .spy-flat { display: none; }
#panel .spy-detail { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity .6s ease; }
#panel .spy-detail.on { opacity: 1; }
#panel .spy-rim { position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  box-shadow: inset 0 0 0 5px #8a6a2c, inset 0 0 0 7px #d8b06a, inset 0 0 calc(18px + 30px * var(--spy-p, 0)) rgba(0,0,0,.75); }
#panel .spy-line { position: absolute; left: 50%; bottom: 4%; transform: translateX(-50%); max-width: 80%;
  padding: 6px 12px; border-radius: 6px; background: rgba(10,8,4,.82); color: #f1e3c0;
  font-size: 14px; text-align: center; opacity: 0; transition: opacity .4s; pointer-events: none; }
#panel .spy-line.on { opacity: 1; }
#panel .spy-line b { color: #d8b06a; font-weight: 600; margin-right: 4px; }
#panel .spy-hint { position: absolute; top: 3%; left: 50%; transform: translateX(-50%);
  font-size: 11px; letter-spacing: .06em; color: rgba(241,227,192,.55); pointer-events: none; }
#panel .insert { position: absolute; inset: 0; z-index: 6; display: grid; place-items: center;
  background: rgba(4,4,6,.72); cursor: zoom-out; animation: insIn .35s ease; }
#panel .ins-frame { margin: 0; max-width: 78%; max-height: 86%; display: flex; flex-direction: column; align-items: center; }
#panel .ins-frame img { max-width: 100%; max-height: 68vh; min-height: 0; border: 4px solid #d8b06a;
  border-radius: 4px; box-shadow: 0 10px 40px rgba(0,0,0,.6); object-fit: contain; }
#panel .ins-frame figcaption { margin-top: 8px; color: #f1e3c0; font-size: 14px; text-align: center; }
#panel .ins-frame figcaption b { color: #d8b06a; margin-right: 6px; }
@keyframes insIn { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { #panel .insert { animation: none; } #panel .spy-detail { transition: none; } }
/* THE GLASS ON A MACHINE (S318, js/detail-view.js drawMachine) — retro and
   the consoles over a still: one canvas in the bitmap's own pixels. The
   painted glass (its see-through shade, smooth edge and brass) is not drawn:
   those are colours the machine does not have. */
#panel .spy.machine .spy-glass { display: none; }
#panel .spy-machine { position: absolute; inset: 0; width: 100%; height: 100%; image-rendering: pixelated; }
#panel .det-hint { position: absolute; left: 10px; bottom: 8px; z-index: 2; pointer-events: none;
  padding: 3px 8px; border-radius: 4px; background: rgba(10,8,4,.6); color: rgba(241,227,192,.8);
  font-size: 11px; letter-spacing: .04em; }
