/* Full-page overlays: the countdown scrim. */

/* The first three seconds of every session. It needs to own the screen:
   the world dims, the number lands with weight, and each tick re-punches. */
.countdown{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
  z-index:80;pointer-events:none;font-family:var(--f-display);font-weight:800;
  font-size:min(19vw,30vh);line-height:1;color:var(--orange);
  text-shadow:0 min(.9vw,1.6vh) 0 rgba(21,42,110,.32),
              0 0 min(2.4vw,4.2vh) rgba(255,255,255,.35);
  -webkit-text-stroke:min(.42vw,.74vh) #fff8ec;paint-order:stroke fill}
/* A navy wash flattens cream paper to grey (it equalises the channels), so
   the HUD read as disabled. Dim multiplicatively instead — the paper keeps
   its warmth and just gets darker. */
.countdown::before{content:"";position:absolute;inset:0;z-index:-1;
  backdrop-filter:brightness(.66) saturate(1.04);
  -webkit-backdrop-filter:brightness(.66) saturate(1.04);
  background:radial-gradient(circle at 50% 48%,transparent 38%,rgba(0,0,0,.16) 88%);
  animation:cdScrim .35s ease-out both}
@keyframes cdScrim{from{opacity:0}to{opacity:1}}
/* The digit lives in its own element so the punch never transforms the
   full-bleed ::before dim layer above.

   THE NUMERAL IS CUT PAPER (cd-1/2/3.png), not type. It was the one number on
   these boards that was a font — Fredoka in --orange with a white stroke —
   sitting over three screens made entirely of torn paper. Each glyph is on
   its own 1254 square canvas with its navy paper shadow and confetti burst
   already in the art, so all three register on one box and the tick beat is
   the only thing this file has to supply.

   The span is the fallback for any count with no art (setCountdown takes the
   seconds actually remaining); `.countdown[data-art]` picks between them. */
.cd-num,.cd-art{display:block;transform-origin:50% 50%}
.cd-art{width:min(30vw,47vh);height:auto;
  filter:drop-shadow(0 min(.9vw,1.6vh) min(.5vw,.9vh) rgba(21,42,110,.34))}
.countdown[data-art] .cd-num{display:none}
.countdown:not([data-art]) .cd-art{display:none}
/* re-triggered by setCountdown() on every change, so 3-2-1 each get a beat */
.cd-num.tick,.cd-art.tick{animation:cdTick .52s cubic-bezier(.16,1.5,.32,1)}
@keyframes cdTick{
  0%{transform:scale(1.75) rotate(-7deg);opacity:0}
  22%{opacity:1}
  55%{transform:scale(.94) rotate(1.6deg)}
  100%{transform:scale(1) rotate(0)}}
@media (prefers-reduced-motion: reduce){ .countdown::before{animation:none} }

/* ---- the countdown YIELDS while the board assembles ----
   The 3·2·1 used to own the screen from frame one: a full-viewport
   brightness(.66) scrim plus a numeral 19vw tall, both up before a single
   paper had moved. The gameplay entrance therefore played in the dark behind
   a giant number, which is why it read as "nothing pops up, they just
   appear" — and because the entrance was ~1.5s of a 3s countdown, the last
   half of the count was a completely static, dimmed board.

   `body[data-intro="game"]` (set by screens.js for the life of `.game-intro`)
   inverts that order without touching a millisecond of countdown timing —
   which matters, because 1v1 and Party start theirs from a server timestamp:
     · the dim holds at 0 and only starts washing in at 1.72s, as the last
       pieces of tape slap down, reaching full just after the board is whole;
     · the numeral still counts every second, but at reduced weight, and
       grows to full size the moment the assembly finishes.
   So the beat is: board builds in the clear → lights down → "1" → GO.
   Under reduced motion none of this applies: the entrance does not run, so
   the countdown keeps its original full-strength presentation. */
@media (prefers-reduced-motion: no-preference){
  /* the wash completes at 2.12s, exactly as the last tape settles, so that
     removing the flag at 2.3s can never step the dim */
  body[data-intro="game"] .countdown::before{
    animation-duration:.46s;animation-delay:1.66s}
  .countdown{transition:font-size .4s cubic-bezier(.2,.9,.3,1)}
  body[data-intro="game"] .countdown{font-size:min(11vw,17.5vh)}
  /* The paper glyph yields the same way the type did: it is sized in vw/vh
     rather than em, so it needs its own transition and its own reduced size
     while the board is still assembling. */
  .cd-num{transition:opacity .4s ease}
  body[data-intro="game"] .cd-num{opacity:.8}
  /* The paper glyph yields with SIZE ONLY, never opacity. The type could be
     dimmed to .8 because a solid fill just gets quieter; a photographic paper
     texture at .8 goes translucent over the live camera and reads as a ghost
     rather than as a smaller object. */
  .cd-art{transition:width .4s cubic-bezier(.2,.9,.3,1)}
  body[data-intro="game"] .cd-art{width:min(20vw,31vh)}
}

/* ================= CHASE / SUDDEN DEATH URGENCY =================
   Two beats that used to be text only. The final chase said "5 SECONDS LEFT"
   on a small paper clock, and sudden death said nothing but a toast — in a
   mode where the whole screen is a camera, neither one registered while you
   were mid-pose and looking at your own body rather than the HUD.

   The pulse is the peripheral one: the edges of the SCREEN breathe, so it
   lands even when your eyes are nowhere near the clock. It is a fixed layer
   of four edge gradients, never an inset shadow on the stage — an inset
   shadow paints under the stage's own video child and is invisible (the same
   trap the countdown scrim and the TIMES UP wash both hit). */
.edgepulse{position:fixed;inset:0;z-index:78;pointer-events:none;opacity:0;
  --pulse:rgba(244,98,46,.9);--reach:8%;
  transition:opacity .32s ease;
  background:
    linear-gradient(to right,var(--pulse),transparent var(--reach)),
    linear-gradient(to left,var(--pulse),transparent var(--reach)),
    linear-gradient(to bottom,var(--pulse),transparent calc(var(--reach) * 1.3)),
    linear-gradient(to top,var(--pulse),transparent calc(var(--reach) * 1.3))}
body:has(.matchstage[data-phase="chase"]) .edgepulse,
body:has(.pmstage[data-phase="chase"]) .edgepulse{opacity:1}
body:has(.matchstage[data-phase="sudden"]) .edgepulse{
  opacity:1;--pulse:rgba(216,32,26,.95);--reach:10%}
/* the two escalations the stages already track drive the tempo */
body:has(.chase-urgent) .edgepulse{--pulse:rgba(232,64,34,.95);--reach:10.5%}
body:has(.chase-final) .edgepulse{--pulse:rgba(216,32,26,1);--reach:13%}
@media (prefers-reduced-motion:no-preference){
  body:has(.matchstage[data-phase="chase"]) .edgepulse,
  body:has(.pmstage[data-phase="chase"]) .edgepulse{
    animation:edgeBeat .62s ease-in-out infinite alternate}
  body:has(.matchstage[data-phase="sudden"]) .edgepulse{
    animation:edgeBeat .46s ease-in-out infinite alternate}
  body:has(.chase-urgent) .edgepulse{animation-duration:.38s}
  body:has(.chase-final) .edgepulse{animation-duration:.24s}
}
@keyframes edgeBeat{from{opacity:.42;scale:1.015}to{opacity:1;scale:1}}

/* SUDDEN DEATH: the Figma two-tone torn banner, full screen. The blue band
   runs 9%-54% of the paper and the orange 41%-87%, so the rule sits on the
   orange and the callout on the blue — type in cqw against the card, as
   everywhere else. */
.sudden{position:fixed;inset:0;z-index:88;display:grid;place-items:center;
  pointer-events:none}
.sudden.hidden{display:none}
.sudden::before{content:"";position:absolute;inset:0;
  background:radial-gradient(circle at 50% 50%,rgba(150,14,10,.34) 0 32%,
    rgba(20,8,30,.6) 100%);
  animation:sdWash .5s ease-out both}
@keyframes sdWash{from{opacity:0}to{opacity:1}}
.sd-card{position:relative;container-type:inline-size;
  width:min(66vw,118vh,920px);
  filter:drop-shadow(0 min(.9vw,1.6vh) min(.6vw,1.07vh) rgba(0,0,0,.45))}
.sd-card img{display:block;width:100%;aspect-ratio:1.3325;object-fit:fill}
.sd-card span{position:absolute;left:0;width:100%;text-align:center;
  translate:0 -50%;font-family:var(--f-lobby);font-weight:400;line-height:1;
  white-space:nowrap}
/* Sized to sit INSIDE the torn bands, not across the card: the blue starts
   about 11% in and the orange about 8%, so type at the card's full width
   hangs off both papers. */
.sd-title{top:30.5%;font-size:9.8cqw;color:#fff8ec;
  text-shadow:.9cqw .9cqw 0 rgba(9,22,62,.55)}
.sd-sub{top:64%;font-size:5.6cqw;color:#fff8ec;letter-spacing:.03em;
  text-shadow:.7cqw .7cqw 0 rgba(122,36,8,.6)}
.sudden.fire .sd-card{animation:sdSlam 1.9s cubic-bezier(.16,1.5,.32,1) both}
@keyframes sdSlam{
  0%{opacity:0;transform:scale(2.1) rotate(-9deg)}
  12%{opacity:1;transform:scale(.94) rotate(2.5deg)}
  20%{transform:scale(1.04) rotate(-1.6deg)}
  27%{transform:scale(.99) rotate(.8deg)}
  34%{transform:scale(1.012) rotate(-.4deg)}
  80%{opacity:1;transform:scale(1) rotate(0)}
  100%{opacity:0;transform:scale(1.1) rotate(0)}}
@media (prefers-reduced-motion:reduce){
  .sudden::before,.sudden.fire .sd-card{animation:none}
  .sudden.fire .sd-card{opacity:1}
}

/* ================= HOW TO PLAY (Figma 268:596, 1088x643) =================
   Shown over the live Solo board before the count-in. Type is authored in
   `cqw` against the container it belongs to — the panel for the title, each
   yellow square for its own label, the mini word card for JUMP — which is
   exactly how Figma sizes it, so it reproduces at any viewport. */
.htp{position:fixed;inset:0;z-index:90;display:grid;place-items:center;
  padding:2vh 2vw;
  background:radial-gradient(circle at 50% 46%,rgba(9,22,62,.5) 0 34%,
    rgba(9,22,62,.72) 100%);
  backdrop-filter:brightness(.72) saturate(1.02);
  -webkit-backdrop-filter:brightness(.72) saturate(1.02);
  animation:htpScrim .3s ease-out both}
.htp.hidden{display:none}
@keyframes htpScrim{from{opacity:0}to{opacity:1}}

/* THE AUTHORED SIZE IS A CEILING, NOT A TARGET. Figma draws this card at
   1088 on a 1440 frame — 75.6% of the width — and 74vw reproduced that
   faithfully, which on a laptop is a modal covering three quarters of the
   screen in both directions. It reads as a page, not as a card over a board,
   and in multiplayer it buries a live match behind itself for the whole
   tutorial grace. Every size inside is `cqw` against this box, so shrinking
   the box is the ONLY edit needed: the whole composition scales with it and
   no percentage in this file changes.
   2026-09-14: smaller again, min(54vw,108vh,800px), at the owner's request
   ("a bit too large on all screens"). The cap also keeps the art closer to
   sharp: htp-panel.webp is 1400 px wide, so above ~700 CSS px a 2x screen
   is stretching it. Every child is cqw of this box, so nothing else moved. */
.htp-panel{position:relative;container-type:inline-size;
  width:min(54vw,108vh,800px);aspect-ratio:1088/643;
  font-family:var(--f-lobby);font-weight:400;
  animation:htpDrop .52s cubic-bezier(.16,1.5,.32,1) both}
@keyframes htpDrop{
  0%{opacity:0;transform:translateY(-7%) scale(.9) rotate(-2.4deg)}
  58%{opacity:1;transform:translateY(0) scale(1.028) rotate(.7deg)}
  100%{opacity:1;transform:none}}

/* Every paper here is a RENDER of its Figma node, not the raw imageRef the
   node points at. That matters twice over: the cream backing carries Figma
   image filters (exposure -.09, contrast +.10, highlights -.19, shadows -.28)
   that give it its depth, and the yellow squares carry an imageTransform that
   crops the source's transparent margin away so the paper fills its whole
   rectangle. Pulling the raw refs gave a washed-out sheet and squares floating
   inside their own boxes — recognisably the wrong art. */
/* the cream backing stops at 88.8% — the READY paper hangs off the bottom */
.htp-paper{position:absolute;left:0;top:0;width:100%;aspect-ratio:1.9054;
  object-fit:fill;
  filter:drop-shadow(0 min(.55vw,.98vh) min(.42vw,.75vh) rgba(0,0,0,.35))}
.htp-title{position:absolute;left:15.993%;top:6.221%;width:67.923%;height:15.397%;
  text-align:left;font-size:9.191cqw;line-height:9.064cqw;color:#17356f;
  letter-spacing:.092cqw;text-shadow:.46cqw .46cqw 0 rgba(255,255,255,.9)}

.htp-step{--htp-paper-rotate:0deg;--htp-paper-width:100%;--htp-paper-height:100%;
  position:absolute;container-type:inline-size;
  transition:translate .2s cubic-bezier(.2,1.3,.35,1),
    scale .2s cubic-bezier(.2,1.3,.35,1),filter .2s ease}
/* Figma rotates only yellow papers 2/3; their captions and illustrations stay
   level. The REST boxes are the rotated axis-aligned bounds, so recover each
   paper's pre-rotation width/height and rotate it about the same centre. This
   preserves both authored angles (+1.56178deg / +3.12452deg) without also
   tilting the text, robot or timer nested in our shared DOM wrapper. */
.htp-step:nth-of-type(2){--htp-paper-rotate:1.56178deg;
  --htp-paper-width:97.582295%;--htp-paper-height:97.165941%}
.htp-step:nth-of-type(3){--htp-paper-rotate:3.124524deg;
  --htp-paper-width:95.049936%;--htp-paper-height:94.878709%}
.htp-sq{position:absolute;left:50%;top:50%;
  width:var(--htp-paper-width);height:var(--htp-paper-height);
  translate:-50% -50%;rotate:var(--htp-paper-rotate);object-fit:fill;
  filter:drop-shadow(0 min(.42vw,.75vh) min(.3vw,.53vh) rgba(0,0,0,.35))}
/* Each caption uses its own Figma text rectangle. Several authored layers
   deliberately overflow a fixed 63px-high box (most visibly Party's three
   lines), so the inline geometry is the text box's top/left/size rather than
   a centre point derived from however many lines the browser happened to
   wrap. Font size and line height are both cqw values from that column. */
.htp-lab{position:absolute;text-align:center;
  color:#17356f;white-space:nowrap;overflow:visible;
  text-shadow:.5cqw .5cqw 0 rgba(255,255,255,.55)}
/* the cards are inert illustrations, so the whole step reacts as one paper */
@media (prefers-reduced-motion:no-preference){
  .htp-step:hover{translate:0 calc(-1 * min(.5vw,.89vh));scale:1.035;
    filter:brightness(1.04)}
}

/* step 1: the word card in miniature. Figma stretches this paper to 2.238,
   not its natural 4.507 — take the aspect from the RECTANGLE. */
.htp-word{position:absolute;container-type:inline-size}
.htp-word-bg{display:block;width:100%;aspect-ratio:2.238;object-fit:fill;
  filter:drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(0,0,0,.3))}
.htp-word-fill{position:absolute;left:3.46%;top:11.6%;width:46.9%;height:76.8%;
  object-fit:fill}
.htp-word span{position:absolute;left:0;top:49.9%;width:100%;text-align:center;
  translate:0 -50%;font-size:29.63cqw;line-height:1;color:#17356f;
  text-shadow:1.2cqw 1.2cqw 0 rgba(255,255,255,.9)}

/* step 2 */
.htp-pose{position:absolute;aspect-ratio:1.3184;object-fit:fill}

/* step 3: the same award paper and the same cqw type as the live payoff */
.htp-award{position:absolute;container-type:inline-size}
.htp-award img{display:block;width:100%;aspect-ratio:2.098;object-fit:fill;
  filter:drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(0,0,0,.3))}
.htp-award span{position:absolute;left:0;width:100%;text-align:center;
  translate:0 -50%;line-height:1;color:#1b7532;white-space:nowrap;
  text-shadow:.6cqw .6cqw 0 rgba(0,0,0,.18)}
.htp-award .aw-got{top:19.8%;font-size:7.8cqw}
.htp-award .aw-pts{top:47.3%;font-size:18.93cqw}
.htp-award .aw-lbl{top:75.6%;font-size:7.8cqw}

/* ---- mode-scoped column content (data-mode: ai | 1v1 | party) ----
   The chrome above (backing/title/three squares/READY) is pixel-identical
   across Figma's three components (268:596, 312:642, 312:641); only what
   sits inside each torn-paper square differs, and by how much per column —
   so rather than three parallel DOMs, every mode-specific piece carries
   data-htp-for and only #howToPlay's own data-mode picks which show. */
[data-htp-for]{display:none}
#howToPlay[data-mode="ai"]    [data-htp-for~="ai"]{display:block}
#howToPlay[data-mode="1v1"]   [data-htp-for~="1v1"]{display:block}
#howToPlay[data-mode="party"] [data-htp-for~="party"]{display:block}

/* 1v1/Party's captions run longer than Solo's two-word originals and wrap to
   two lines rather than the single nowrap line the first three used.
   Namespaced .htp-wrap, not .wrap — .wrap is the page-layout wrapper
   (theme.css, min-height:100vh) and collides silently on class name alone. */
.htp-lab.htp-wrap{white-space:normal}

/* col 2 (1v1/Party) and col 3 (Solo) use static body/face composites. These
   are square image-fill groups in the tutorial components, not the
   tall 520×610 live gameplay rig. Reusing .grobot's aspect ratio made every
   instructional mascot 33–34 design pixels too tall. */
.htp-rig{position:absolute;aspect-ratio:1}
.htp-rig .rb-body{position:absolute;inset:0;width:100%;height:100%;
  filter:drop-shadow(0 min(.3vw,.53vh) min(.34vw,.6vh) rgba(0,0,0,.34))}
.htp-rig .rb-face{position:absolute;object-fit:fill}
.htp-rig-thinking .rb-face{left:35.047%;top:20.561%;width:30.453%;height:21.028%}
.htp-rig-thumbsup .rb-face{left:35.348%;top:20.975%;width:29.309%;height:18.318%}

/* col 3 (1v1 only): the mini word-timer card, same paper/aspect-from-rect
   convention as the live .sp-wtimer, just this instance's own Figma rect
   (not the live HUD card's 252px/2.250) and the matching cqw type — this
   component scales identically wherever it's placed.

   THE BOX IS THE FRAME; THE PAPER IS THE CARD INSIDE IT. Figma wraps this one
   in a `Word Timer` FRAME (310:591, 257x117.2) that is wider than the paper it
   holds — `robot-guess-card-tertiary-cream` (310:592) is 201.4x114.2, inset
   3.11%/2.56% into it. The frame is the right box for the two text layers,
   which ARE positioned against it, and the wrong one for the paper: stretched
   to the frame the card came out 28% too wide and at 2.19 instead of its
   authored 1.76, the one asset on any of these three cards not drawn at the
   size Figma draws it. Every other paper here happens to sit in a rectangle of
   its own, which is why this was the only one that could go wrong. */
.htp-timer{position:absolute;container-type:inline-size;aspect-ratio:257/117.21}
.htp-timer img{position:absolute;left:3.113%;top:2.560%;
  width:78.366%;height:97.440%;object-fit:fill;
  filter:drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(0,0,0,.3))}
.htp-timer span{position:absolute;left:18.527%;width:57.245%;text-align:center;
  color:#17356f;line-height:23.41cqw;text-shadow:1.19cqw 1.19cqw 0 rgba(255,255,255,.9)}
.htp-timer .htp-tlab{top:0;font-family:var(--f-sign);font-weight:800;
  font-size:11.88cqw;white-space:nowrap;word-spacing:.16em}
.htp-timer .htp-tval{top:38.542%;font-size:22.06cqw}

/* col 3 (Solo only): the mini TIMES UP chip — the same guess-card paper and
   clock icon the live board already uses, at a bespoke small size. Figma
   left-aligns this one's copy against the clock rather than centering it. */
.htp-timesup{position:absolute;container-type:inline-size}
.htp-timesup .htp-tu-bg{display:block;width:100%;aspect-ratio:2.724;object-fit:fill;
  filter:drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(0,0,0,.3))}
.htp-timesup .htp-tu-clock{position:absolute;left:1.74%;top:20.7%;width:20.29%;
  aspect-ratio:1;background:#f28d40;
  /* Figma node I309:558;245:896 filters the shared white clock fill orange.
     Use that rendered color through the source alpha instead of showing the
     raw white PNG; the hard .727px shadow remains a separate node effect. */
  -webkit-mask:url("../assets/ui/figma/sp-clock.webp") center/contain no-repeat;
  mask:url("../assets/ui/figma/sp-clock.webp") center/contain no-repeat;
  filter:drop-shadow(.67cqw .67cqw 0 rgba(0,0,0,.5))}
.htp-timesup > span:not(.htp-tu-clock){position:absolute;left:22.03%;top:48.4%;width:76.5%;
  translate:0 -50%;text-align:left;font-size:14.68cqw;line-height:1;
  color:#17356f;text-shadow:.67cqw .67cqw 0 rgba(255,255,255,.9)}

.htp-ready{position:absolute;left:28.125%;top:77.605%;width:43.75%;height:22.395%;
  background:none;border:0;padding:0;cursor:pointer;container-type:inline-size;
  transition:translate .18s cubic-bezier(.2,1.3,.35,1),
    scale .18s cubic-bezier(.2,1.3,.35,1),filter .18s ease}
.htp-ready img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  filter:drop-shadow(0 min(.5vw,.89vh) min(.36vw,.64vh) rgba(0,0,0,.38))}
.htp-ready span{position:absolute;left:0;top:49.5%;width:100%;translate:0 -50%;
  text-align:center;font-size:16.77cqw;line-height:1;color:#fff;
  text-shadow:1.1cqw 1.1cqw 0 rgba(0,0,0,.3)}
/* THE GRACE COUNTDOWN, on the button itself. In 1v1 and Party the match
   starts a few seconds from now whether or not this is pressed, so the one
   control on the card also has to say when — a screen that gives no reason it
   has not started is the exact thing this replaced. It is empty (and takes no
   space) in Solo, which has no clock to report. */
.htp-count{font-style:normal;display:none;margin-left:.42em;opacity:.86;
  font-variant-numeric:tabular-nums}
.htp-count:not(:empty){display:inline}
@media (prefers-reduced-motion:no-preference){
  .htp-count.beat{animation:htpCountBeat .5s cubic-bezier(.2,1.5,.35,1)}
}
@keyframes htpCountBeat{0%{scale:1.34;opacity:1}60%{scale:.97}100%{scale:1}}
.htp-ready:hover,.htp-ready:focus-visible{
  translate:0 calc(-1 * min(.34vw,.6vh));scale:1.035;filter:brightness(1.06)}
.htp-ready:active{scale:.975;filter:brightness(.97)}
.htp-ready:focus-visible{outline:min(.208vw,.37vh) solid #fff;
  outline-offset:min(.28vw,.5vh)}

@media (prefers-reduced-motion:reduce){
  .htp,.htp-panel{animation:none}
}
/* portrait / very tall viewports: the panel is width-bound, so let it use
   nearly the full width rather than shrinking against a height it never hits */
@media (max-aspect-ratio:1/1){ .htp-panel{width:min(94vw,152vh)} }

/* ================= ENABLE CAMERA — the boot gate =================
   Figma "ENABLE CAMERA SCREEN" 322:717, 1539x1460 (aspect 1.0541). Every
   left/top/width here is a percentage of THAT component, and every font-size
   is its Figma px over 1539 — the panel is a container, so 1cqw is 1% of it
   and the type reproduces at any size exactly as Figma authors it.

   Two things overflow the component on purpose and must not be clipped: the
   title card hangs off the top (T0.685) and the robot off the right
   (69.526 + 32.099 = 101.6%).

   The scrim is lighter than HOW TO PLAY's. HOW TO PLAY sits over a live board
   the player must not act on; this sits over a Home that is deliberately
   mid-assembly (see .home-held in screens.css) and dimming it to the same
   depth hides the very thing it is there to show. */
/* ===================== AGE CHECK (js/ageGate.js) =====================
   The HOW TO PLAY card's family: same scrim, the same htp-panel paper at its
   authored aspect, the same title type and the same READY paper hanging off
   the bottom (so the panel keeps HTP's 1088/643 box and the button keeps its
   rectangle). Every size inside is cqw against .ag-panel. z 91: over HOW TO
   PLAY (90), under the boot gate (92). */
.ag{position:fixed;inset:0;z-index:91;display:grid;place-items:center;
  padding:2vh 2vw;
  background:radial-gradient(circle at 50% 46%,rgba(9,22,62,.5) 0 34%,
    rgba(9,22,62,.72) 100%);
  backdrop-filter:brightness(.72) saturate(1.02);
  -webkit-backdrop-filter:brightness(.72) saturate(1.02);
  animation:htpScrim .3s ease-out both}
.ag.hidden{display:none}
/* SIZED TO ITS CONTENT, and small enough to stay sharp: htp-panel.webp is
   1400 px wide, so at 620 CSS px a 2x screen still gets real pixels, where the
   first 760 px version was stretched and its torn edge went blocky. Every
   length inside is cqw of this box, so the whole card scales as one. The
   paper is 43.5cqw tall (the art stretched a little wider than its own
   1.905), the content is centred in it, and the READY paper hangs off the
   bottom edge the way HOW TO PLAY's does. */
.ag-panel{position:relative;container-type:inline-size;
  width:min(40vw,80vh,620px);aspect-ratio:100/49.1;
  font-family:var(--f-lobby);font-weight:400;color:#17356f;
  animation:htpDrop .52s cubic-bezier(.16,1.5,.32,1) both}
.ag-panel:not([data-view="ask"]) [data-ag-when="ask"],
.ag-panel:not([data-view="young"]) [data-ag-when="young"]{display:none}
.ag-paper{position:absolute;left:0;top:0;width:100%;height:43.5cqw;object-fit:fill;
  filter:drop-shadow(0 min(.55vw,.98vh) min(.42vw,.75vh) rgba(0,0,0,.35))}
.ag-content{position:absolute;left:7cqw;right:7cqw;top:4.5cqw;height:31cqw;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2.6cqw;
  text-align:center}
.ag-title{font-size:6.2cqw;line-height:1.02;letter-spacing:.06cqw;text-wrap:balance;
  text-shadow:.46cqw .46cqw 0 rgba(255,255,255,.9)}
.ag-pick{display:flex;gap:2.6cqw;justify-content:center}
.ag-pick select{appearance:none;-webkit-appearance:none;cursor:pointer;
  font:400 4.3cqw/1 var(--f-lobby);color:#17356f;letter-spacing:.04em;
  background:#fffaf0 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%2317356f' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 1.9cqw center/2.2cqw;
  border:.42cqw solid #17356f;border-radius:1.5cqw;
  padding:1.5cqw 5.4cqw 1.5cqw 2.4cqw;min-width:24cqw;
  box-shadow:0 .6cqw 0 rgba(23,53,111,.25);
  transition:translate .15s ease,box-shadow .15s ease}
.ag-pick select:hover{translate:0 -.3cqw;box-shadow:0 .9cqw 0 rgba(23,53,111,.25)}
.ag-pick select:focus-visible{outline:.42cqw solid #f5b51c;outline-offset:.4cqw}
.ag-note{margin:0;font:700 3cqw/1.3 var(--f-label);opacity:.8}
.ag-say{margin:0;font:700 3.4cqw/1.35 var(--f-label);text-wrap:balance}
.ag-back{background:none;border:0;padding:.6cqw 1cqw;cursor:pointer;color:#17356f;
  font:400 2.9cqw/1 var(--f-lobby);letter-spacing:.05em;text-decoration:underline;
  text-underline-offset:.5cqw;transition:translate .15s ease,color .15s ease}
.ag-back:hover,.ag-back:focus-visible{color:#f45a3c;translate:0 -.2cqw}
.ag-back:active{translate:0 .2cqw}
.ag-go{position:absolute;left:30cqw;top:37cqw;width:40cqw;height:12.1cqw;
  background:none;border:0;padding:0;cursor:pointer;container-type:inline-size;
  transition:translate .18s cubic-bezier(.2,1.3,.35,1),
    scale .18s cubic-bezier(.2,1.3,.35,1),filter .18s ease,opacity .18s ease}
.ag-go img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  filter:drop-shadow(0 min(.5vw,.89vh) min(.36vw,.64vh) rgba(0,0,0,.38))}
.ag-go span{position:absolute;left:0;top:49.5%;width:100%;translate:0 -50%;
  text-align:center;font-size:15.5cqw;line-height:1;color:#fff;
  text-shadow:1.1cqw 1.1cqw 0 rgba(0,0,0,.3)}
.ag-go:not(:disabled):hover,.ag-go:not(:disabled):focus-visible{
  translate:0 calc(-1 * min(.34vw,.6vh));scale:1.035;filter:brightness(1.06)}
.ag-go:not(:disabled):active{scale:.975;filter:brightness(.97)}
.ag-go:focus-visible{outline:min(.208vw,.37vh) solid #fff;outline-offset:min(.28vw,.5vh)}
.ag-go:disabled{cursor:default;opacity:.6;filter:saturate(.55)}
@media (prefers-reduced-motion:reduce){ .ag,.ag-panel{animation:none} }
@media (max-aspect-ratio:1/1){ .ag-panel{width:min(92vw,130vh)} }

.cg{position:fixed;inset:0;z-index:92;display:grid;place-items:center;
  padding:2vh 2vw;
  background:radial-gradient(circle at 50% 46%,rgba(9,22,62,.34) 0 34%,
    rgba(9,22,62,.58) 100%);
  backdrop-filter:brightness(.84) saturate(1.02);
  -webkit-backdrop-filter:brightness(.84) saturate(1.02);
  animation:htpScrim .3s ease-out both}
.cg.hidden{display:none}
/* Privacy Policy and Terms of Use, on the scrim under the card. Absolute, so
   the grid that centres the card never sees it. */
.cg-legal{position:absolute;left:50%;bottom:max(1.4vh,10px);translate:-50% 0;
  display:flex;gap:1.6em;font:700 clamp(11px,1.45vh,15px)/1 var(--f-label);
  letter-spacing:.02em;white-space:nowrap}
.cg-legal a{color:rgba(255,247,230,.82);text-decoration:none}
.cg-legal a:hover,.cg-legal a:focus-visible{color:#fff;text-decoration:underline}

/* The card and the boot readout are ONE centred column. The readout is not
   part of 322:717 but it is very much part of what the player sees, so
   centring the component alone parked the whole screen low — a wide band of
   scrim above the title card and the progress strip almost touching the
   bottom edge. The stack carries the width (and therefore the container, so
   every cqw under it still measures against the card) and .cg's grid centres
   the pair. */
/* THE BOX IS CENTRED; THE PAPER IS NOT, AND THE PAPER IS WHAT THE EYE
   READS. 322:717's panel rectangle runs from the title card hanging off the
   top (.6849%) to the foot of the cream sheet (~99.2%), but the sheet itself
   only begins at 16.301% — so a perfectly centred BOX leaves the sheet's own
   centre at 56.5% of the screen, with 21.9% of scrim above it and 8.9%
   below. Measured at all four target viewports, and it is why the card read
   as sitting low with LET'S GO nearly on the bottom edge.
   The offset is a constant fraction of the panel, not a viewport figure,
   because the sheet's place inside the composition is fixed by the art: the
   correction comes out at 7.8% of panel height at 16:9, 16:10 and FHD and
   7.8% at 5:4. 84vh rather than 88vh buys the room to make it without
   jamming the title card against the top edge. */
.cg-stack{display:flex;flex-direction:column;align-items:center;
  container-type:inline-size;width:min(66vw,84vh,1539px);
  translate:0 -7.8%}

.cg-panel{position:relative;container-type:inline-size;overflow:visible;
  width:100%;aspect-ratio:1539/1460;
  font-family:var(--f-lobby);font-weight:400;color:#17356f;
  animation:htpDrop .52s cubic-bezier(.16,1.5,.32,1) both}

/* Every paper on this component carries the same authored DROP_SHADOW —
   rgba(0,0,0,.35), y 7, blur 4 on a 1539px frame. drop-shadow, never
   box-shadow: these are torn papers in transparent bounding boxes. */
.cg-panel img{position:absolute;display:block;
  filter:drop-shadow(0 .455cqw .26cqw rgba(0,0,0,.35))}

.cg-paper{left:0;top:16.301%;width:100%;aspect-ratio:1539/1211;object-fit:fill}
/* The title card is a RENDER of this screen's own instance (321:665), for the
   same reason the two tapes below are: it is ROTATED (+0.08574 rad) and the
   shared Home asset is not the raw art — `title-card.webp` is itself a render
   of Home's instance 47:121, which bakes in a DIFFERENT rotation (-0.10269
   rad). Hanging that bitmap in 321:665's absoluteBoundingBox — the box the
   rotation expands to, 648.26x463.65 — drew the logo ~11 degrees off Figma's,
   too wide and too short, and THAT is what left both tapes floating clear of
   the corners they are supposed to be holding down. The rect is the node's
   absoluteRenderBounds; rotation, crop and its own drop shadow are baked, so
   no browser filter is added on top. */
.cg-panel img.cg-titlecard{left:27.9221%;top:.6849%;width:42.5481%;
  aspect-ratio:655/473;object-fit:fill;filter:none}
/* These are rendered INSTANCE exports, not the raw imageRefs. Both source
   images live on large transparent canvases and Figma applies an imageTransform
   plus a strong instance rotation; stretching those raw PNGs produced the two
   tiny, almost-horizontal scraps this screen used to show. Rendering 321:667
   and 321:666 bakes crop + rotation + shadow together. Their CSS rectangles
   are therefore the nodes' absoluteRenderBounds relative to 322:717, and no
   second browser drop-shadow is applied. */
.cg-panel img.cg-tape{filter:none;object-fit:fill}
.cg-tape-y{left:27.5236%;top:0;width:13.1335%;aspect-ratio:405/383}
.cg-tape-c{left:59.1293%;top:20.4794%;width:13.8453%;aspect-ratio:427/312}

/* Figma gives this line a 98.62px line box for 50px type, i.e. the glyphs sit
   centred in a box twice their height. Centre it rather than reproducing the
   leading, so a fitted (smaller) size stays on the same optical line. */
.cg-title{position:absolute;left:16.959%;top:28.151%;width:69.396%;height:6.781%;
  display:grid;align-content:center;align-content:safe center;
  text-align:center;white-space:nowrap;
  font-size:3.249cqw;line-height:1.06;letter-spacing:.0325cqw;
  text-shadow:.325cqw .325cqw 0 rgba(255,255,255,.9)}

/* three identical torn strips (321:658/676/678, one shared imageRef) */
.cg-row{left:13.06%;width:73.879%;aspect-ratio:4.679;object-fit:fill}
.cg-row1{top:34.315%}
.cg-row2{top:50.959%}
.cg-row3{top:67.603%}

/* scaleMode FILL on all three -> object-fit:cover, and each keeps its own
   authored rectangle: they are different shapes, not one icon slot */
.cg-panel img.cg-icon{object-fit:cover;filter:none}
.cg-icon-cam {left:18.129%;top:37.397%;width:16.439%;aspect-ratio:1.6536}
.cg-icon-hi5 {left:16.959%;top:52.671%;width:16.049%;aspect-ratio:1.479}
.cg-icon-room{left:16.504%;top:66.986%;width:17.024%;aspect-ratio:1}

.cg-head{position:absolute;width:46.589%;height:6.781%;
  display:grid;align-content:center;align-content:safe center;
  font-size:3.249cqw;line-height:1.06;letter-spacing:.0325cqw;
  text-shadow:.325cqw .325cqw 0 rgba(255,255,255,.9)}
.cg-head1{left:36.452%;top:36.37%}
.cg-head2{left:35.802%;top:52.808%}
.cg-head3{left:35.802%;top:69.178%}

/* The running copy is Beiruti 800 with a 40px line box on a 1539px frame.
   Figma's boxes are one line high and its sample copy overflows them, so the
   height here is the authored top plus room for the wrap the browser
   genuinely needs — the text is centred on the authored line either way. */
.cg-sub{position:absolute;
  display:grid;align-content:center;align-content:safe center;
  font-family:var(--f-sign);font-weight:800;
  line-height:2.599cqw;letter-spacing:.0325cqw;text-wrap:pretty;
  text-shadow:.325cqw .325cqw 0 rgba(255,255,255,.65)}
.cg-sub1{left:36.452%;top:41.5%;width:46.589%;height:8%;font-size:3.249cqw}
/* Row 1 has two readings because the card is shown on every boot, not only
   the first: this is a consent/briefing screen, and the permission line is the
   only part of it that goes stale once the browser has said yes. Both spans
   sit in the same authored rectangle and exactly one is displayed. */
[data-cg-when]{display:none}
.cg:not([data-granted]) [data-cg-when="ask"],
.cg[data-granted] [data-cg-when="ready"]{display:grid}
.cg-sub2{left:35.802%;top:57.8%;width:49.123%;height:8%;font-size:3.054cqw}
.cg-sub3{left:35.802%;top:74.4%;width:40.481%;height:8%;font-size:3.249cqw}

/* ---- the boot readout: ON THE SCRIM, not on the paper ----
   322:717 is a finished composition — three rows butted against the button
   with about 22 authored pixels between them. There is no slot on it for a
   progress strip or an error line, and inventing one means covering the
   robot or a row of the design. Both therefore live in their own band just
   below the card, unmistakably chrome rather than part of the paper, and
   they share the slot: an error replaces the progress, because once the
   camera has been refused the download finishing is not the news. */
/* Empty on the happy path, and taking no space when it is: the column is
   centred as the card alone unless something has actually gone wrong. */
.cg-status{width:88%;text-align:center;
  font-family:var(--f-sign);font-weight:800}
.cg[data-state="blocked"] .cg-status,
.cg[data-state="failed"]  .cg-status{margin-top:.85cqw}
.cg-note{display:none;font-size:2.3cqw;line-height:1.3;color:#ffc4b2;
  text-shadow:0 .12cqw .3cqw rgba(0,0,0,.5)}
.cg[data-state="blocked"] [data-cg-note="blocked"],
.cg[data-state="failed"]  [data-cg-note="failed"]{display:block}

/* THE ROBOT (322:713 body_point_bottom_left + 322:714 face_joyful).
   `point` is unique to this screen: he is pointing DOWN-LEFT, at the button.
   The other five bodies are the RObot Sprites section's raw square fills —
   1254x1254, same framing, so one rule dresses all six — and the faces are
   the shared union-box rb-face-* set the whole app already uses.

   The face rectangle is NOT the authored 322:714 rect. That rect is sized to
   face_joyful's RAW canvas (aspect 1.60); the union-box exports are 1.744 and
   cropped tight to their ink. The values below are the rect that lands the
   union art's ink exactly where Figma draws the raw art's ink, and the
   per-pose left/top are each sprite's measured head-screen centre relative to
   `point`. Eyeballing either one puts the eyes off the screen. */
 /* z-index 4 because 322:713 is the LAST child of the component: Figma
    paints him over the ENABLE CAMERA paper, and his pointing hand crossing
    in front of it is the whole gesture. He is pointer-events:none, so
    standing on the button costs it nothing. */
.cg-rig{position:absolute;left:69.526%;top:66.164%;width:32.099%;z-index:4;
  aspect-ratio:1;pointer-events:none}
.cg-rig-body{left:0;top:0;width:100%;height:100%;object-fit:cover}
.cg-panel img.cg-rig-face{left:41.503%;top:24.711%;width:22.799%;aspect-ratio:300/172;
  object-fit:fill;filter:none}
.cg-rig[data-pose="idle"]      .cg-rig-face{left:38.832%;top:25.110%}
.cg-rig[data-pose="present"]   .cg-rig-face{left:42.659%;top:24.950%}
.cg-rig[data-pose="thumbsup"]  .cg-rig-face{left:38.832%;top:25.110%}
.cg-rig[data-pose="surprised"] .cg-rig-face{left:38.832%;top:25.030%}
.cg-rig[data-pose="thinking"]  .cg-rig-face{left:38.951%;top:24.870%}
@media (prefers-reduced-motion:no-preference){
  .cg-rig{animation:cgBob 2.9s ease-in-out infinite;transform-origin:50% 96%}
}
@keyframes cgBob{
  0%,100%{translate:0 0;rotate:-1deg}
  50%    {translate:0 -1.5%;rotate:1deg}}

/* CENTRED, not on its authored left. 322:712 sits at left 17.414% because
   the composition balances it against the robot hanging off the right edge —
   but the robot is drawn OVER the paper and outside it, so on the paper
   itself the button read as shoved left. It keeps its authored width and
   aspect and is centred on the card instead. */
.cg-enable{position:absolute;left:50%;translate:-50% 0;top:85.753%;width:54.451%;
  aspect-ratio:838/208;z-index:3;
  background:none;border:0;padding:0;cursor:pointer;container-type:inline-size;
  transition:translate .18s cubic-bezier(.2,1.3,.35,1),
    scale .18s cubic-bezier(.2,1.3,.35,1),filter .18s ease}
.cg-enable img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  /* 322:712's own shadow: y 7, blur 4 on an 838px component */
  filter:drop-shadow(0 .835cqw .477cqw rgba(0,0,0,.35))}
.cg-enable span{position:absolute;left:0;top:49.5%;width:100%;translate:0 -50%;
  text-align:center;font-size:9.1cqw;line-height:1;letter-spacing:.1cqw;
  color:#17356f;text-shadow:.6cqw .6cqw 0 rgba(255,255,255,.9)}
/* The lift has to carry the centring offset with it: `translate` is one
   property, and writing `0 -.34vw` here would snap the button back to
   left:50% for the length of every hover. */
.cg-enable:not(:disabled):hover,.cg-enable:focus-visible{
  translate:-50% calc(-1 * min(.34vw,.6vh));scale:1.035;filter:brightness(1.06)}
.cg-enable:not(:disabled):active{scale:.975;filter:brightness(.96)}
.cg-enable:focus-visible{outline:min(.208vw,.37vh) solid #fff;
  outline-offset:min(.28vw,.5vh)}
/* Between the click and the browser's own prompt the engine may still be
   downloading. Say so on the button rather than leaving one that visibly did
   nothing — and stop it taking a second click while it waits. */
.cg[data-pending] .cg-enable{pointer-events:none;cursor:wait;filter:saturate(.72)}
@media (prefers-reduced-motion:no-preference){
  .cg[data-pending] .cg-enable{animation:cgWait 1.15s ease-in-out infinite}
  /* The paper twitches every few seconds until it is pressed, so the one
     control on the card is never mistaken for part of the illustration. It
     animates `rotate`, which the hover lift's translate/scale do not touch,
     so the two compose instead of one killing the other. */
  .cg:not([data-pending]) .cg-enable{animation:cgBeckon 3.4s ease-in-out infinite}
}
@keyframes cgWait{0%,100%{opacity:.72}50%{opacity:1}}
@keyframes cgBeckon{0%,72%,100%{rotate:0deg}84%{rotate:-.9deg}92%{rotate:.7deg}}

@media (prefers-reduced-motion:reduce){
  .cg,.cg-panel{animation:none}
}
/* portrait / very tall viewports: the panel is width-bound there, so let it
   use nearly the full width rather than shrinking against a height it never
   hits — the robot's 1.6% right overhang is inside .cg's own 2vw padding. */
@media (max-aspect-ratio:1/1){ .cg-stack{width:min(92vw,82vh)} }


/* The toast. It had NO stylesheet at all — it laid out static below the fold,
   so every message the game has ever sent (room errors, "X left the party",
   the skip penalty, SUDDEN DEATH, copy-failed fallbacks) was invisible.
   Paper-craft to match the rest: a cream card with a torn-ink border, pinned
   above the bottom edge where it never covers the word or the score. */
/* TORN PAPER, not a rounded rectangle. Every other surface in this game is a
   piece of cut paper taped to a board; the toast was a bordered box with a
   box-shadow, which is the only element that ever read as a web page. The
   backing is `cam-row.webp` — the ENABLE CAMERA screen's own info-row paper —
   stretched with object-fit:fill, which is how every re-used paper in this
   project is fitted (its authored aspect is a source ratio, never a promise).
   The shadow is a `drop-shadow` FILTER on the image, never a box-shadow: a
   box-shadow round torn paper in a transparent bounding box draws a rectangle
   in mid-air. */
/* THE CAP IS NARROWER THAN THE OLD BOX ON PURPOSE. A toast wide enough to
   hold a whole sentence on one line is ~12:1, and at that aspect the torn
   edge is stretched to a couple of pixels and the paper reads as a plain
   rectangle again — the exact thing this replaced. Wrapping to two lines
   keeps it near the source paper's own proportions, and a sentence somebody
   is meant to read in a hurry wants two short lines anyway. */
.toast{position:fixed;left:50%;bottom:4.5vh;z-index:97;
  display:none;width:max-content;max-width:min(32rem,58vw);
  translate:-50% 0;pointer-events:none}
.toast-paper{position:absolute;inset:0;width:100%;height:100%;
  object-fit:fill;
  filter:drop-shadow(0 min(.5vw,.9vh) min(.35vw,.62vh) rgba(9,22,62,.34))}
.toast-text{position:relative;display:block;
  padding:1.05em 1.7em;
  font-family:var(--f-sign),var(--f-body);font-weight:800;
  font-size:clamp(13px,min(1.32vw,2.35vh),24px);line-height:1.25;
  letter-spacing:.01em;text-align:center;text-wrap:balance;
  color:#17356f;text-shadow:0 .06em 0 rgba(244,236,224,.55)}
/* A slip of paper lands with a bit of skew — the old entrance was a straight
   vertical pop, which is a notification, not something tossed onto a desk. */
.toast.show{display:block;animation:toastIn .3s cubic-bezier(.2,1.5,.35,1)}
@keyframes toastIn{
  from{opacity:0;translate:-50% 46%;scale:.9;rotate:-2.4deg}
  60%{opacity:1;translate:-50% -7%;scale:1.03;rotate:.9deg}
  to{opacity:1;translate:-50% 0;scale:1;rotate:0deg}}
@media (prefers-reduced-motion: reduce){
  .toast.show{animation:none}
}

/* ================= MOVE BACK (framing warning) =================
   Figma's blank red warning cutout (317:651, 1283x715) with live type, the
   same treatment SUDDEN DEATH and the 3·2·1 get. Type is `cqw` against the
   card, so it reproduces at any viewport.

   z-index 87 puts it UNDER sudden death (88), the tutorial (90) and the boot
   gate (92), and over the edge pulse. It is the least urgent of the full
   screen messages: the others announce something that just happened to the
   match, this one is advice about the room, and it must never be the thing
   covering a rule change. `pointer-events:none` throughout, because it is a
   hint and nothing on this screen should become unclickable because of it. */
.moveback{position:fixed;inset:0;z-index:87;display:grid;place-items:center;
  pointer-events:none}
.moveback.hidden{display:none}
.mvb-card{position:relative;container-type:inline-size;
  width:min(44vw,74vh,660px);
  filter:drop-shadow(0 min(.7vw,1.24vh) min(.5vw,.89vh) rgba(0,0,0,.42));
  animation:mvbIn .42s cubic-bezier(.16,1.5,.32,1) both}
/* aspect from the Figma RECTANGLE (1283/715), never the PNG's own 1680x936 */
.mvb-card img{display:block;width:100%;aspect-ratio:1.7944;object-fit:fill}
.mvb-card span{position:absolute;left:6%;width:88%;text-align:center;
  translate:0 -50%;font-family:var(--f-lobby);font-weight:400;line-height:1.05;
  color:#fff8ec}
/* nowrap, and sized to fit inside it: at 15.5cqw "MOVE BACK!" was wider than
   the paper's text box and broke onto a second line that landed straight on
   top of the sub. The headline of a warning is the one string that must never
   reflow. */
.mvb-title{top:41%;font-size:13cqw;white-space:nowrap;
  text-shadow:.9cqw .9cqw 0 rgba(122,20,8,.55)}
/* the sub wraps to two balanced lines on purpose: one line at this width would
   have to drop to about 4cqw and stop reading as part of the same object */
.mvb-sub{top:72%;font-size:5.4cqw;letter-spacing:.02em;
  text-shadow:.5cqw .5cqw 0 rgba(122,20,8,.5)}
@keyframes mvbIn{
  0%{opacity:0;transform:scale(1.5) rotate(-5deg)}
  60%{opacity:1;transform:scale(.97) rotate(1.4deg)}
  100%{opacity:1;transform:scale(1) rotate(0)}}
/* A held warning has to keep saying itself without ever slamming again — a
   repeating slam on a persistent state reads as a bug. */
@media (prefers-reduced-motion:no-preference){
  .moveback:not(.hidden) .mvb-card{animation:mvbIn .42s cubic-bezier(.16,1.5,.32,1) both,
    mvbBreathe 1.5s 0.42s ease-in-out infinite}
}
@keyframes mvbBreathe{
  0%,100%{scale:1}
  50%{scale:1.035}}
@media (prefers-reduced-motion:reduce){
  .mvb-card{animation:none;opacity:1}
}

/* ================= FULL BODY CHECK (before the 3·2·1) =================
   Same paper and card box as MOVE BACK, so it inherits the entrance and the
   cqw type scale. The copy is one sentence, so it wraps to three balanced
   lines instead of carrying MOVE BACK's title/sub split. z-index 88 puts it
   over MOVE BACK (87), which is suppressed while it is up anyway. */
.framecheck{z-index:88}
.fc-title{position:absolute;left:9%;width:82%;top:52%;translate:0 -50%;
  text-align:center;font-family:var(--f-lobby);font-weight:400;
  font-size:9.4cqw;line-height:1.08;color:#fff8ec;text-wrap:balance;
  text-shadow:.7cqw .7cqw 0 rgba(122,20,8,.55)}
@media (prefers-reduced-motion:no-preference){
  /* lands, then shakes, then holds still for the rest of the beat */
  .framecheck:not(.hidden) .mvb-card{animation:mvbIn .42s cubic-bezier(.16,1.5,.32,1) both,
    fcShake .62s .42s ease-in-out 2}
}
@keyframes fcShake{
  0%,100%{translate:0 0;rotate:0deg}
  15%{translate:-2.4% 0;rotate:-2.2deg}
  30%{translate:2.2% 0;rotate:1.9deg}
  45%{translate:-1.7% 0;rotate:-1.4deg}
  60%{translate:1.3% 0;rotate:1deg}
  75%{translate:-.7% 0;rotate:-.5deg}}
