/* ============================================================================
   JOURNEY.CSS · everything a card page adds on top of the shared shell, ONCE.

   Loaded AFTER shell.css, so it wins on equal specificity — the per-page
   override contract back.js and vo.js already publish.

   It replaces the two hand-copied `app/card/<slug>/card.css` files. The honest
   finding from card two, and the reason this file is keyed rather than flat:
   **"which game" is a config, but "where THAT game puts its own chrome" is real
   geometry.** So the shared rules are unkeyed, and each game's dodge lives in
   one block selected by `body[data-game=…]`, which journey.js sets from the
   card's config. A ninth card that plays dig inherits dig's block; it writes no
   CSS at all.

   THE STANDING GATE THIS FILE EXISTS TO HONOUR (ROUND-3-PLAN §2, five separate
   incidents): nothing the host draws may lie on the PATH between what the child
   is told to touch and where they are told to take it. Not merely on the target.
   `pointer-events: none` is not a defence — a child aims around anything that
   looks like a control.
   ========================================================================== */

/* ---- LANDSCAPE, AND WHERE IT IS HANDLED ------------------------------------
   A5 §2.2, measured at 844x390: the printed card was clipped by the bottom
   edge, the mentor's bubble (since removed) was drawn OVER it, and the "Tap to
   wake it" prompt sat behind the chapter-dot rail. No orientation rule existed —
   `grep -r orientation app/` returned zero hits.

   The decision (DESIGN B7, "one shared pattern, not 17 local ones") is that the
   five-step spine is composed for a tall frame and is NOT re-laid-out for a
   posture nobody reads a picture book in. Every card page and `/when/` include
   `shared/ui/rotate-hint.js`, which is a `display:none` plate everywhere except
   `(orientation:landscape) and (max-height:500px) and (max-width:1000px)` —
   i.e. phones only, no tablet in any posture, no desktop. It carries one
   adult-register way past it, because nothing in this product is ever locked.

   THERE IS DELIBERATELY NO LANDSCAPE COMPOSITION HERE. A second layout for the
   host chrome would be a second thing to keep correct forever, and the scenes
   themselves already solve by aspect ratio. If that decision is ever reversed,
   this is the file it belongs in — not journey.js, and not seventeen pages.

   ---- THE STEP DOTS ARE A TOUCH TARGET, AND AT 390 THEY WERE 41px ----------
   A5 §3.1: `.cur-dot` measured 40.8 x 52 on a 390px phone — five of them at
   x=15,58,101,143,186 — the only sub-44px clickable control in the product, and
   it is the step navigation. At five, a finger's contact patch is about 10 mm;
   a 3 px margin does not survive that, and the failure mode is aiming at "the
   book one" and landing on "the game one".

   WHY THEY WERE 41 AND NOT THE 52 shell.css ASKS FOR. Nothing overrides the
   width. The strip simply had 226 px to stand in and needed 280: shell.css
   reserves `right: 96px` for the forward chip, and guide.js (which appends the
   chip's WORD and so makes it up to ~220 px wide) re-reserves
   `min(--cur-next-w, 38vw) + 8px` — 156 px at 390. Five 52 px cells, four 2 px
   gaps and 12 px of pill padding do not fit in what is left, so every dot
   shrank by 11 px.

   THE FIX IS ROOM, NOT A BIGGER NUMBER. Three cheap millimetres, in order:
     · the strip starts at 2 px from the edge instead of 8 (it is a bottom
       corner nothing else uses — the back chip is TOP-left);
     · below 560 px the pill gives up its 2 px inter-dot gap and 5 of its 6 px
       of side padding: the pucks are 32 px inside their cells, so the visible
       separation is carried by the cell, not by the gap, and drops from 10.8 px
       to 13.6 px between pucks — it goes UP;
     · `min-width` stops the flexbox shrinking a cell below the floor below.
   Measured at 390 with real touch: **40.8 -> 45.6 px** per dot. The 52 px cells
   at 768/810/1440 are untouched (the media query is off and there is room
   anyway) — this only ever grows a dot, never shrinks one, and a pure
   `elementFromPoint` sweep of every dot's left edge, centre and right edge
   returns the right dot at 320/360/390/414/768/810/1440.

   THE RESERVATION STOPPED LYING (r4 F1 · D1 major 2). When this rule was
   written, guide.js capped the dots' reservation at 38vw while the chip it
   reserved for rendered 164 px — 148 px promised at 390 — so the strip ran
   under the chip and a tap on the last dot's right rim went to the forward
   chip on every card (at 320 the whole last dot was stolen; that was the
   `next chip x dots` note verify-walk printed on `alive`, and it was not
   noise). guide.js now clamps the CHIP and the reservation to the same 60vw
   and reserves the chip's measured width, so the strip and the chip cannot
   overlap; the min() in the floor below quotes guide.js's cap and MUST move
   with it. The honest floors after the fix (guide.js slims the chip word's
   plate below 560 to pay for them): 320 -> 30.5, 360 -> 38.5, 390 -> 44.4,
   414 -> 48.3, 768+ -> 52 — and the elementFromPoint sweep is clean at every
   width, which before the fix it never actually was.

   AND THEN THE WHOLE BARGAIN WAS UNWOUND (exp-26 dock centring, owner report).
   Everything above is the record of a strip trying to live in the ~235 px a
   right-corner chip left it. It cost the dots 6 px each, it cost the deck two
   rounds of tap-thief findings, and it put the dock 75–87 px LEFT of the middle
   of the screen — 2 px differently on e4bgz than on h3n8q, because the
   reservation was the chip's own measured width and that is a per-card number.
   The owner saw a navigation bar that sat in a different place on every card.

   The chip moved up one line instead (shell.css, below 720 px), and the reserve
   is GONE: the strip has the whole width, its centre is the viewport's centre by
   construction, and the cells stop shrinking — 46.2 -> 52 px at 390, 43.2 -> 52
   at 375, 40.8 -> 52 at 320. Six honest millimetres, from geometry rather than
   from three cheap ones.

   THE FLOOR STAYS, as a belt and nothing more. It is still 44 px *or one fifth
   of the room there actually is*, whichever is smaller, so a phone narrower than
   five 44 px cells (or one with enormous side insets) still shrinks its dots
   rather than overflowing the pill — it just never binds at any width the deck
   is reviewed at any more (390 -> 76.4, 320 -> 62.4, both far above 44). The
   min() no longer quotes guide.js's chip cap, because the strip no longer
   reserves anything for the chip: below 720px the chip steps up one line of its
   own (shell.css), which is what stopped the two competing for the same 390px.
   guide.js still MEASURES the chip and publishes `--cur-next-w`, but nothing in
   the deck's layout consumes it now — the mentor's band used to, and the
   caption ruling of 2026-08-31 removed that band with the bubble. */
#cur-shell .cur-dots {
  --cur-dot-floor: calc((100vw
      - 2 * max(2px, env(safe-area-inset-left), env(safe-area-inset-right))
      - 4px) / 5);
}
#cur-shell .cur-dot { min-width: min(44px, var(--cur-dot-floor, 44px)); }
/* THE HERE-DOT'S PUCK IS PAINT, NOT A CONTROL (r4 F1). The current chapter's
   32px puck is scaled 1.34 -> 42.9px, which overflows any cell under 43px —
   real at 320/360 now that the strip reserves the chip's honest width — and a
   scaled puck that can be hit steals the rim of the dot NEXT to it: measured,
   a tap on the keep dot's left rim landed on the play puck. The buttons are
   the controls; their decoration may not take a tap. (A tap through the puck
   still targets the button underneath it — same handler, same behaviour.) */
#cur-shell .cur-dot-puck { pointer-events: none; }
@media (max-width: 559px) {
  #cur-shell .cur-dots-inner { gap: 0; padding: 3px 1px; }
}

/* ---- KEEP: two labelled ways onward ---------------------------------------
   "Back to camp" alone leaves a child who liked it with nowhere to go but the
   browser's back button. The two moves sit side by side on a wide frame and
   stack on a phone; both clear the 88px target and both carry a word. */
.cur-keep-moves {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.cur-keep-again {
  display: flex; align-items: center; gap: 11px;
  min-height: 88px; padding: 0 24px 0 15px; border-radius: 46px;
  border: 2px solid rgba(255, 232, 190, .24);
  background: linear-gradient(180deg, rgba(52,35,20,.62), rgba(30,20,11,.74));
  color: var(--ink);
  font-family: inherit; font-size: clamp(15px, 3.9vw, 18px); font-weight: 700;
  cursor: pointer;
  box-shadow: 0 6px 16px rgba(18,10,3,.40), inset 0 1px 0 rgba(255,255,255,.14);
  transition: transform 160ms cubic-bezier(.2,1.4,.35,1);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.cur-keep-again:active { transform: scale(.96); }
.cur-keep-again-puck {
  width: 52px; height: 52px; 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, .26);
}

@media (min-width: 1024px) {
  /* the shell's desktop `keep` is a two-column grid with four explicit rows
     (v5 added the next-dinosaur shelf at row 3); the moves take the last one,
     side by side */
  .cur-keep > .cur-keep-moves { grid-column: 2; grid-row: 4; justify-self: start; }
  .cur-keep-moves { flex-wrap: nowrap; }
}

/* UNDER 560 THE TWO MOVES STAND SIDE BY SIDE, and pay for it in horizontal fat
   (exp-26 UI-01). Stacked, the pair is 188px tall — the single biggest consumer
   on the one screen that must also fit the card, the shelf and the count inside
   ~616px of real visible height; that stack is what slid under the destination
   dock in the Telegram screenshots. Both keep the full 88px touch height; the
   pucks and paddings slim so "Back to camp" + "See it again" fit inside a
   375px line. If a longer translation ever overflows, flex-wrap simply stacks
   them again — and the keep column's clamp()s absorb it. */
@media (max-width: 559px) {
  .cur-keep-moves { gap: 10px; }
  .cur-keep-home { padding: 0 14px 0 10px; gap: 9px; font-size: 14px; }
  .cur-keep-home-puck { width: 44px; height: 44px; }
  .cur-keep-again { padding: 0 14px 0 10px; gap: 9px; font-size: 14px; }
  .cur-keep-again-puck { width: 44px; height: 44px; }
}

/* ── AND ON THE SHORTEST FRAME THE COLUMN'S GAPS PAY FOR THE LAST 8px ────────
   Found by the geometry gate the moment the dock was centred (exp-26 dock
   centring). At 375x548 the keep column is over budget with its card already at
   the 64px keepsake floor: content 370.8px in a 331.2px box, `justify-content:
   center`, so it overflows 19.8px at BOTH ends — the moves row's bottom lands at
   451.5 with the dock's top at 444.2.

   It was already there and already red: the gate reported "only 71% of Back to
   camp reaches itself — the chapter dots take the rest" against the OLD
   left-hugging dock too. Centring the pill did not create it; it widened the
   dock from 217 to 264px, so the same 7px of spill now reaches "See it again"
   as well, and one standing finding became two.

   The card cannot give any more and the 88px touch height may not. The gaps
   can: 2.5% of a 548px frame is 13.7px four times over, and 8px still reads as
   separation on a screen this small — 22.8px back, halved by the centring, is
   11.4px of lift and the row clears the dock by 4px. Only on frames the
   contract runtime has already called tight (visible height < 600), so 616,
   664, 844, 900 and 954 are byte-identical. */
html.cur-vv-tight .cur-keep { gap: clamp(8px, calc(var(--vvh, 100vh) * .014), 30px); }

/* ---- PLAY: the game is full-bleed, and the host gets out of its way --------
   The first attempt started the game 86 px down so its meter cleared the step
   bar. That worked and looked like a letterbox — a black band across the top of
   the one step that is supposed to feel like a toy. The game is full-bleed
   instead and the HOST moves. */
.cur-gameslot { top: 0; }

/* THE GAME BOOTS AT FULL SIZE, OFF SCREEN.
   The iframe is warmed during the discovery so that play opens on a live game
   instead of a six-megabyte download. The shell hides an unclaimed persisted
   element with `display:none` — and a game inside a display:none subtree gets a
   0x0 viewport, so the feed game solved its camera and its food tray from zero
   and poisoned their springs with NaN. No later resize recovers a spring whose
   position is already NaN, and the scene still LOOKED right: the step said
   "drag food to the T. rex" with no food anywhere on screen.

   So while it warms, the slot is parked two screens to the left at exactly the
   geometry it will be played at. It lays out properly, boots properly, and on
   the play step it simply appears — no resize, no re-solve, no seam. It is also
   PAUSED while parked (journey.js), so two WebGL scenes never render at once. */
#cur-shell:not([data-ch="play"]) .cur-gameslot {
  display: block !important;
  left: -200vw; right: auto; width: 100vw;
  pointer-events: none;
}

/* ── ONE PLACE FOR THE STEP BAR, THE SAME ON EVERY GAME ─────────────────────
   feed (belly), hatch (crack) and dig (progress) each carry a compact bead row
   at y≈14–52, so the host's line sits just under it at +54; how-big carries a
   permanent `#cap` label at y 14–54 instead, which lands in the same place. It
   used to have a fourth arrangement of its own, 100px down and hugging the left
   edge — see the scar below for why that stopped being safe on a short screen.

   This block was also once half of a pair: while the child played, the bar sat
   here; at the payoff a second rule DROPPED it below each game's big printed
   fact caption. Those captions are gone (owner's caption ruling, 2026-08-31)
   and so is the drop (Lilia, same day) — the bar simply stays here all step
   long, on all four games. */
body[data-game] .cur-guide[data-ch="play"] .cur-guide-bar {
  top: calc(var(--vv-top, 0px) + max(10px, env(safe-area-inset-top)) + 54px);
  left: calc(96px + max(0px, env(safe-area-inset-left)));
  right: calc(12px + max(0px, env(safe-area-inset-right)));
  transform: none;
  align-items: flex-start;
  max-width: none;
}
body[data-game] .cur-guide[data-ch="play"] .cur-guide-bar.on { transform: none; }
body[data-game] .cur-guide[data-ch="play"] .cur-guide-eyebrow { text-align: left; }
/* THE 96px IS THE BACK CHIP, NOT A MARGIN. back.js draws an 88px touch target
   in the top-left corner with a 56px clay puck centred in it, and this bar's
   first row — the "STEP 4 OF 5 · Feed it" eyebrow — starts at y=64. On a phone
   this rule used to be overridden back to left:10px, on the reasoning that "the
   game's caption is nearly full-bleed, so there is no left column to hide in
   and only the vertical clearance does the work". True of the GAME's caption,
   and it quietly forgot the host's own corner: measured at 390, the eyebrow ran
   under the back puck (bar x=10..378 y=64..150 against back 0..88 × 0..88 —
   24% of the chip) and "STEP 4 OF 5" was half-covered by the way home.
   The clearance is now the same at every width, which is also one less
   composition to reason about. It costs nothing: the widest prompt in the deck
   ("Keep feeding it, or go on") is ~225px, so at 390 it runs 96→321 inside a
   378px line. If a longer one is ever authored it wraps DOWNWARD, into empty
   sky, which is the harmless direction. */

/* ── THE BAR USED TO DIVE INTO THE MIDDLE OF THE FRAME AT THE PAYOFF ─────────
   AND THAT IS LILIA'S BUG (2026-08-31): *"надо это поменять чтобы картинку
   динозавра не загораживали никакие кнопки"* — reported on the T. rex feed,
   the Velociraptor hatch, Spinosaurus, Therizinosaurus, Microraptor and
   Ankylosaurus, always at the moment the animal or the baby is revealed.

   WHAT STOOD HERE. Two rules that moved the step bar down to
   `--cur-cap-b + 14px` on `cur-play-done`, with a 196px fallback, plus a
   `cur-cap-gone` rescue that sent it home again. They were RIGHT when they were
   written: each game raised its own big printed fact caption into the top band
   at its payoff, so the host's bar stepped down below that caption rather than
   printing across it.

   WHY THEY ARE WRONG NOW. The owner's caption ruling of 2026-08-31 deleted every
   printed narrative line in this deck, on the cards AND inside the games (see
   `app/dig/index.html`, `app/feed/js/app.js`, `app/hatch/js/app.js` — each
   keeps its own scar). So at the payoff there is no caption to step below. The
   measured `--cur-cap-b` was never set, the 196px FALLBACK took over, and the
   bar parked at a hard `10 + 196 + 14 = 220px` — the vertical middle of a phone
   — on the one frame the whole step exists to deliver. The rescue could not
   fire either: it is guarded on `capMax > 0`, and a caption that never appears
   never gives the watcher a height to remember. Two halves of a contract, both
   still honouring a caption that no longer exists, and between them they stood
   the words "STEP 4 OF 5 · Feed it" on the T. rex's open mouth.

   WHAT REPLACES THEM: nothing. The bar keeps the top-band position the rules
   above give it for the whole step, played or paid off. That position is the
   one the games are already built around — the host publishes its painted
   bottom edge as `cur:band` and each game composes the animal below it — so the
   host's furniture and the animal cannot collide by construction, instead of by
   a constant that was only ever correct for copy that has since been deleted.

   `cur-play-done` itself is untouched: it is still set by journey.js on
   `cur:done` and `app/card/9yn55/js/tones.js` still reads it. It simply no
   longer moves this bar. */

/* ── how-big USED TO BE THE EXCEPTION, AND IT IS NOT ONE ────────────────────
   It sat 100px down the frame, hugging the left edge, on this reasoning: its
   `#cap` is not a late payoff but a permanent label naming the animal being
   measured, so the host's lines went UNDER it — "the one quiet band this game
   has: under the caption, above the animals, well clear of the tray."

   The band was quiet at 844 and not at 616. Sweeping the deck for Lilia's rule
   found "How big was it?" printed across the T. rex's snout on all three
   how-big cards in Telegram's in-app browser: a shorter frame does not move the
   host's constant, but the scene composes into the height it can actually see,
   so the animal's head rises to meet a bar that stayed put. Same defect class
   as the caption-era drop above, arrived at from the other direction — a
   constant placed against one viewport's composition.

   MEASURED, at both heights (they are identical, which is the point — this
   game's chrome does not move with the frame):
       #cap       x  93–296   y 14–54
       back chip  x   0– 88   y  0–88
   So the slot the other three games already use — `left: 96px` (which IS the
   back chip, not a margin) and `top: +54px` — is free here too: it starts 42px
   below the cap's bottom edge and 8px right of the back chip's. how-big
   therefore takes the SAME placement as feed, hatch and dig rather than a
   fourth arrangement, and the bar rises 46px clear of the animal.

   The tray reasoning above is unaffected: this only ever moves the bar UP, and
   the tray is at the bottom of the frame.

   THERE IS NO how-big RULE HERE ANY MORE. The shared `body[data-game]` block
   above now carries all four games, which is the whole point — a fourth
   placement is a fourth thing to keep correct at every viewport height, and it
   was the one that went wrong. */

/* ---- the discovery owns the whole frame -----------------------------------
   Its own lit world is behind it (place:false), and the chapter root is a
   centring grid, which would otherwise shrink-wrap the host box. */
#cur-shell[data-ch="meet"] .cur-ch { display: block; }
/* THE SCENE'S FRAME IS WHAT THE CHILD CAN SEE (exp-26 UI-03). Every discovery
   lays itself out from `st.host.clientHeight` — fractions of the host place the
   hero, the specimen and the answer tiles. When the host was the LAYOUT
   viewport, a 616px visual height inside Telegram left the tiles' row (idea
   y≈0.80) underneath the chapter dock and the hero's head past the top edge.
   So the composition solves against the height that is actually visible;
   viewport.js re-broadcasts resize, which is the event the stages already
   re-solve on.

   THE CLAMP USED TO LIVE HERE, AND IT WAS A CLAMP TOO MANY. This rule read
   `bottom: auto; height: var(--vvh, 100%)` — which fixed the composition and, in
   the same stroke, shrank the box that PAINTS. `meet` is the one chapter that
   stands the shared clay world down (`place:false`), so under a short host the
   strip below it was `#cur-shell`'s gradient bottoming out at #0b0806: measured
   on all fifteen cards at layout 844 / visible 664, 179px of near-black under
   the world, reported by the owner on u6dbp as "снизу чёрный фон… фон сдвинут".
   Chrome may be sized to the visual viewport; a surface that paints the world
   may not, because the strip below is page a child can still see.

   The host is full-bleed again and the composition keeps its visible frame —
   `Stage.prototype.compH()` (shared/meet/js/stage.js) is that number now, and it
   is the same number this clamp produced, so every fraction lands where it did.
   The canvas paints the whole box and the camera is extended downward to match. */
#cur-shell[data-ch="meet"] .meet-host { position: absolute; inset: 0; }

/* ── THE MENTOR HAD A BUBBLE HERE, AND IT SHAPED THIS WHOLE FILE ────────────
   Removed 2026-08-31 with the owner's caption ruling; narration is audio only.
   What stood here was a set of per-game and per-tier placements: the bubble
   moved to the top on feed/hatch/dig's play step and again on a BESPOKE
   discovery (so it did not lie on the working surface the child drags across),
   hugged the left and narrowed to 30ch so it never sat on the animal's face,
   and traded height for width under `html.cur-vv-tight`.

   TWO SCARS ARE WORTH KEEPING, because they are about the code that is still
   here. (1) The bespoke override was keyed on `data-tier` precisely because
   `bottom: auto` on a top-anchored fixed element does NOT compute to "auto" —
   Chrome returns the used value (~600px) — and common-tier `chromeFloor()` read
   that as "the host's furniture reaches 600px up the frame" and lifted the
   specimen clean off the top of the screen. Every automated check passed; only
   a screenshot caught it. (2) That same `chromeFloor()` composed the scene
   AROUND the bubble by measuring a CSS longhand. Both are moot now — the
   bubble is gone and `chromeFloor()` no longer measures anything but the dock
   — but they are the reason to be suspicious of any future chrome that anchors
   to one end and is measured from the other. */