/* ============================================================================
   SHELL.CSS · the guided-sequence chrome: the stage, the card box, the chapter
   dots, the next chip, and THE CURTAIN.

   The curtain is the #1 quality item on this build (FULL-DECK-PLAN A1), so the
   numbers here are deliberate and every one of them is parameterised in
   shell.js (DS.Shell.TUNE) for the orchestrator to tune by feel:

   - It is NEVER white. A white flash between two moody lit dioramas is the
     single most page-like thing a screen can do. The curtain's peak is a warm
     near-black with an amber core, so a transition reads as the lamp swinging
     away rather than as a browser navigating.
   - It carries DRIFTING DUST (a small 2D canvas, ~44 motes), which is what makes
     it read as air in a lit room instead of a CSS fade.
   - The incoming chapter SETTLES: it arrives a touch low and a touch small and
     springs to rest on an overshooting curve. Nothing snaps.
   - There is no spinner, ever. The next chapter's bytes are fetched during the
     current one, so the cover is a beat of atmosphere, not a wait.

   GRIP ZONES (design doc 2.2). The tablet rests on a lap, held in two hands: the
   bottom 8% is thumbs. `--cur-grip-b` is the floor everything interactive stands
   on, and desktop gets its own value because a desktop is not held.
   ========================================================================== */

:root {
  /* THE GRIP ZONE IS 8% OF WHAT THE CHILD CAN SEE, not of the layout viewport
     (exp-26 UI-01: at 616 px visible inside Telegram's browser the 8vh grip
     was computed against a taller frame and the dock rode up over "See it
     again"). --vv-gap-b climbs from the layout viewport's bottom edge to the
     VISUAL bottom, so every consumer of this one var — the dots, the next
     chip, the keep padding, the card slot's band, chromeFloor() — anchors to
     the edge the child actually sees. Falls back to exactly the old numbers
     where viewport.js is absent. */
  /* two forms, one number: --cur-grip-vis is the grip measured INSIDE the
     visual frame (for boxes that already end at the visual bottom, like the
     keep column at height --vvh); --cur-grip-b adds the layout→visual gap on
     top (for anything anchored to the LAYOUT viewport's bottom edge — the
     fixed dock, the next chip, the card/game slots). Using the
     gapped form inside a visual-height box double-counts the gap; that is a
     measured bug (keep card slid under the band on the forced-shrink control). */
  --cur-grip-vis: max(calc(env(safe-area-inset-bottom) + 12px), calc(var(--vvh, 100vh) * .08));
  --cur-grip-b: calc(var(--vv-gap-b, 0px) + var(--cur-grip-vis));
  /* HOW MUCH BOTTOM BAND THE CARD SLOT RESERVES, in one number, because two
     places read it and they must not drift.

     IT USED TO BE THE MENTOR'S BAND (`--cur-vo-band`: 152px, 194px on the
     narrow breakpoint, 158px when the visual viewport was tight) — the VO
     slot's own 74px offset over the grip, plus the tallest bubble that slot
     could hold. The owner's caption ruling of 2026-08-31 deleted the bubble,
     so that arithmetic is gone: a reserve for an element that is never built
     is a hole, and it measured 82px (390x844), 80px (390x616), 98px (768x1024)
     and 62px (1440x900) of nothing between the card's own tap hint and the
     chapter dock.

     IT MAY NOT GO TO ZERO EITHER, and that is not a matter of taste — it was
     measured. With the band removed outright the card and its hint fall
     straight onto the dock: hint over dock by 15.8px at 390x844 and 13.6px at
     1440x900, and at 390x616 / 375x548 the CARD ITSELF lands 57px inside the
     dock. The strip was never only the bubble's.

     So the number is re-derived from what is actually down there:
       · the dock — 60px of pill standing on the grip;
       · the card's own tap hint, which hangs 3.5rem below the card and so
         eats ~55px of the band before the dock's 60 even begins;
       · below 720px ONLY, the forward chip, which steps up one line to stop
         fighting the centred dock (see `.cur-next`) and stands from grip+62 to
         grip+150 — directly over the card's lower right at phone widths.
     136 = 55 (hint) + 60 (dock) + 21 air, and it clears the chip's >=720
     position (grip+72) by 64. The hint and the chip never coexist: the hint is
     hidden the moment the card plays, which is the same moment the chip
     arrives. */
  --cur-card-band: 136px;
  /* The printed card's own geometry — art is 720x1008 (5:7). The width used to
     be capped at 54vh, which is a LAYOUT-viewport height: at 616 px visible the
     card overflowed the band gap and its top slid under the prompt pill — the
     exact "Watch what happens over the dinosaur's head" screenshots (UI-02).
     The cap is the space BETWEEN THE BANDS: visual height minus the measured
     top band (--cur-band-t, published by guide.js; fallback = one chip row +
     one prompt line) minus the bottom band (the same --cur-card-band that
     .cur-cardslot pads with, over the grip, + 10px of air at the top), divided
     by the card's own 1008/720 aspect. At 768+ the 400px cap binds and the card
     is unchanged; where the band was binding, the card is what gets the
     mentor's old strip back — measured 186.4px -> 204.7px at 390x616. */
  --card-w: min(86vw,
              calc((var(--vvh, 100vh) - var(--cur-band-t, 101px) - var(--cur-grip-vis, 80px)
                    - var(--cur-card-band, 136px) - 10px) / 1.4),
              400px);
  --card-r: 6.5% / 4.6%;
  --clay-lit: rgba(255, 238, 208, .34);
  --clay-dark: rgba(52, 34, 16, .52);
  --clay-edge: rgba(255, 232, 190, .40);
  --ink: #f7ecda;
}
@media (min-width: 1024px) { :root { --cur-grip-vis: 28px; } }

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%; width: 100%;
  background: #0b0806;
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
  color: var(--ink);
  font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* THE STAGE. touch-action:none is load-bearing: a swipe here must not navigate
   back, pull to refresh, or scroll. It is the difference between an app and a
   web page, and it broke a previous attempt. */
#cur-shell {
  position: fixed; inset: 0;
  touch-action: none;
  /* the warm moody plate every chapter sits on, so an empty frame is still a lit
     place. Warm-shifted from come-alive's cool #0b0a0f — this deck is clay. */
  background: radial-gradient(122% 82% at 50% 10%, #2a1e16 0%, #170f0b 46%, #0b0806 100%);
}

.cur-layer { position: absolute; inset: 0; }
.cur-place { z-index: 0; overflow: hidden; }
.cur-persist { z-index: 1; }
.cur-chapters { z-index: 2; }

/* THE PLAY CHAPTER'S LAYER MUST NOT EAT THE GAME'S TAPS.
   The embedded game lives in the persist layer (z-index 1) and keeps running across
   chapters; the chapter layer sits above it at z-index 2. On the play chapter that
   upper layer is visually empty but still hit-testable, so `elementsFromPoint` over
   the game returned `.cur-chapters`, not the IFRAME — every tap landed on nothing.
   The screen looked perfect and the game was completely unplayable: sixteen honest
   taps on the egg produced crackPct 0. Found while building card two; card one had
   it too and its driver missed it, because that driver checked the food was ON SCREEN
   and never actually played. Anything the play chapter genuinely needs to be tappable
   (the guide chrome) re-enables pointer-events on itself. */
#cur-shell[data-ch="play"] .cur-chapters { pointer-events: none; }

/* THE VOICE BUBBLE USED TO STEAL TAPS HERE, and the fix is now structural.
   On the hatch step the bubble sat over the TOP of the egg — the biggest part of it and
   exactly where the prompt pointed — and it was tappable because tapping it skipped the
   line. A tap there returned `DIV.cur-vo-bubble` as the topmost element and did nothing,
   while taps at its middle cracked the shell: a child taps the obvious place, nothing
   happens, and they conclude the game is broken. This rule turned the bubble transparent
   to taps during PLAY. The caption ruling (2026-08-31) deleted the bubble outright, so
   there is nothing left to make transparent and the whole class of bug is gone with it. */

/* ---- THE PLACE ------------------------------------------------------------
   The card does not sit on a dark screen. It sits in the same lit clay world
   the games are in — that is the entire thesis of this fourth attempt, and a
   beautiful card on #0b0806 is exactly the "floating image on a web page" the
   owner rejected three times.

   This is the DOM answer to app/feed/js/scene.js: the same plates, the same
   light, the same layer order — backdrop far, warm clay ground tray catching
   the light near the camera and receding into dark, dust drifting through the
   beam, one warm key from the UPPER-LEFT (the direction shadow.json's shear
   encodes, so the card's cast shadow and the world agree), cool vignette.
   Done in CSS + one 2D canvas rather than three.js because this layer has to be
   on screen in the first 200 ms, and 608 KB of WebGL before the child sees
   their card is the wrong trade. The plates are the committed art/gen ones,
   re-encoded to webp: 5.7 MB of PNG → 213 KB. */
.cur-place-sky {
  position: absolute; inset: 0;
  background: url("place/backdrop.webp") 50% 30% / cover no-repeat;
}
.cur-place-ground {
  position: absolute; left: -4%; right: -4%; bottom: -2%;
  height: var(--cur-ground-h, 52%);
  background: url("place/ground.webp") 50% 64% / cover no-repeat;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,.6) 14%, #000 36%);
  mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,.6) 14%, #000 36%);
}
/* the tray's own light: dark where it recedes, warm where the lamp pools near
   the front — the shader in scene.js, expressed as two gradients */
.cur-place-ground::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(90% 74% at 40% 116%, rgba(255,196,116,.30) 0%, rgba(255,168,86,0) 64%),
    linear-gradient(to bottom, rgba(12,9,6,.88) 0%, rgba(18,12,7,.40) 24%, rgba(0,0,0,0) 58%);
}
.cur-place-dust { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.cur-place-key {
  position: absolute; inset: 0; mix-blend-mode: screen;
  background: radial-gradient(56% 44% at 21% 7%, rgba(255,214,140,.36) 0%, rgba(255,180,92,.11) 44%, rgba(255,170,80,0) 74%);
}
.cur-place-vig {
  position: absolute; inset: 0;
  background: radial-gradient(122% 96% at 50% 40%, rgba(20,26,20,0) 42%, rgba(13,18,14,.44) 78%, rgba(5,8,6,.82) 100%);
}
/* the keep chapter stands its things further forward, so the ground comes up */
#cur-shell[data-ch="keep"] { --cur-ground-h: 62%; }

/* every chapter's own root — absolutely positioned, so two chapters can never
   push each other around during a swap (no layout jump). */
.cur-ch {
  position: absolute; inset: 0;
  display: grid; place-items: center;
}
.cur-ch.settling { animation: curSettle var(--cur-reveal, 420ms) cubic-bezier(.16, 1.16, .34, 1) both; }
@keyframes curSettle {
  from { opacity: .0; transform: translateY(14px) scale(.986); }
  to   { opacity: 1;  transform: none; }
}

/* persisted things position themselves; the layer is a bare canvas.

   The card does not centre in the VIEWPORT, it centres in what is left of it:
   the bottom band belongs to the dots strip over the grip (and, at phone
   widths, to the forward chip's own line above it), and the card's own tap-hint
   hangs 3.5rem below the card. Centring in the whole screen put the hint under
   the chrome — measured, seen in shots/w390-0-arrive.jpg, fixed here rather
   than by shrinking the card. */
.cur-cardslot {
  position: absolute; inset: 0; display: grid; place-items: center;
  /* the TOP band is reserved (exp-26 UI-02): guide.js publishes the measured
     bottom edge of the step-chip + prompt stack as --cur-band-t, and the card
     centres in what is left between the bands — so the prompt can no longer
     stand on the dinosaur's head at ANY height.
     THE BAND BELOW IS --cur-card-band, over the grip. It used to be the
     mentor's bubble's reserve; the caption ruling removed the bubble and the
     number was re-derived from the printed chrome that is still there (see
     :root above). --card-w's arithmetic uses the SAME var plus 10px of air
     under the top band; the two must never drift, which is why the number was
     shared in the first place. */
  padding-top: calc(var(--cur-band-t, 101px) + 10px);
  padding-bottom: calc(var(--cur-grip-b) + var(--cur-card-band, 136px));
}
/* The embedded game gets a VIEWPORT, not an overlap. Its own UI lives along its
   bottom edge (the feed's food tray), so the iframe stops where the dots strip
   begins and the game lays itself out inside what it is given. */
.cur-gameslot { position: absolute; left: 0; right: 0; top: 0; bottom: calc(var(--cur-grip-b) + 62px); }
/* THE APRON (v5, owner bug report): the slot's bottom reserve keeps the game
   clear of the dock, but it exposed the page's raw near-black body — a black
   band across the bottom of the one step that must feel like a toy (T. rex
   feed, 844-tall screens). A warm clay apron now stands behind the reserve so
   every game's ground fades into a stage floor instead of into nothing. It
   overlaps the slot's last pixels slightly so no seam line can show. */
.cur-gameslot::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 100%; margin-top: -14px; height: calc(var(--cur-grip-b) + 76px);
  pointer-events: none;
  background: linear-gradient(180deg, rgba(46, 28, 12, 0) 0%, rgba(46, 28, 12, .85) 14px, #2a1a0c 30%, #1d1207 100%);
}
.cur-gameslot iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; background: transparent; }

/* ---- THE CARD BOX ---------------------------------------------------------
   One box, shared by the arrive chapter (the printed card) and the alive chapter
   (the film). Identical width, aspect, corners and shadow — measured at 0.00px
   difference by the 09-fix-pass driver — so the film cannot read as a cut to a
   differently-shaped thing. Do not give the video its own holder. Ever. */
/* the card and its shadow are siblings: the card breathes, the shadow stays on
   the earth (a shadow that rides along with the object is the tell of a sticker) */
.cur-card-stand { position: relative; display: block; width: var(--card-w); }
.cur-card-shadow {
  position: absolute; left: 50%; bottom: -3.2%;
  width: 122%; height: 17%; pointer-events: none;
  /* light from the UPPER-LEFT → the shadow lies down and to the right, sheared
     the way shadow.json shears the animals' (0.34) */
  transform: translateX(-45%) skewX(-17deg);
  background: radial-gradient(50% 50% at 50% 50%, rgba(20,11,3,.88) 0%, rgba(26,15,5,.44) 46%, rgba(26,15,5,0) 75%);
  filter: blur(10px);
  animation: curShadowBreathe 5.5s ease-in-out infinite;
}
/* the tight dark core where the card actually meets the ground */
.cur-card-shadow::after {
  content: ""; position: absolute; left: 12%; right: 10%; top: 20%; height: 40%;
  border-radius: 50%; background: rgba(12,6,1,.8); filter: blur(6px);
}
@keyframes curShadowBreathe {
  0%,100% { opacity: 1;   transform: translateX(-45%) skewX(-17deg) scale(1, 1); }
  50%     { opacity: .76; transform: translateX(-45%) skewX(-17deg) scale(.95, .86); }
}

.cur-cardwrap {
  position: relative;
  width: var(--card-w);
  aspect-ratio: 720 / 1008;
  border-radius: var(--card-r);
  transform: translateZ(0);
  /* subtler than come-alive's: there the card was alone in a void and the float
     was the only life in the frame. Here the world is alive around it, so the
     card only breathes. */
  animation: curFloat 5.5s ease-in-out infinite;
  will-change: transform;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,.45), 0 18px 40px -14px rgba(0,0,0,.7);
}
@keyframes curFloat {
  0%,100% { transform: translateY(0) rotate(-.35deg); }
  50%     { transform: translateY(-.9%) rotate(.35deg); }
}
.cur-cardimg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: var(--card-r); display: block;
  box-shadow: 0 0 0 1px rgba(255,255,255,.04) inset;
}
/* alive-at-rest: printed paper catching the lamp. Stands down once it is film. */
.cur-cardwrap::after {
  content: ""; position: absolute; inset: 0; border-radius: var(--card-r);
  background: linear-gradient(115deg, transparent 38%, rgba(255,255,255,.16) 50%, transparent 62%);
  background-size: 240% 240%; mix-blend-mode: screen; opacity: 0;
  animation: curSheen 6.5s ease-in-out infinite; pointer-events: none;
}
@keyframes curSheen {
  0%, 62%, 100% { background-position: 130% 0; opacity: 0; }
  18% { opacity: .5; }
  40% { background-position: -30% 0; opacity: 0; }
}
.cur-cardwrap.playing::after { animation: none; opacity: 0; }
.cur-cardwrap.playing { cursor: default; }

.cur-film {
  position: absolute; inset: 0; border-radius: var(--card-r); overflow: hidden;
  opacity: 0; pointer-events: none; transition: opacity .18s ease; transform: translateZ(0);
}
.cur-film.on { opacity: 1; }
.cur-film video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: var(--card-r); display: block;
  opacity: 0; transition: opacity .16s linear;
}
.cur-film video.show { opacity: 1; }

/* the wordless "tap me" tell: a ring that breathes under the card */
.cur-tap {
  position: absolute; left: 50%; bottom: -3.5rem; transform: translateX(-50%);
  display: flex; align-items: center; gap: .55rem;
  color: #f4e9d8; font-size: clamp(13px, 3.4vw, 16px); font-weight: 600;
  white-space: nowrap; text-shadow: 0 1px 8px rgba(0,0,0,.6);
  pointer-events: none; opacity: 0;
  animation: curFadeIn .6s ease .7s forwards, curBreathe 2.2s ease-in-out 1.4s infinite;
  transition: opacity .2s ease;
}
.cur-tap .ring {
  width: 1.6rem; height: 1.6rem; border-radius: 50%; border: 2px solid #ffd27a;
  display: grid; place-items: center; animation: curRing 2.2s ease-out infinite;
}
.cur-tap .ring::after { content: ""; width: .5rem; height: .5rem; border-radius: 50%; background: #ffd27a; }
@keyframes curFadeIn { to { opacity: 1; } }
@keyframes curBreathe { 0%,100% { opacity: .75 } 50% { opacity: 1 } }
@keyframes curRing {
  0%   { box-shadow: 0 0 0 0 rgba(255,210,122,.45); }
  70%  { box-shadow: 0 0 0 12px rgba(255,210,122,0); }
  100% { box-shadow: 0 0 0 0 rgba(255,210,122,0); }
}
.cur-cardwrap.playing .cur-tap { opacity: 0 !important; animation: none; }

/* ---- CHROME: dots + next chip --------------------------------------------- */
.cur-chrome { position: fixed; inset: 0; z-index: 600; pointer-events: none; }
.cur-chrome > * { pointer-events: auto; }

/* ── THE DOCK IS IN THE MIDDLE, AT EVERY WIDTH, ON EVERY CARD (exp-26 owner) ──
   It used to sit in a BOX that stopped short of the next chip's corner
   (`right: calc(96px + safe)`, re-reserved per-card by guide.js from the chip's
   MEASURED width) and centre inside THAT. So the pill's centre was
   viewport-centre minus half the reserve — 75–87 px left of the middle,
   measured, and 2 px different between two cards at the same width because
   e4bgz's forward word is 3 px wider than h3n8q's. The owner read the same dock
   in three places on three screens and called it what it is.

   The box now spans the whole width and the pill centres in it, so its centre is
   the viewport's centre by construction: no reserve, nothing card-dependent,
   nothing that moves when the chip appears. The SAME symmetric inset is used on
   both sides (`max()` of the two safe-area insets), because an asymmetric
   padding is exactly the bug this rule is replacing.

   THE CHIP'S CORNER IS A REAL COLLISION AND IT IS SOLVED VERTICALLY. Five 52 px
   dots are a 264 px pill; centred at 390 that is 63…327, and the forward chip
   with its word runs 232…386. There is no horizontal answer: a symmetric cap
   that cleared a 145 px worded chip would leave the pill 80 px wide — 16 px per
   dot, a third of the floor A5 §3.1 was written to raise. So below 720 px the
   CHIP steps up one line (see `.cur-next`), which is the only move that costs
   the dots nothing: with the whole width to stand in they stop shrinking
   altogether and go from 46.2 px to the full 52 px they were always asked for. */
.cur-dots {
  position: fixed;
  left: 0; right: 0;
  padding: 0 max(2px, env(safe-area-inset-left), env(safe-area-inset-right));
  bottom: var(--cur-grip-b);
  display: flex; justify-content: center;
  pointer-events: none;
}
.cur-dots-inner {
  pointer-events: auto;
  max-width: 100%;
  display: flex; align-items: center; gap: 2px;
  padding: 3px 6px; border-radius: 34px;
  background: linear-gradient(180deg, rgba(48,32,18,.42), rgba(26,17,10,.56));
  border: 1px solid rgba(255,226,180,.14);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  box-shadow: 0 8px 20px -10px rgba(8,4,1,.7);
}
/* 52px touch target per dot: five of them plus the pill's padding is 264px at
   phone widths, and with the chip on its own line the pill now has the whole
   390px to stand in — so the cells are the honest 52px at every width the deck
   is reviewed at, instead of the 46.2px the old reserve squeezed them to. The
   PRIMARY advance control (the next chip) is the full 88px; these are secondary
   jump targets, and nothing here is ever locked (A3), so a mis-tap costs a child
   nothing but a curtain. */
.cur-dot {
  width: 52px; height: 52px; border: 0; background: none; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.cur-dot-puck {
  width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  background: radial-gradient(circle at 38% 28%, rgba(255,238,208,.16), rgba(52,34,16,.42));
  border: 1.5px solid rgba(255,232,190,.20);
  opacity: .55;
  transition: transform 220ms cubic-bezier(.2,1.4,.35,1), opacity 220ms ease,
              background 220ms ease, border-color 220ms ease, box-shadow 220ms ease;
}
.cur-dot-ico { width: 19px; height: 19px; display: block; filter: drop-shadow(0 1px 2px rgba(15,8,2,.6)); }
.cur-dot.visited .cur-dot-puck { opacity: .82; }
.cur-dot.here .cur-dot-puck {
  opacity: 1; transform: scale(1.34);
  background: radial-gradient(circle at 38% 28%, rgba(255,236,196,.46), rgba(84,52,20,.62));
  border-color: rgba(255,222,164,.72);
  box-shadow: 0 0 0 1px rgba(255,208,140,.18), 0 3px 10px -2px rgba(255,180,90,.35);
}
.cur-dot:active .cur-dot-puck { transform: scale(.9); }
.cur-dot.here:active .cur-dot-puck { transform: scale(1.2); }

/* the one 88px control: "I'm ready, go on" */
.cur-next {
  position: fixed;
  right: max(4px, env(safe-area-inset-right));
  bottom: calc(var(--cur-grip-b) - 16px);
  width: 88px; height: 88px; border: 0; background: none; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity 260ms ease;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.cur-next.on { opacity: 1; pointer-events: auto; }

/* ── BELOW 720 THE CHIP STANDS ABOVE THE DOCK, NOT BESIDE IT ─────────────────
   The dock is centred on the viewport now, so at phone widths its right end
   reaches into the corner the chip used to own. Measured at 390: pill 63…327,
   chip 232…386 — 95 px of overlap, i.e. two whole dots stolen by the control
   that already stole a twelfth of one (D1 major 2). The chip steps up by the
   dock's own height instead. Nothing about it changes: same corner, same 88 px
   touch box, same word, same spring — it is one line higher, and it is one line
   higher on EVERY card and in every chapter, so it still never moves under a
   finger.

   720 IS MEASURED, NOT ROUND. A centred 282 px pill clears a right-anchored
   worded chip when 50vw ≥ 141 + chipW; the widest chip in the deck is ~164 px,
   so the two stop colliding at ~610 px and the query is set clear of it. At 768
   and above nothing here applies and the composition is byte-identical.

   THE MENTOR NO LONGER NEEDS A COLUMN HERE — BUT THE CHIP STILL NEEDS THE BAND.
   The chip's new line used to be the VO band, so the bubble reserved the chip's
   column at these widths and the strip under the card grew to 194px (158px on a
   tight frame) to hold four lines of type. The caption ruling (2026-08-31)
   removed the bubble and that arithmetic with it — but not the reason the strip
   is deep here. The chip on its own line runs from grip+62 to grip+150 and it
   stands over the CARD's lower right at phone widths (measured at 390: card
   26…364, chip 241…386), so --cur-card-band grows to clear it: 150 + 16 of air.
   A tight frame gives that air up rather than its card, which is the band
   contract's own rule — 158 is the chip's line plus 8, and it is exactly what
   the tight bubble reserved before, so 375x548 is byte-identical. */
@media (max-width: 719px) {
  .cur-next { bottom: calc(var(--cur-grip-b) + 62px); }
  :root { --cur-card-band: 166px; }
  html.cur-vv-tight { --cur-card-band: 158px; }
}

/* The 88x88 TOUCH TARGET never scales — only the visible puck springs in. A
   transform on the button shrinks its hit rect mid-animation, which is exactly
   the moment an impatient child aims at it (caught by card-shot.mjs measuring
   79px while the chip was still arriving). */
.cur-next-puck {
  width: 62px; height: 62px; border-radius: 50%;
  display: grid; place-items: center;
  background: radial-gradient(circle at 38% 28%, var(--clay-lit), var(--clay-dark));
  border: 2px solid var(--clay-edge);
  -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
  box-shadow: 0 5px 14px rgba(18,10,3,.42), inset 0 1px 0 rgba(255,255,255,.30);
  transform: translateY(10px) scale(.86);
  transition: transform 380ms cubic-bezier(.18,1.32,.4,1);
}
.cur-next.on .cur-next-puck { transform: none; }
.cur-next:active .cur-next-puck { transform: scale(.9); }
.cur-next.pulse .cur-next-puck { animation: curPulse 2.4s ease-in-out infinite; }
@keyframes curPulse {
  0%, 100% { box-shadow: 0 5px 14px rgba(18,10,3,.42), inset 0 1px 0 rgba(255,255,255,.30), 0 0 0 0 rgba(255,204,132,.32); }
  55%      { box-shadow: 0 5px 14px rgba(18,10,3,.42), inset 0 1px 0 rgba(255,255,255,.30), 0 0 0 13px rgba(255,204,132,0); }
}
.cur-next-ico { width: 30px; height: 30px; display: block; filter: drop-shadow(0 2px 3px rgba(15,8,2,.5)); }

/* ---- THE DIORAMA HOST (meet) ---------------------------------------------- */
.meet-host { position: absolute; inset: 0; }


/* ---- KEEP: the card goes on the shelf -------------------------------------
   Wordless first: the card, then fifteen clay beads of which n are full. The
   numeral under them is a wordmark for the adult in the room — the beads are
   what the child reads, and the closing line says the same thing out loud. */
.cur-keep {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* every number here compresses with the VISUAL height (exp-26 UI-01: at
     616 px this column used to be laid out for 844 and its overflow slid the
     moves under the fixed dock and the card under the prompt pill):
     · the top padding reserves the measured top band, so the guide's chrome
       and the card can no longer stack by construction;
     · the bottom padding reserves the dock's own strip over the grip zone,
       and gives its 96px of air back gradually under height pressure;
     · the gaps breathe at 2.5% of the visible height instead of 3.2vh. */
  gap: clamp(10px, calc(var(--vvh, 100vh) * .025), 30px);
  padding: calc(var(--cur-band-t, 101px) + 8px)
           16px
           calc(var(--cur-grip-vis, 80px) + clamp(64px, calc(var(--vvh, 100vh) * .11), 96px));
  width: 100%; height: var(--vvh, 100%);
  /* the chapter root centres its item; a column exactly as tall as the VISUAL
     viewport must instead start at the top of the layout viewport, or the
     browser-chrome gap would push half of it below what the child can see */
  align-self: start;
}
.cur-keep-card {
  /* 52vw at every geometry the deck was reviewed at; under height pressure the
     card is the item that gives, down to a 64px-wide keepsake, so the shelf,
     the count, the next-dinosaur shelf and both labelled moves stay whole and
     un-overlapped. The 374 is the rest of the column: bead shelf + count +
     the next-dinosaur faces (v5, ~86px) + the moves row + four gaps. */
  width: min(52vw,
           clamp(64px,
             calc((var(--vvh, 100vh) - var(--cur-band-t, 101px) - var(--cur-grip-vis, 80px) - 374px) / 1.4),
             240px));
  aspect-ratio: 720 / 1008;
  border-radius: var(--card-r); position: relative;
  box-shadow: 0 2px 6px rgba(0,0,0,.45), 0 16px 34px -14px rgba(0,0,0,.72);
  animation: curKeepLand 620ms cubic-bezier(.16, 1.2, .34, 1) both;
}
/* it lands ON the ground, so it casts on the ground — same light, same shear */
.cur-keep-card::before {
  content: ""; position: absolute; left: -11%; right: -11%; bottom: -6.5%; height: 15%;
  transform: skewX(-15deg);
  background: radial-gradient(50% 50% at 50% 50%, rgba(20,11,3,.84) 0%, rgba(26,15,5,.4) 46%, rgba(26,15,5,0) 76%);
  filter: blur(9px);
}
.cur-keep-card img {
  position: relative; z-index: 1;
  width: 100%; height: 100%; object-fit: cover; border-radius: var(--card-r); display: block;
}
@keyframes curKeepLand {
  from { transform: translateY(-16px) scale(1.06) rotate(-2.4deg); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
.cur-keep-shelf {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 5px; max-width: min(92vw, 420px);
  padding: 9px 13px; border-radius: 22px;
  background: linear-gradient(180deg, rgba(48,32,18,.40), rgba(26,17,10,.54));
  border: 1px solid rgba(255,226,180,.13);
  box-shadow: inset 0 1px 0 rgba(255,240,214,.09), 0 8px 18px -12px rgba(6,3,1,.8);
}
.cur-keep-bead { width: 17px; height: 17px; display: grid; place-items: center; opacity: .38; }
.cur-keep-bead.full { opacity: 1; filter: drop-shadow(0 1px 2px rgba(15,8,2,.55)); }
.cur-keep-bead.pop { animation: curBeadPop 640ms cubic-bezier(.16, 1.5, .34, 1) 320ms both; }
@keyframes curBeadPop {
  0%   { transform: scale(.3); opacity: 0; }
  55%  { transform: scale(1.28); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.cur-keep-count {
  font-size: clamp(14px, 3.6vw, 17px); font-weight: 700; letter-spacing: .08em;
  color: rgba(247,236,218,.62);
}
/* THE NEXT-DINOSAUR SHELF (v5 wave 1): up to three un-found card faces between
   the count and the camp door — the deck's own invitation to continue. Faces,
   not words: a child who cannot read still reads them. Under height pressure
   the faces give with the viewport, like everything else on this screen. */
.cur-keep-next { display: flex; gap: 12px; align-items: center; justify-content: center; }
.cur-keep-next-card {
  display: block; height: clamp(56px, calc(var(--vvh, 100vh) * .105), 86px);
  aspect-ratio: 720 / 1008; border-radius: 8px; overflow: hidden;
  box-shadow: 0 2px 6px rgba(0,0,0,.5), 0 10px 22px -12px rgba(0,0,0,.7),
              inset 0 0 0 1px rgba(255,232,190,.16);
  transition: transform 220ms ease;
}
.cur-keep-next-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cur-keep-next-card:active { transform: translateY(-4px) scale(1.06); }
.cur-keep-next-card:focus-visible { outline: 3px solid rgba(255,200,120,.9); outline-offset: 3px; }
.cur-keep-home {
  display: flex; align-items: center; gap: 12px;
  min-height: 88px; padding: 0 26px 0 16px; border-radius: 46px;
  text-decoration: none; color: var(--ink); font-size: clamp(16px, 4.2vw, 20px); font-weight: 700;
  background: linear-gradient(180deg, rgba(72,48,26,.72), rgba(42,27,14,.82));
  border: 2px solid var(--clay-edge);
  box-shadow: 0 6px 16px rgba(18,10,3,.46), inset 0 1px 0 rgba(255,255,255,.22);
  transition: transform 160ms cubic-bezier(.2,1.4,.35,1);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.cur-keep-home:active { transform: scale(.96); }
.cur-keep-home-puck {
  width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center;
  background: radial-gradient(circle at 38% 28%, var(--clay-lit), var(--clay-dark));
  border: 2px solid rgba(255,232,190,.34);
}
@media (min-width: 1024px) {
  .cur-keep { flex-direction: row; gap: 46px; }
  .cur-keep-card { width: min(26vw, calc(var(--vvh, 100vh) * .44), 300px); }
  .cur-keep > .cur-keep-shelf, .cur-keep > .cur-keep-count, .cur-keep > .cur-keep-home { margin: 0; }
  /* desktop is its own composition: card on the left, shelf + way home stacked */
  .cur-keep { display: grid; grid-template-columns: auto minmax(320px, 460px);
              grid-template-rows: auto auto auto auto; align-content: center; justify-content: center;
              column-gap: 52px; row-gap: 18px; }
  .cur-keep-card { grid-column: 1; grid-row: 1 / span 4; align-self: center; }
  .cur-keep-shelf { grid-column: 2; grid-row: 1; justify-self: start; }
  .cur-keep-count { grid-column: 2; grid-row: 2; justify-self: start; }
  .cur-keep-next { grid-column: 2; grid-row: 3; justify-self: start; }
  .cur-keep-home { grid-column: 2; grid-row: 4; justify-self: start; }
}

/* ---- THE CURTAIN ---------------------------------------------------------- */
/* Opacity is driven from JS on the same rAF clock as the dust — NOT by a CSS
   transition. A CSS transition only advances once the compositor has produced a
   frame with the start value, and on a phone-sized context mid-video-swap that
   first frame can arrive late enough that the whole 300 ms cover elapses at
   ~0.1 opacity: the curtain "worked" on desktop and silently didn't on the
   primary device (caught at 390 by card-shot.mjs, peak 0.17 vs 0.59). Driving it
   per frame makes cover/hold/reveal frame-accurate on both, and makes TUNE
   changes apply live with no CSS-variable plumbing. */
.cur-curtain {
  position: fixed; inset: 0; z-index: 960; pointer-events: none; opacity: 0;
  /* warm dark with an amber core — the lamp swinging away, never a white flash */
  background:
    radial-gradient(58% 42% at 50% 46%, rgba(255,196,120,.13) 0%, rgba(255,170,90,0) 72%),
    radial-gradient(140% 100% at 50% 40%, #33220f 0%, #17100a 52%, #0a0705 100%);
}
.cur-curtain.closing { pointer-events: auto; }
.cur-curtain canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

@media (prefers-reduced-motion: reduce) {
  .cur-cardwrap, .cur-cardwrap::after { animation: none; }
  .cur-ch.settling { animation-duration: 1ms; }
  .cur-tap { animation: curFadeIn .3s ease forwards; }
}
