/* Per-screen layouts. body[data-screen] gates which chrome shows. */

.screen{flex:1;display:flex;flex-direction:column;min-height:0}

/* ============================================================
   SHARED RESPONSIVE PRIMITIVES
   The three immersive screens (home, solo, lobby/party) place their
   components as percentages of a fixed 1440×810 design box. To keep those
   percentages coherent at every viewport, the box is LETTERBOXED to the
   design aspect ratio and centred; the paper background fills any margin.
   This is why one set of %-coordinates renders identically at 16:9, 4:3,
   ultrawide and portrait — the box is always 1440:810.
   ============================================================ */
:root{
  --pp-ar: 1440 / 810;            /* design aspect ratio (16:9) */
  --pp-maxw: 2200px;              /* sanity cap on very large displays */
  --pp-gap: clamp(6px, 1.2vmin, 18px);
}
/* the fixed viewport frame that centres a stage and holds the wallpaper */
.pp-frame{position:fixed;inset:0;overflow:hidden;display:grid;place-items:center}
.pp-wall{position:absolute;inset:0;z-index:0;background-position:center;background-size:cover}
/* the centred, fixed-aspect content stage — everything inside is % of THIS */
.pp-stage{position:relative;z-index:1;overflow:hidden;
  width:min(100dvw, calc(100dvh * (var(--pp-ar))), var(--pp-maxw));
  aspect-ratio:var(--pp-ar);}
/* portrait / very narrow: use the full width as a horizontal band rather
   than shrinking the whole 16:9 box into a tiny letterbox */
@media (max-aspect-ratio: 1/1){
  .pp-stage{width:100dvw;}
}

/* =================== HOME (Figma "Screen / Home / Desktop v2", 50:72) =================== */
:root{
  --fig-navy:#3172C3; --fig-blue:#124C9D; --fig-red:#DE7234;
  --fig-yellow:#EFB43F; --fig-cream:#F1E5D3; --fig-muted:#DAD3CA;
  --fig-ink:#17356F;
}
/* home: fixed frame + letterboxed 16:9 stage; paper wallpaper fills margins */
body[data-screen="home"] .wrap{max-width:none;padding:0}
#scrHome{position:fixed;inset:0;overflow:hidden;display:grid;place-items:end center;background:var(--fig-cream)}
#scrHome::before{content:"";position:absolute;inset:0;z-index:0;
  background:url(../assets/ui/figma/home-bg2.webp) center/cover}
.homestage{position:relative;z-index:1;overflow:hidden;background:none;
  width:min(100dvw, calc(100dvh * (var(--pp-ar))), var(--pp-maxw));
  aspect-ratio:var(--pp-ar)}
@media (max-aspect-ratio: 1/1){ .homestage{width:100dvw} }
.campark{position:absolute;left:4px;top:4px;width:2px;height:2px;overflow:hidden;opacity:.04;pointer-events:none;z-index:0}
.campark .slot{width:2px;height:2px}

.fh{position:absolute;display:block}
.fh.bg{inset:0;width:100%;height:100%;object-fit:cover;z-index:1}
.fh img{width:100%;display:block}

/* ---------- everything is alive ---------- */
/* generic: lift on hover, wiggle on click (js adds .shook) */
.iv{cursor:pointer;transition:transform .18s ease, filter .18s ease}
.iv:hover{transform:translateY(-6px) rotate(-1.2deg);filter:brightness(1.04) drop-shadow(0 6px 8px rgba(23,53,111,.22))}
.iv.wave{z-index:2;transition:scale .25s ease,filter .18s ease;transform-origin:50% 100%}
.iv.wave:nth-of-type(1){animation:swellA 7.5s ease-in-out infinite}
.iv.wave:nth-of-type(2){animation:swellB 9s ease-in-out infinite}
.iv.wave:nth-of-type(3){animation:swellC 8.2s ease-in-out infinite}
.iv.wave:hover{scale:1.008 1.025}
@keyframes swellA{0%,100%{transform:translateX(0) scaleY(1)}50%{transform:translateX(1%) scaleY(1.018)}}
@keyframes swellB{0%,100%{transform:translateX(0) scaleY(1)}50%{transform:translateX(-1.2%) scaleY(1.02)}}
@keyframes swellC{0%,100%{transform:translateX(0) scaleY(1.01)}50%{transform:translateX(.8%) scaleY(1.025)}}
.iv.conf:hover{transform:translateY(-7px) rotate(9deg) scale(1.1)}
.iv.boat{animation:boatrock 3.2s ease-in-out infinite;transform-origin:50% 92%}
/* the boat's LinkedIn link: its own box is empty (the img is absolute), so the
   keyboard focus ring goes on the boat itself */
.boat-link:focus-visible{outline:none}
.boat-link:focus-visible > #homeBoat{outline:3px solid #fff;outline-offset:4px;border-radius:12px}
@keyframes boatrock{0%,100%{transform:rotate(-4deg) translateY(0)}33%{transform:rotate(2deg) translateY(-9px)}66%{transform:rotate(4deg) translateY(-3px)}}

/* The page's original confetti is permanent. These are extra scraps tucked
   behind the PARTY card that spring out and stay for the whole hover. */
.party-hover-confetti{pointer-events:none;opacity:0;scale:.14;rotate:var(--party-card-r,0deg);
  translate:var(--party-card-x,0) var(--party-card-y,0);
  transition:opacity .16s ease,scale .42s cubic-bezier(.18,1.55,.34,1),
    translate .42s cubic-bezier(.18,1.55,.34,1),rotate .42s cubic-bezier(.18,1.55,.34,1)}
#partyHoverLeftTop{--party-card-x:120%;--party-card-y:20%;--party-card-r:-14deg;transition-delay:20ms}
#partyHoverRightTop{--party-card-x:-120%;--party-card-y:20%;--party-card-r:17deg;transition-delay:65ms}
#partyHoverLeftBottom{--party-card-x:130%;--party-card-y:-100%;--party-card-r:-19deg;transition-delay:105ms}
#partyHoverRightBottom{--party-card-x:-120%;--party-card-y:-95%;--party-card-r:20deg;transition-delay:145ms}
.homestage:has(#modeParty:hover) .party-hover-confetti,
.homestage:has(#modeParty:focus-visible) .party-hover-confetti{
  opacity:1;scale:1;translate:0 0
}

.shook{animation:ivshake .55s cubic-bezier(.36,.07,.19,.97)}
.spun{animation:ivspin .65s cubic-bezier(.3,.8,.4,1)}
.popped{animation:ivpop .5s cubic-bezier(.2,1.6,.4,1)}
@keyframes ivshake{
  0%,100%{transform:none}
  15%{transform:translateX(-7px) rotate(-4deg)}
  35%{transform:translateX(6px) rotate(3deg)}
  55%{transform:translateX(-4px) rotate(-2deg)}
  75%{transform:translateX(3px) rotate(1.2deg)}
}
@keyframes ivspin{0%{transform:rotate(0) scale(1)}60%{transform:rotate(340deg) scale(1.12)}100%{transform:rotate(360deg) scale(1)}}
@keyframes ivpop{0%{transform:scale(1)}40%{transform:scale(1.22) rotate(4deg)}70%{transform:scale(.94)}100%{transform:scale(1)}}
#logoHomeCraft{animation:logofloat 8s ease-in-out infinite;transition:translate .2s ease, scale .2s ease}
#logoHomeCraft:hover{translate:0 -5px;scale:1.015}
@keyframes logofloat{0%,30%,100%{transform:rotate(0) translateY(0)}60%{transform:rotate(.5deg) translateY(-4px)}}

/* ---------- mode cards: hang from their tape and slip when touched ---------- */
.fh.card{background:none;border:none;padding:0;cursor:pointer;transform-origin:50% -10%;
  animation:cardfloat 7s ease-in-out infinite;
  transition:rotate .22s cubic-bezier(.3,1.4,.5,1), translate .22s cubic-bezier(.3,1.4,.5,1)}
#modeVS.card{animation-delay:-2.3s}
#modeParty.card{animation-delay:-4.6s}
@keyframes cardfloat{0%,35%,100%{transform:translateY(0)}65%{transform:translateY(-4px)}}

/* The choices arrive like paper cards being placed onto the desk. Their
   paths follow the composition, then the existing gentle float takes over. */
.homestage.home-entering #modeParty.card>img{
  --home-in-x:0%;--home-in-y:-72%;--home-in-r:8deg;
  transform-origin:50% -10%;
  animation:homeCardEnter .92s cubic-bezier(.16,1.38,.32,1) .10s both}
.homestage.home-entering #modeVS.card>img{
  --home-in-x:-50%;--home-in-y:48%;--home-in-r:-8deg;
  transform-origin:50% -10%;
  animation:homeCardEnter .96s cubic-bezier(.16,1.38,.32,1) .30s both}
.homestage.home-entering #modeAI.card>img{
  --home-in-x:64%;--home-in-y:18%;--home-in-r:9deg;
  transform-origin:50% -10%;
  animation:homeCardEnter 1s cubic-bezier(.16,1.38,.32,1) .50s both}
/* Each tape lands after the card it holds down. It arrives from much further
   out than the card did — a big slap that settles — so the eye is pulled to
   the tape rather than it just appearing. */
.homestage.home-entering :is(#tapeParty,#tapeVS,#tapeSolo){
  opacity:0;animation:homeTapeSlap .66s cubic-bezier(.16,1.5,.32,1) both}
.homestage.home-entering #tapeParty{animation-delay:.78s}
.homestage.home-entering #tapeVS{animation-delay:1.02s}
.homestage.home-entering #tapeSolo{animation-delay:1.26s}
@keyframes homeCardEnter{
  0%{opacity:0;translate:var(--home-in-x) var(--home-in-y);
    rotate:var(--home-in-r);scale:.76}
  68%{opacity:1;translate:0 1.8%;rotate:-1.6deg;scale:1.045}
  84%{translate:0 -.7%;rotate:.7deg;scale:.985}
  100%{opacity:1;translate:0 0;rotate:0deg;scale:1}}
@keyframes homeTapeSlap{
  0%{opacity:0;transform:scale(2.6) rotate(-14deg)}
  46%{opacity:1;transform:scale(1.22) rotate(5deg)}
  74%{transform:scale(.93) rotate(-2.5deg)}
  89%{transform:scale(1.035) rotate(1deg)}
  100%{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .homestage.home-entering :is(#tapeParty,#tapeVS,#tapeSolo){opacity:1!important}
}

/* ---------- leaving: the card you pressed comes off the board ----------
   The entrance deals three cards onto a desk, so picking one has to be that
   same gesture in reverse — and for the ~0.4s before the next screen exists
   it is the ONLY answer the player gets that their press registered.

   The chosen card presses IN first and then pops out towards the viewer: the
   anticipation is what makes the pop read as a pop rather than a zoom. Its
   tape lets go a beat ahead of it, because a taped-down card cannot leave
   until the tape has. Everything else — the two cards not chosen, the logo,
   the robot, his bubble, the boat, the confetti — drops away underneath,
   quieter and faster, so the pick stays the loudest thing on screen.

   `.home-leaving` is transient, exactly like `.home-entering`: it is cleared
   by playHomeEntrance() on the way back in, so Home is never found parked at
   the end of an exit with everything at opacity 0. */
.homestage.home-leaving{pointer-events:none}
.homestage.home-leaving .fh.card>img{
  transform-origin:50% -10%;                    /* the unchosen ones swing off their tape */
  animation:homeCardDrop .34s cubic-bezier(.55,0,.85,.4) both}
.homestage.home-leaving
  :is(#logoHomeCraft,#homeRobot,.fh.bubble,#homeBoat,.pp-corner,.home-legal,.fh.iv.conf,
      #tapeParty,#tapeVS,#tapeSolo,#tapeTitleA,#tapeTitleB){
  animation:homePieceOut .3s ease-in both}
/* the chosen pair. No transform-origin: this one scales about its own centre,
   towards the player, not about the tape it is hanging from. */
.homestage.home-leaving[data-pick="ai"]    #modeAI.card>img,
.homestage.home-leaving[data-pick="vs"]    #modeVS.card>img,
.homestage.home-leaving[data-pick="party"] #modeParty.card>img{
  transform-origin:50% 50%;
  animation:homeCardPick .44s cubic-bezier(.36,0,.24,1) .07s both}
.homestage.home-leaving[data-pick="ai"]    #tapeSolo,
.homestage.home-leaving[data-pick="vs"]    #tapeVS,
.homestage.home-leaving[data-pick="party"] #tapeParty{
  animation:homeTapePeel .3s cubic-bezier(.3,0,.4,1) both}
@keyframes homeCardPick{
  0%  {opacity:1;translate:0 0;rotate:0deg;scale:1}
  24% {opacity:1;translate:0 2.4%;rotate:-2.2deg;scale:.92}
  100%{opacity:0;translate:0 -7%;rotate:2.4deg;scale:1.42}}
@keyframes homeCardDrop{
  0%  {opacity:1;translate:0 0;rotate:0deg;scale:1}
  100%{opacity:0;translate:0 7%;rotate:-5deg;scale:.92}}
@keyframes homeTapePeel{
  0%  {opacity:1;transform:none}
  35% {opacity:1;transform:scale(1.18) rotate(-9deg)}
  100%{opacity:0;transform:translateY(-38%) scale(.6) rotate(22deg)}}
@keyframes homePieceOut{
  0%  {opacity:1;translate:0 0;scale:1}
  100%{opacity:0;translate:0 2.6%;scale:.97}}

/* ---------- the boot hold ----------
   `body[data-boot]` is written in the markup and cleared the same frame Home
   is released (cameraGate.js hands over, homeAlive.js takes it). Home is HELD,
   not hidden: the wallpaper and the waves stay up and every paper is parked
   at the state its entrance starts from. A finished screen sitting behind the
   ENABLE CAMERA scrim reads as a screen being withheld from you; a screen
   visibly still arriving reads as one that is loading — which is the truth,
   because MediaPipe's wasm and both model files are downloading behind it. */
body[data-boot] .homestage
  :is(#logoHomeCraft,.fh.card,.fh.iv.tape,.fh.iv.conf,#homeBoat,#homeRobot,.fh.bubble,
      .pp-corner){
  opacity:0;translate:0 2.4%;pointer-events:none}

/* Releasing it. The cards and tapes already own an entrance; this is for the
   pieces that don't — the logo, the robot, his bubble and the boat. It is a
   transient class rather than a state selector for the same reason the
   results papers' [data-enter] is: a rule keyed on a lasting state replays
   every time its element comes back from display:none. It also outranks the
   idle float/wave/rock animations for its 1.8s, exactly as the mode cards'
   entrance outranks `cardfloat`, and hands them back afterwards.
   The confetti is deliberately NOT in here: spinConfetti() throws it in the
   same beat, and `ivspin` is the better arrival for a paper scrap. */
.homestage.home-arriving :is(#logoHomeCraft,#homeRobot,.fh.bubble,#homeBoat,.pp-corner,.home-legal){
  animation:homePieceIn .62s cubic-bezier(.16,1.38,.32,1) both}
.homestage.home-arriving #logoHomeCraft{animation-delay:.02s}
.homestage.home-arriving #homeRobot{animation-delay:.2s}
.homestage.home-arriving #homeBoat{animation-delay:.34s}
.homestage.home-arriving .fh.bubble{animation-delay:.46s}
.homestage.home-arriving :is(.pp-corner,.home-legal){animation-delay:1.1s}
@keyframes homePieceIn{
  0%  {opacity:0;translate:0 3.4%;scale:.94}
  62% {opacity:1;translate:0 -.7%;scale:1.02}
  100%{opacity:1;translate:0 0;scale:1}}
@media (prefers-reduced-motion:reduce){
  .homestage.home-arriving
    :is(#logoHomeCraft,#homeRobot,.fh.bubble,#homeBoat,.pp-corner){
    animation:none;opacity:1}
}
.fh.card:hover{rotate:-3.2deg;translate:0 7px}
.fh.card:active{rotate:-4.5deg;translate:0 11px}
#modeParty.card:hover{rotate:2.8deg;translate:0 7px}   /* taped at top-left, tips the other way */
#modeParty.card:active{rotate:4deg;translate:0 11px}
/* the card's tape twists with it */
.homestage:has(#modeAI:hover)    #tapeSolo{transform:rotate(-9deg) scale(1.05)}
.homestage:has(#modeVS:hover)    #tapeVS{transform:rotate(-8deg) scale(1.05)}
.homestage:has(#modeParty:hover) #tapeParty{transform:rotate(8deg) scale(1.05)}
.homestage:has(#logoHomeCraft:hover) #tapeTitleA{transform:rotate(7deg)}
.homestage:has(#logoHomeCraft:hover) #tapeTitleB{transform:rotate(-6deg)}

/* ---------- the robot: a jointed puppet, not four sprites ----------
   The art rect (see THE PAPER ROBOT RIG below for what that means) sized and
   hung so his HEAD lands exactly on the old head sprite's ink — the speech
   bubble's tail is aimed at that head and was never going to be re-authored.
   The idle wave, the lean, the breath and the tap reaction all live in
   js/paperRig.js now; the four @keyframes that used to be here were a
   symmetric CSS sine on each sprite, which is the one thing a jointed rig
   exists not to look like. */
.homerobot{pointer-events:none;container-type:inline-size;aspect-ratio:1087/1309}

/* ---------- speech bubble ----------
   Figma 302:637 supplies the blank 302:638 paper. The current composition's
   visible live-copy reference is its sibling 302:635; the older Beiruti
   sentence inside the component (302:639) is hidden. The paper is narrowed
   from its authored 25% to 21.6% so its 1.06 tap-pop retains a 12px+ design
   gap from the 1v1 card instead of sweeping over it. */
.fh.bubble{background:none;border:none;padding:0;cursor:pointer;
  container-type:inline-size;transition:transform .18s ease}
.fh.bubble:hover{transform:translateY(-5px) rotate(-1deg) scale(1.02)}
/* Figma 302:637 carries its own DROP_SHADOW — rgba(0,0,0,.35), y 3.49,
   blur 1.99 on a 360px frame — and it had never been ported, so the one
   paper on Home that is supposed to float sat flat on the wallpaper. It goes
   on the PAPER as a drop-shadow, not on the button as a box-shadow: the art
   is torn paper in a transparent bounding box, and a box-shadow would draw a
   rectangle around it. */
.fh.bubble>img{filter:drop-shadow(0 .97cqw .554cqw rgba(0,0,0,.35))}
/* The text box is the balloon's interior, centred on the same point Figma's
   text rectangle is (50.28% / 40.17%) but taller and wider than the authored
   276x82 node, and the copy is centred in it on BOTH axes rather than parked
   at its top-left. Figma's own STRIKE A POSE! draws ~394px wide at its
   authored 51.88px — wider than the 360px paper, never mind the text node —
   so laid out verbatim it wrapped to two lines and the second line was cut
   off by the box. `safe center` keeps a line that somehow still overflows
   readable from the top instead of bleeding out of both ends, and the fitter
   in homeAlive.js measures every phrase against this box before it shows. */
.fh.bubble span{position:absolute;left:8.28%;top:12.9%;width:84%;height:54.5%;
  display:grid;align-content:center;align-content:safe center;
  text-align:center;box-sizing:border-box;
  overflow:visible;overflow-wrap:anywhere;text-wrap:balance;
  white-space:pre-line;
  font-family:var(--f-lobby);font-weight:400;color:#17356f;
  font-size:14.411cqw;line-height:1.02;letter-spacing:.01em;pointer-events:none;
  text-shadow:.833cqw .833cqw 0 rgba(255,255,255,.9)}
.fh.bubble.pop{animation:bubblepop .4s cubic-bezier(.2,1.6,.4,1)}
@keyframes bubblepop{0%{transform:scale(.9)}60%{transform:scale(1.06)}100%{transform:none}}

.pp-corner{position:absolute;bottom:3.5%;z-index:10;display:flex;gap:12px}
.pp-corner.left{left:2%}
.pp-circle{width:54px;height:54px;border-radius:50%;background:var(--fig-cream);border:none;display:flex;align-items:center;justify-content:center;color:var(--fig-blue);box-shadow:0 4px 10px rgba(23,53,111,.35);cursor:pointer;transition:transform .15s ease;opacity:.92}
.pp-circle:hover{transform:scale(1.08) rotate(12deg)}
.pp-circle:active{transform:scale(.94) rotate(0deg);box-shadow:0 2px 5px rgba(23,53,111,.35)}
.pp-circle svg{width:24px;height:24px}
/* Privacy / Terms. Centred with left+width, never translate: homePieceIn
   animates `translate` and would shove it off centre while Home arrives. */
.home-legal{position:absolute;left:40%;width:20%;bottom:1.6%;z-index:10;
  display:flex;justify-content:center;gap:1.4em;
  font:700 clamp(10px,min(1vw,1.8vh),15px)/1 var(--f-label);letter-spacing:.03em}
.home-legal a{color:rgba(255,247,230,.86);text-decoration:none;
  transition:color .15s ease,transform .15s ease;display:inline-block}
.home-legal a:hover,.home-legal a:focus-visible{color:#fff;text-decoration:underline;transform:translateY(-1px)}
.home-legal a:active{transform:translateY(1px)}

/* =================== SHARED GAMEPLAY ENTRANCE ===================
   Solo, 1v1 and Party count in with the SAME motion language as Home: every
   piece travels a long way in from the edge it logically comes from, scales
   .76 -> 1 with a settle, and the tape that pins it SLAPS down out of nowhere
   about .58s later. The eye follows paper -> tape, paper -> tape across the
   board instead of watching a HUD switch on.

   What it replaced: every paper made the same small -42% drop in .66s, and
   ALL the tape — nine or ten pieces — shared ONE .72s delay, so the whole
   tape layer arrived in a single flash. That is the "nothing pops up, they
   just appear, all the tape appears together" complaint.

   ONE shared beat index, `--gi`, orders the composition:

     0    run / match clock (top-left)      + its tape
     1    left score card (1v1 you, party blue)   + tape
     2    word clock (top-centre)           + tape
     3    right score (solo SCORE, 1v1 rival, party orange) + tape
     4    solo STREAK / the left guess paper       + tape
     5    the right guess paper(s)          + tape
     5.4  solo SKIP                         + tape
     6.2  the WORD card — the hero, last, biggest travel + right tape
     6.9  the word card's second tape

   Papers land at `.04s + gi*.145s`; the tape that holds a paper down lands at
   `.615s + gi*.145s`, the same paper->tape lag Home uses. Tapes are the only
   elements carrying `--gi` inline in index.html, because most of them have no
   id or distinguishing class — that is all `--gi` does there.

   The whole sequence finishes at ~2.12s, inside the FIXED 3s countdown
   (PRECOUNT_STEP_MS x3 locally, server `preCountMs` online), so no network
   timing moves and the client never starts late. `body[data-intro="game"]`
   in overlays.css holds the 3-2-1 scrim off the board until the assembly is
   done — before that, this all played under a brightness(.66) wash behind a
   giant numeral and was practically invisible.

   Only presentation is animated; gameplay state and timing are untouched. */
@media (prefers-reduced-motion:no-preference){
  /* one rule animates every paper; each rule below only supplies the beat,
     the direction it flies in from, and its duration */
  :is(.solostage,.matchstage,.pmstage).game-intro
    :is(.sp-card,.v1-scorecard,.sp-word,.sp-skip,.sp-guess:not(.empty),.grobot){
    animation:gamePaperIn var(--gi-dur,.92s) cubic-bezier(.16,1.38,.32,1)
      calc(.04s + var(--gi,0) * .145s) both}
  /* run/match clock — slides in off the left edge */
  :is(.solostage,.matchstage,.pmstage).game-intro .sp-rtimer{
    --gi:0;--game-in-x:-270%;--game-in-y:-24%;--game-in-r:-9deg;--gi-dur:.9s}
  /* the two score papers hang off the top edge, tipped away from centre */
  :is(.matchstage,.pmstage).game-intro :is(.v1-scorecard.you,.pm-score.blue){
    --gi:1;--game-in-x:-72%;--game-in-y:-400%;--game-in-r:-8deg;--gi-dur:.94s}
  :is(.solostage,.matchstage,.pmstage).game-intro .sp-wtimer{
    --gi:2;--game-in-x:0%;--game-in-y:-440%;--game-in-r:6deg;--gi-dur:.96s}
  :is(.solostage).game-intro .sp-score{
    --gi:3;--game-in-x:130%;--game-in-y:-330%;--game-in-r:9deg;--gi-dur:.94s}
  :is(.matchstage,.pmstage).game-intro :is(.v1-scorecard.rival,.pm-score.orange){
    --gi:3;--game-in-x:72%;--game-in-y:-400%;--game-in-r:8deg;--gi-dur:.94s}
  /* streak lives against the right edge, so it comes straight off it */
  :is(.solostage).game-intro .sp-streak{
    --gi:4;--game-in-x:250%;--game-in-y:18%;--game-in-r:-8deg;--gi-dur:.9s}
  /* guess papers slide in from whichever side of the board they sit on */
  :is(.solostage,.matchstage,.pmstage).game-intro .sp-guess:not(.empty){
    --gi:5;--game-in-x:180%;--game-in-y:26%;--game-in-r:7deg;--gi-dur:.88s}
  :is(.matchstage,.pmstage).game-intro .sp-guess:is(.you,.mine):not(.empty){
    --gi:4;--game-in-x:-180%;--game-in-r:-7deg}
  /* The robot walks on just AFTER the guess paper he leans on, from whichever
     edge he stands against — `data-side` is which way his bubble opens, so a
     bubble opening right means he is on the LEFT of the board, propped on the
     `you`/`mine` paper at beat 4; everyone else props on the beat-5 paper.
     He used to land at a flat 4.6, which put the two outer rigs on screen
     before the paper under their feet existed. */
  :is(.solostage,.matchstage,.pmstage).game-intro .grobot{
    --gi:5.15;--game-in-x:165%;--game-in-y:26%;--game-in-r:9deg;--gi-dur:.9s}
  :is(.solostage,.matchstage,.pmstage).game-intro .grobot[data-side="right"]{
    --gi:4.15;--game-in-x:-165%;--game-in-r:-9deg}
  :is(.solostage,.matchstage,.pmstage).game-intro .sp-skip{
    --gi:5.4;--game-in-x:-200%;--game-in-y:55%;--game-in-r:-8deg;--gi-dur:.86s;
    --game-intro-opacity:.55}
  /* the word is the hero: last in, furthest travel, longest settle */
  :is(.solostage,.matchstage,.pmstage).game-intro .sp-word{
    --gi:6.2;--game-in-x:0%;--game-in-y:210%;--game-in-r:3deg;--gi-dur:.98s}

  /* Tape: the same slap Home uses — huge, rotated, several settling bounces —
     one piece at a time, .145s apart, each landing after the paper it pins.
     History tapes are excluded: their paper is `.empty` at count-in and the
     :has() rules below hold them at opacity 0, so animating them would have
     flashed a tape onto a card that is not there. */
  :is(.solostage,.matchstage,.pmstage).game-intro
    :is(.sp-tape,.v1-tape,.pm-tape,.sp-skip-tape):not(.sp-tape-history,
      .v1-tape-history,.pm-tape-history){
    animation:gameTapeStick .5s cubic-bezier(.16,1.5,.32,1)
      calc(.615s + var(--gi,0) * .145s) both}

  /* the split divider frames the board, so it draws itself first */
  :is(.matchstage,.pmstage).game-intro :is(.pm-divider,.v1-divider){
    animation:gameDividerIn .8s cubic-bezier(.16,1.38,.32,1) .09s both}
  /* party's centre tiles are seats, not paper — they keep their Figma --rot */
  .pmstage.game-intro .pm-tile{
    animation:gameTileIn .88s cubic-bezier(.16,1.38,.32,1)
      calc(.04s + var(--gi,0) * .145s) both}
  .pmstage.game-intro .pm-tile.blue{--game-in-x:-120%;--game-in-r:-7deg}
  .pmstage.game-intro .pm-tile.orange{--game-in-x:120%;--game-in-r:7deg}
  .pmstage.game-intro [data-seat="blue1"]{--gi:0}
  .pmstage.game-intro [data-seat="orange1"]{--gi:.5}
  .pmstage.game-intro [data-seat="blue2"]{--gi:1}
  .pmstage.game-intro [data-seat="orange2"]{--gi:1.5}
}
@keyframes gamePaperIn{
  0%{opacity:0;translate:var(--game-in-x,0%) var(--game-in-y,-40%);
    rotate:var(--game-in-r,0deg);scale:.76}
  68%{opacity:var(--game-intro-opacity,1);translate:0 1.6%;
    rotate:calc(var(--game-in-r,0deg) * -.15);scale:1.045}
  84%{translate:0 -.7%;rotate:.5deg;scale:.99}
  100%{opacity:var(--game-intro-opacity,1);translate:0 0;rotate:0deg;scale:1}}
/* the tape arrives from way outside the frame and settles — same shape as
   homeTapeSlap, so Home and a round in progress read as one game */
@keyframes gameTapeStick{
  0%{opacity:0;transform:scale(2.5) rotate(-13deg)}
  46%{opacity:1;transform:scale(1.2) rotate(5deg)}
  74%{transform:scale(.94) rotate(-2.4deg)}
  89%{transform:scale(1.03) rotate(.9deg)}
  100%{opacity:1;transform:none}}
@keyframes gameDividerIn{
  0%{opacity:0;scale:.5 1.22;translate:0 -7%}
  62%{opacity:1;scale:1.09 .98;translate:0 1%}
  84%{scale:.98 1.01;translate:0 -.4%}
  100%{opacity:1;scale:1;translate:0 0}}
/* a party seat must finish on its authored Figma --rot, never on 0deg */
@keyframes gameTileIn{
  0%{opacity:0;translate:var(--game-in-x,0%) var(--game-in-y,0%);
    rotate:calc(var(--rot,0deg) + var(--game-in-r,0deg));scale:.72}
  66%{opacity:1;translate:0 1.4%;rotate:calc(var(--rot,0deg) * 1.6);scale:1.045}
  85%{translate:0 -.5%;scale:.99}
  100%{opacity:1;translate:0 0;rotate:var(--rot,0deg);scale:1}}

/* =================== SOLO PLAY (Figma "Singe player" 150:454) =================== */
/* HUD overlay on the full-bleed camera. body[data-screen] hides the header;
   #scrSolo[data-rstate] gates the award/flash so states never co-render. */
/* solo: camera + HUD share the letterboxed 16:9 stage so the HUD always
   aligns to the camera edges; navy fills any margin */
body[data-screen="solo"] .wrap{max-width:none;padding:0}
#scrSolo{position:fixed;inset:0;overflow:hidden;display:grid;place-items:center;background:#0E1B4A}
.solostage{position:relative;z-index:1;overflow:hidden;background:#0E1B4A;font-family:var(--f-lobby,'Baloo 2',sans-serif);
  width:min(100dvw, calc(100dvh * (var(--pp-ar))), var(--pp-maxw));
  aspect-ratio:var(--pp-ar)}
@media (max-aspect-ratio: 1/1){ .solostage{width:100dvw} }
.solostage .slot{position:absolute;inset:0;z-index:0}

/* Camera fills the stage; the HUD is pure paper on top, so the dock's own
   chrome (brackets, live badge, AI pill) would just be noise here. */
body[data-screen="solo"] .camdock .livebadge,
body[data-screen="solo"] .camdock .aipill,
body[data-screen="solo"] .camdock .brackets{display:none}
.solostage .slot .camdock,.solostage .slot video{width:100%;height:100%}
.solostage .slot video{object-fit:cover}

/* These are exported from each Solo tape node, including the rotated canvas.
   Keeping the outer Figma bounds prevents generic tape art from being rotated
   a second time or exposing the wrong transparent padding. */
.sp-tape{position:absolute;z-index:6;pointer-events:none}
.sp-tape-history{transition:opacity .3s ease}
.solostage:has(#spGuessMid.empty) #spTapeGuessMid,
.solostage:has(#spGuessBot.empty) #spTapeGuessBottom{opacity:0}

/* green win flash */
/* YOU GOT IT, in green, and YOU RAN OUT OF TIME, in red — as big as the
   screen. The flash used to be a soft glow in the middle that faded over
   0.8s, and players looked straight past it. It is now the whole stage: a
   tint over the camera (so you flash green yourself), a thick border and a
   deep glow in from every edge, snapped on and HELD for about half a second
   before it fades, because a flash that starts fading on its first frame is
   the easy one to miss. The red is the same flash in the deadline red every
   clock in this game already uses. It sits over the camera and the robot and
   under nothing that matters: the award paper and confetti are fixed to the
   body, above every stage. */
.sp-flash{--fx:63,201,140;position:absolute;inset:0;z-index:8;pointer-events:none;opacity:0;
  background:rgba(var(--fx),.34);
  box-shadow:inset 0 0 0 min(1.3vw,2.3vh) rgba(var(--fx),.95),
    inset 0 0 min(14vw,25vh) min(4vw,7vh) rgba(var(--fx),.85)}
.sp-flash.miss{--fx:232,64,42}
.sp-flash.fire{animation:spflash 1.15s ease-out}
@keyframes spflash{0%{opacity:0}7%{opacity:1}45%{opacity:1}100%{opacity:0}}

/* --- paper cards -------------------------------------------------------
   Each card is an inline-size container, so `cqw` == 1% of the card width.
   Every font-size / offset / shadow below is the Figma value divided by that
   card's Figma width — which is why the three guess cards, being one scaled
   component, share a single rule. Paper art carries no CSS shadow of its own
   in Figma; the soft one here is what lifts it off the live camera. */
.sp-card,.sp-guess,.sp-word,.sp-skip,.sp-award{container-type:inline-size}
.sp-art,.gcard,.sp-word-bg{width:100%;display:block;
  filter:drop-shadow(0 min(.292vw,.519vh) min(.167vw,.296vh) rgba(0,0,0,.35))}

.sp-card{position:absolute;z-index:4}
.sp-card>span{position:absolute;left:0;width:100%;text-align:center;white-space:nowrap;overflow:hidden}
.sp-lab{font-family:var(--f-sign);font-weight:800;word-spacing:.12em}
.sp-val{font-family:var(--f-lobby);font-weight:400}

/* Round Timer — Figma card 231px wide */
.sp-rtimer .sp-art{aspect-ratio:2.319}
.sp-rtimer .sp-clock{position:absolute;left:7.8%;top:22.9%;width:23.4%;aspect-ratio:1;
  filter:drop-shadow(1.149cqw 1.149cqw 0 rgba(0,0,0,.5))}
.sp-rtimer .sp-val{top:19.7%;left:31.3%;width:61.5%;height:66%;color:#fff;
  font-size:24.63cqw;line-height:26.15cqw;text-shadow:1.609cqw 1.609cqw 0 rgba(0,0,0,.5)}

/* Word Timer — 252px */
.sp-wtimer .sp-art{aspect-ratio:2.250}
.sp-wtimer .sp-lab{top:-2.7%;color:#17356f;font-size:11.87cqw;line-height:23.41cqw;
  text-shadow:1.188cqw 1.188cqw 0 rgba(255,255,255,.9)}
.sp-wtimer .sp-val{top:35.8%;height:58%;color:#17356f;font-size:22.06cqw;line-height:23.41cqw;
  text-shadow:1.188cqw 1.188cqw 0 rgba(255,255,255,.9)}

/* Score — 227.7px */
.sp-score .sp-art{aspect-ratio:1.634}
.sp-score .sp-lab{top:.9%;color:#17356f;font-size:17.35cqw;line-height:26.31cqw;
  text-shadow:1.333cqw 1.333cqw 0 rgba(255,255,255,.9)}
.sp-score .sp-val{top:41%;color:#17356f;font-size:31.97cqw;line-height:26.31cqw;
  text-shadow:1.333cqw 1.333cqw 0 rgba(255,255,255,.9)}

/* Streak — 210px */
.sp-streak .sp-art{aspect-ratio:1.662}
.sp-streak .sp-lab{top:0;color:#fff;font-size:20.19cqw;line-height:30.63cqw;
  text-shadow:1.553cqw 1.553cqw 0 rgba(0,0,0,.5)}
.sp-streak .sp-val{top:35.6%;color:#fff;font-size:27.96cqw;line-height:30.63cqw;
  text-shadow:1.553cqw 1.553cqw 0 rgba(0,0,0,.5)}

/* pulse when score/streak change */
.sp-card.bump{animation:spbump .4s cubic-bezier(.2,1.6,.4,1)}
@keyframes spbump{0%{transform:scale(1)}45%{transform:scale(1.12)}100%{transform:scale(1)}}
/* Losing points is a HIT, not a bump: the card recoils rather than swelling,
   and the figure inside it flashes the same red the penalty arrived in, so
   the number you watched fly in is unmistakably the number that just came
   off the total. */
.sp-card.bump-hit,.v1-scorecard.bump-hit{animation:sphit .44s cubic-bezier(.2,1.5,.4,1)}
@keyframes sphit{0%{transform:scale(1)}
  22%{transform:scale(.9) rotate(-2.2deg)}
  58%{transform:scale(1.04) rotate(1deg)}
  100%{transform:scale(1) rotate(0)}}
.sp-card.bump-hit .sp-val,.v1-scorecard.bump-hit .sp-val{animation:sphitink .44s ease-out}
@keyframes sphitink{0%,45%{color:#e8402a}100%{color:inherit}}
/* word-timer runs red in the final seconds */
/* The word clock's deadline red. The RUN/MATCH clock gets its own, softer
   warning — it had none at all in any mode, so every game just stopped. The
   :is() matters: #soloRoundTime/#v1MatchTime/#pmMatchTime all live inside
   .sp-rtimer, which the old .sp-wtimer-only selector could never match. */
.sp-wtimer .sp-val.low{color:#e8402a;animation:sppulse .6s ease-in-out infinite}
.sp-rtimer .sp-val.low{color:#ffd04a;animation:sppulse 1s ease-in-out infinite}
/* a streak of zero is a real state, not a hidden x1 */
.sp-card.cold .sp-val{opacity:.55}
@keyframes sppulse{0%,100%{opacity:1}50%{opacity:.45}}

/* --- guess stack: one component at three scales, newest green on top --- */
.sp-guess{position:absolute;z-index:3;transition:opacity .3s ease}
.sp-guess .gcard{aspect-ratio:1.906}
.sp-guess .glabel,.sp-guess .gpct{position:absolute;left:0;width:100%;text-align:center;
  z-index:2;white-space:nowrap;overflow:hidden;
  text-shadow:.956cqw .956cqw 0 rgba(255,255,255,.9)}
.sp-guess .glabel{top:10%;font-family:var(--f-sign);font-weight:800;font-size:17.21cqw;line-height:18.84cqw;word-spacing:.1em}
.sp-guess .gpct{top:49%;font-family:var(--f-lobby);font-weight:400;font-size:22.94cqw;line-height:18.84cqw}
.sp-guess .glabel.fit-wrap{top:2%;height:43%;padding:0 4%;box-sizing:border-box;
  display:flex;align-items:center;justify-content:center;
  white-space:normal;text-wrap:balance;overflow-wrap:anywhere;
  line-height:.9;word-spacing:normal}
/* The stack sinks by shrinking, cooling green -> blue -> purple, and clearly
   fading. That separation keeps the current guess dominant at a glance. */
.sp-guess.top{opacity:1}
.sp-guess.top .glabel,.sp-guess.top .gpct{color:#1b7532}
.sp-guess.mid{opacity:.68}
.sp-guess.mid .glabel,.sp-guess.mid .gpct{color:#0d6ebb}
.sp-guess.bot{opacity:.38}
.sp-guess.bot .glabel,.sp-guess.bot .gpct{color:#583ca2}
.sp-guess.empty{opacity:0}
.sp-guess.pop{animation:gpop .42s cubic-bezier(.2,1.6,.4,1)}
@keyframes gpop{0%{transform:translateY(16%) scale(.82)}60%{transform:translateY(0) scale(1.06)}100%{transform:none}}
.sp-guess.shift{animation:gshift .34s ease-out}
@keyframes gshift{0%{transform:translateY(-16%)}100%{transform:none}}
.sp-guess.recognized{filter:brightness(1.09) saturate(1.1);
  animation:guessRecognized .72s cubic-bezier(.2,1.6,.4,1)}
@keyframes guessRecognized{
  0%{scale:1}
  42%{scale:1.11;rotate:-2deg}
  100%{scale:1;rotate:0deg}
}
/* Kept as a compatibility alias for existing paper controls that use the
   original lock-pop keyframe name. */
@keyframes v1locked{0%{scale:1}42%{scale:1.11;rotate:-2deg}100%{scale:1}}

/* ============ THE ROBOT — a rig, not a sticker (js/robot.js) ============
   Body sprite + face sprite composited into the body's head screen. The eight
   bodies were exported through ONE shared crop box, so they are in register
   with each other and the face is a single rectangle — only `present` (head
   leans right) and `sad` (head slumps) need a nudge, which is why one rule
   dresses all eight poses.

   Transforms are split across elements so none of them can clobber another —
   `animation-fill-mode:both` beats a plain declaration, and stacking two
   owners on one element was the whole reason the old robot could not move:
     .grobot      `scale` from the meter, plus the BREATH and LEAN that
                  js/robot.js writes into --rb-shift-x/y every frame. The
                  count-in entrance is an animation on this element and
                  outranks all of it for its ~1s, then hands back. There is
                  deliberately no `--rb-tilt`: the roll it used to carry moved
                  into the TORSO joint, where the head can counter it at the
                  neck, and a rig-wide rotate cannot be countered by anything.
                  Declaring it here anyway left a var nothing ever wrote.
     .rb-rig      the recognition SLAM, and nothing else.
     .rp          EVERY puppet part: js/robot.js composes that part's whole
                  joint chain (torso -> upper arm -> forearm -> hand, or
                  torso -> head -> eye) into one transform per part per frame.
     .rb-eye-*    a little more travel than the mouth gets, on top of the head
                  chain — that difference is the parallax that reads as a head
                  turning rather than a decal sliding.
     .rb-bubble   its own pop.

   The breath used to be a CSS keyframe: a symmetric sine on translate+rotate.
   It moved to JS because a sine is the universal signature of a cheap sprite
   — real breathing is asymmetric, and because the same loop has to lag the
   face behind the body, which needs a phase this side of the animation. */
/* z-index 2 puts him above the camera (0) and under every paper in all three
   modes — .sp-guess 3, .sp-card/.sp-word/.v1-guess 4, .pm-guess 7. He is a
   character standing BEHIND the board, so a slam can grow as big as it likes
   without ever covering a clock, a score or the word you are playing. */
.grobot{position:absolute;z-index:2;pointer-events:none;
  aspect-ratio:520/610;container-type:inline-size;
  scale:var(--rb-scale,1);
  /* percentages resolve against his own box, so every one of these is
     resolution-independent without a single px anywhere */
  translate:var(--rb-shift-x,0%) var(--rb-shift-y,0%);
  transform-origin:50% 94%;   /* his feet: a lean pivots there, not at his waist */
  transition:scale .3s cubic-bezier(.2,1.5,.4,1)}
/* The warmth behind him climbs with the meter, so "you are getting through"
   is legible from the corner of the eye without reading the number. */
.grobot::before{content:"";position:absolute;left:50%;top:44%;width:155%;
  aspect-ratio:1;translate:-50% -50%;z-index:-1;pointer-events:none;
  opacity:calc(var(--rb-hype,0) * .8);
  /* rbBurst is fill:both and ends at scale 1.3 / rotate 18deg, so dropping the
     slam class snapped the glow back to 1/0deg with only opacity easing. */
  transition:opacity .3s ease,scale .34s ease,rotate .34s ease;
  /* no blur filter: the gradient's own stops are already soft, and this layer
     animates its opacity every frame he charges — a filter pass on top of that
     is paint work for nothing, on a board that can have two rigs up at once */
  background:radial-gradient(circle,rgba(255,228,145,.6) 0 22%,
    rgba(255,203,66,.3) 44%,transparent 70%)}
/* THE PAPER IS IN FRONT OF HIM NOW (bubble z 4 against the rig's 3), AND IT
   CLEARS HIS FACE — the two have to be solved together.

   He used to stand in front of his own bubble, because the side gap had been
   tuned against the old bust and the opaque cream paper landed across this
   head — one eye included. Standing him in front fixed the face and broke the
   thing the paper is FOR: measured on the 640 canvas, his head spans 15%–84.3%
   of his box and the copy region starts 66.6% across, so the first sixth of
   every line was behind his head, and during the recognition slam (rig scales
   1.42x) it was nearly a fifth — of the loudest line in the game.

   So the ORDER changed and the gap did not. The gap was a red herring, and
   proving that took measuring the paper vertically: its balloon body bottoms
   out 20.8% down his box (22.1% for Solo's smaller paper) while his eye ink
   starts at 25.9%, so at rest the paper passes clean ABOVE his face and only
   the tail crosses him. Pushing it sideways to 46% did clear his head — and
   cost the tail its connection to his mouth, tripped the E2E windows in Solo
   and Party, and put the Solo paper 3.6px from WORD TIME. None of that bought
   anything the z-index had not already fixed.

   THE SLAM DOES NOT NEED AN EXCEPTION EITHER, and it cost a wrong fix to
   establish that. A QA pass reported the paper lying across his eye for the
   first 35-85ms of every recognition, with per-frame overlap figures — so the
   slam got its own wider gap and a 200ms ease to carry it there. Both were
   wrong, because the measurement was: it took the eye ELEMENT's rect, and
   every layer in this rig IS the whole 640 canvas, so it was really measuring
   canvas-against-paper, which of course intersects. Frozen through the WAAPI
   timeline at the exact reported instants and screenshotted, his face is
   clear at 35ms, 52ms and every frame after; a sweep of the gap from 28% to
   54% finds zero eye-ink overlap at any of them across the full 1500ms.

   What the wider gap DID do was throw the tail half his width from his mouth
   and 37px below it, pointing at empty body, for the whole 1.45s celebration
   — a real regression bought to fix an imaginary one. One gap, no exception,
   no transition. When a probe and a screenshot disagree, believe the
   screenshot. */
.grobot .rb-rig{position:absolute;inset:0;transform-origin:50% 92%;z-index:3}
/* THE SLAM MUST NOT SCALE HIM OFF THE SCREEN.
   1v1 and Party tuck their rigs THROUGH the outer edge so they never stand
   over a player's body, which leaves his ink starting within ~3px of x=0. A
   1.42x slam about the box centre then moves that ink edge OUTWARD by 42% of
   the distance from the centre to it — measured, 25px in 1v1 and 20px in
   Party — and the loudest beat in the game rendered with half his head, one
   eye and an entire arm sliced off by the viewport.

   The fix is not to move him inboard (he would cover the player again) or to
   shrink the slam (it is the payoff). It is to scale about his own INK EDGE:
   his silhouette spans 15.1%-84.9% of his box, so pinning the origin there
   makes that edge the FIXED POINT of the scale. It cannot travel outward at
   any scale — the guarantee holds for whatever the keyframe does, not just
   for 1.42 — and he grows inward, out of the edge he is tucked into, which is
   the direction with room. Solo is deliberately excluded: its rig sits at 70%
   with nothing to clip against, and the centred slam is what that composition
   was tuned with. */
:is(.matchstage,.pmstage) .grobot[data-side="right"] .rb-rig{transform-origin:15.1% 92%}
:is(.matchstage,.pmstage) .grobot[data-side="left"]  .rb-rig{transform-origin:84.9% 92%}

/* ---- THE PUPPET (assets/ui/rig/) ----
   Ten body parts, two gesture-hand alternatives and the registered face
   states all use the SAME 640x640 canvas. Every active layer is drawn at
   inset:0, so registration belongs to the art rather than to CSS offsets.

   .rb-canvas is that square, sized and offset so the robot's INK lands exactly
   where the old single-sprite robot's ink landed — his ink is 92.58% of the
   canvas tall and centred at 50.01% across, and .grobot's box is 520/610. So
   the five rig placements in index.html, the count-in entrance, the z-band and
   the speech-bubble tail geometry all keep working untouched; the swap is
   entirely inside this box. overflow stays visible: an arm thrown up in a
   celebration is allowed to leave the square. */
.grobot .rb-canvas{position:absolute;left:-13.66%;top:-5.69%;
  height:108.01%;aspect-ratio:1;pointer-events:none}
/* Every part is the whole canvas. `transform-origin:0 0` because js/robot.js
   writes the full joint chain as translate/rotate pairs in PERCENTAGES of this
   box — percentages resolve against the element's own border box, which is the
   same box for all thirteen, so one composed transform per part is exact at
   any size with not a pixel value anywhere. */
.grobot .rp{position:absolute;inset:0;width:100%;height:100%;
  transform-origin:0 0;will-change:transform}
/* Default puppet depth, back to front. The crossing poses selectively lift a
   forearm or hand without changing its transform owner, so the spring can
   travel continuously into and out of folded silhouettes. */
.grobot .rp:is([data-j="legL"],[data-j="legR"]){z-index:0}
.grobot .rp:is([data-j="uarmL"],[data-j="uarmR"]){z-index:1}
.grobot .rp:is([data-j="handL"],[data-j="handR"]){z-index:2}
.grobot .rp:is([data-j="farmL"],[data-j="farmR"]){z-index:3}
.grobot .rp[data-j="torso"]{z-index:4}
.grobot .rp[data-j="head"]{z-index:6}
.grobot .rp:is(.rb-eye-l,.rb-eye-r,.rb-mouth){z-index:9}

/* In-front-of-torso plane: belly fold, heart hand, hip hand and the hanging
   sad forearms. The head stays above this plane.

   KEYED ON `data-fold`, NEVER ON `data-pose`. The pose attribute changes on
   the frame the mood does; the limbs need the spring's whole travel to get
   back out of a fold. Keyed on the pose, leaving `thinking` dropped both arms
   BEHIND the torso while they were still crossed in front of it — they
   disappeared into the body and then slid out from under it. js/robot.js
   leads `data-fold` on the way in and lags it on the way out, so the plane is
   always in place for where the limbs actually ARE. */
/* thinking brings BOTH forearms forward: the chin arm folds up the FRONT of
   the body, so its forearm has to cross the torso, and its elbow sits inside
   the torso silhouette where the torso hides the joint. */
.grobot[data-side="left"][data-fold="thinking"] .rp:is([data-j="handL"],[data-j="farmL"],[data-j="farmR"]),
.grobot[data-side="right"][data-fold="thinking"] .rp:is([data-j="handR"],[data-j="farmR"],[data-j="farmL"]),
.grobot[data-side="left"][data-fold="present"]  .rp:is([data-j="handL"],[data-j="farmL"]),
.grobot[data-side="right"][data-fold="present"] .rp:is([data-j="handR"],[data-j="farmR"]),
.grobot[data-side="left"][data-fold="thumbsup"]  .rp:is([data-j="handL"],[data-j="farmL"]),
.grobot[data-side="right"][data-fold="thumbsup"] .rp:is([data-j="handR"],[data-j="farmR"]),
.grobot[data-fold="sad"]      .rp:is([data-j="handL"],[data-j="handR"],[data-j="farmL"],[data-j="farmR"]){z-index:5}
/* Chin and cheek hands cross the bezel; facial ink remains the top plane. */
.grobot[data-side="left"][data-fold="thinking"] .rp[data-j="handR"],
.grobot[data-side="right"][data-fold="thinking"] .rp[data-j="handL"],
.grobot[data-fold="surprised"] .rp:is([data-j="handL"],[data-j="handR"],[data-j="farmL"],[data-j="farmR"]){z-index:7}
/* ---- THE PAPER SHADOW, PER PIECE ----
   One drop-shadow on the whole canvas draws the puppet's OUTLINE and nothing
   else, which is fine for a sticker and wrong for eleven pieces of cut paper.
   It is why the folded poses read as flat ink printed on the torso: a fist at
   the chin is cream over cream over cream — hand, torso and head bezel all
   the same paper — and with only a silhouette shadow there is nothing at all
   to separate them.

   Figma does not draw them that way. Zoom body_thinking (286:611) and there
   is a distinct shadow under the fist where it crosses the head, and another
   under the crossing forearm. The rig section 353:752 authors it as a real
   per-layer DROP_SHADOW: y 12, blur 4, black 35% on its 1309 canvas — TIGHT
   and offset, which is why the usual worry about stacking them does not
   apply. That worry is about a soft halo bleeding sideways out of every edge;
   this is a third of a percent wide and lands almost entirely under the piece
   above it.

   The two lengths are Figma's, carried across the two boxes: `.grobot` is
   already an inline-size container and the art canvas is 1.267x its width, so
   12/1309 of the canvas is 1.16cqw here and 4/1309 is 0.39cqw. */
.grobot .rp{filter:drop-shadow(0 1.16cqw .39cqw rgba(0,0,0,.35))}
/* The face is INSIDE the head's screen. A shadow here falls on the blue panel
   the eyes are drawn on, which no cut-paper reading supports and which at
   gameplay size only reads as the eyes looking smudged. */
.grobot .rp:is(.rb-eye-l,.rb-eye-r,.rb-mouth){filter:none}
/* All face_xxx art is registered inside one 193.8/640 = 30.3%-wide face
   rectangle. The percentage lives in the generated art, not in CSS, so eyes,
   blinks, resting mouths and speech visemes cannot drift to different scales. */

/* THE FACE. Each exact Figma expression is split into a left eye, right eye
   and mouth layer; the refitted pack art supplies the neutral blink and five
   speech visemes. All three active layers are parented to the head, so they
   inherit its whole chain from js/robot.js and need no geometry here at all.

   Only the extra eye-look rides on top: the eyes travel a little further than
   the mouth does, and that difference is the parallax that reads as a head
   turning rather than a decal sliding across a screen. The blink is ART now,
   not a scaleY — neutral swaps to `blink_*`, and a drawn closed eyelid beats
   a squashed open one every time. */
.grobot .rb-eye-l,.grobot .rb-eye-r{
  translate:var(--rb-eye-x,0%) var(--rb-eye-y,0%)}


/* The recognition slam. He punches in oversized and wobbles like a sticker
   being stuck down, then settles back to his own size — ending at scale 1 so
   removing the class is seamless rather than a visible deflate. */
.grobot .rb-rig.slam{animation:rbSlam 1.45s cubic-bezier(.16,1.5,.32,1) both}
@keyframes rbSlam{
  0%  {opacity:.15;translate:0 14%;rotate:-15deg;scale:.4}
  18% {opacity:1;translate:0 -7%;rotate:7deg;scale:1.42}
  28% {translate:0 -2%;rotate:-4deg;scale:1.24}
  40% {translate:0 -1%;rotate:3deg;scale:1.36}
  56% {rotate:-2deg;scale:1.3}
  78% {translate:0 0;rotate:1deg;scale:1.32}
  100%{translate:0 0;rotate:0deg;scale:1}}
/* the glow punches with him rather than easing, which is what sells it as an
   impact instead of a fade-up */
.grobot:has(.rb-rig.slam)::before{animation:rbBurst 1.45s ease-out both}
@keyframes rbBurst{
  0%{opacity:0;scale:.3;rotate:-16deg}
  16%{opacity:.95;scale:1.16}
  60%{opacity:.6;scale:1.3;rotate:14deg}
  100%{opacity:.4;scale:1.24;rotate:18deg}}

/* --- what he says. The dedicated Figma ROBOT SPEECH BUBBLE (294:634). ---
   This is not the rough-edged home bubble. Its text-free paper layer is
   1356x880 and the measured tail tip is x=20.50%, y=96.59%. The paper flips
   for a robot on the opposite side; the sibling <b> never does.

   `--talk-gap-x` is the air between the visible tail and his face. It has been
   wrong in both directions: at 78% the tail was displaced 89–109px and read as
   unrelated to him; at 46% and again at 48% it tripped the E2E windows in both
   Solo and Party and left the slam's tail pointing at his knee.

   28% is the authored value and it is the right one, because the collision it
   was twice widened to solve does not exist — see the note by `.rb-rig`. The
   paper clears his face on the VERTICAL, at rest and through the slam, so the
   horizontal budget belongs entirely to keeping the tail on his cheek. 28% is
   also where the E2E windows for Solo (.26–.36) and Party (.26–.30) overlap.
   About 39px in desktop 1v1 and 32px in Party. ONE value, no per-beat
   exception — which is why there is no `@property` here either: nothing about
   this number animates, and registering it only to transition it was scaffold
   for a fix that turned out to be the regression.

   The two FALLBACKS are the real rest state, not decoration: robot.js writes
   `--talk-x` (measures 47.8–50.1% across the bands) and `--talk-y` (37.8%) on
   every frame, but not before its first one. The old 30.6% put anything
   reading this pre-first-frame 5.9% of his height off. */
.grobot .rb-bubble{--talk-x:49.7%;--talk-y:37.8%;--talk-gap-x:28%;
  position:absolute;bottom:calc(97.70% - var(--talk-y));width:122%;
  aspect-ratio:1356/880;z-index:4;opacity:0;pointer-events:none;
  container-type:inline-size;
  /* NO transition on the anchor. It eased because the anchor used to jump
     between per-pose constants; robot.js now recomputes it every frame from
     the head's live transform, so an ease is not smoothing anything — it is a
     200ms lag that leaves the tail pointing at where his mouth was, measurably
     11px above where it is. Continuous input, no easing. */}
/* The slam scales the body, not the speech paper. Solo keeps the normal live
   mouth anchor because WORD TIME sits directly above it. The two multiplayer
   edge rigs have open board above them, so their catch paper lifts by 50% of
   robot height: 23% follows the head's mechanical rise and the remaining 27%
   keeps the opaque balloon clear of both eyes throughout the 1.42x overshoot.
   The tail still points toward the head, and z-index 4 keeps the complete paper
   in front of the z-index 3 rig at every animation frame. */
.grobot:has(.rb-rig.slam) .rb-bubble{
  bottom:calc(97.70% - var(--talk-y))}
:is(.matchstage,.pmstage) .grobot:has(.rb-rig.slam) .rb-bubble{
  bottom:calc(97.70% - var(--talk-y) + 50%);
  transition:bottom .26s cubic-bezier(.16,1.5,.32,1)}
/* Aiming the tail is solved from the exported pixels, not eyeballed.
   `--talk-x/y` is the face target for that pose. At 122% bubble width, the
   20.50% tail is 25.01% of the robot's width from the paper's left edge; once
   mirrored it is 96.99% across, yielding the 74.99% right-edge equation.

   HIS HEAD MOVES BETWEEN POSES — this is the bit that looked broken. `present`
   leans right to 55% and `thinking` leans left to 48.7%, so one fixed offset
   pointed the tail a full 5% of his width off his mouth in the two poses he is
   in most. The side equations then leave the small `--talk-gap-x` breathing
   room from that target: left for a right-edge robot, right for a left-edge robot.
   Vertically, `97.70% - --talk-y` accounts for the small transparent strip
   below the tail so its tip stays on the mouth line, not the antenna. The
   generic 122% route serves multiplayer; Solo gets its collision-safe
   override immediately below. */
.grobot[data-side="left"]  .rb-bubble{
  right:calc(74.99% - var(--talk-x) + var(--talk-gap-x));
  transform-origin:79.4985% 96.5909%}
.grobot[data-side="right"] .rb-bubble{
  left:calc(var(--talk-x) - 25.01% + var(--talk-gap-x));
  transform-origin:20.5015% 96.5909%}
/* Solo's timer occupies the upper-left edge of the generic speech-paper
   route. A slightly smaller paper puts the entire rbSay animation into the
   open pocket between the timer, robot and guess stack. 108% × 20.5015% = a
   22.142% near-tail inset. The gap matches the generic route because the
   clearance it buys is symmetric — Solo's rig is the mirrored one. It is
   restated rather than inherited only because this rule already exists for
   the width; there is no per-beat override for it to have to outrank. */
#spRobot .rb-bubble{--talk-gap-x:28%;width:108%}
#spRobot.grobot[data-side="left"] .rb-bubble{
  right:calc(77.858% - var(--talk-x) + var(--talk-gap-x))}
.grobot[data-side="left"] .rb-bubble img{scale:-1 1}
.grobot .rb-bubble img{position:absolute;inset:0;width:100%;height:100%;
  filter:drop-shadow(0 min(.24vw,.42vh) min(.3vw,.53vh) rgba(0,0,0,.28))}
/* The Figma component authors the horizontal copy region at x=146..1210
   inside 1356px (10.77% / 78.47%). Its browser box is deliberately taller
   than Figma's text node because dialogue is editable and may wrap to two
   lines. textFit.js searches both axes before the paper is revealed; hidden
   overflow and anywhere wrapping are only final safety nets for extreme edits. */
.grobot .rb-bubble b{position:absolute;left:10.77%;top:12%;width:78.47%;height:58%;
  display:flex;align-items:center;justify-content:center;text-align:center;
  box-sizing:border-box;overflow:hidden;overflow-wrap:anywhere;word-break:normal;
  font-family:var(--f-sign);font-weight:800;font-size:14.75cqw;line-height:.94;
  color:#17356f;text-transform:uppercase;text-wrap:balance;
  /* the sign face sets a very tight space — the guess papers carry the same
     correction, or "NAILED IT!" reads as one word */
  word-spacing:.16em;
  text-shadow:.7cqw .7cqw 0 rgba(255,255,255,.85)}
.grobot .rb-bubble.show{animation:rbSay 1.7s cubic-bezier(.16,1.5,.32,1) both}
@keyframes rbSay{
  /* Scale and rotate around the measured tail tip. The paper grows out from
     the mouthward point instead of dropping through the robot on its way in. */
  0%  {opacity:0;translate:0 0;rotate:-7deg;scale:.55}
  12% {opacity:1;translate:0 -4%;rotate:2.5deg;scale:1.07}
  22% {translate:0 0;rotate:-1deg;scale:1}
  84% {opacity:1;translate:0 0;rotate:0deg;scale:1}
  100%{opacity:0;translate:0 -14%;rotate:3deg;scale:.9}}
/* Under reduced motion he still changes pose and still talks — those are
   information, not decoration. Only the movement goes. */
@media (prefers-reduced-motion:reduce){
  .grobot{transition:none}
  .grobot .rb-bubble.show{opacity:1}
}

/* --- word card: the yellow paper IS the progress bar (578px) --- */
.sp-word{position:absolute;z-index:4;transform-origin:50% 60%}
.sp-word .sp-word-bg{aspect-ratio:4.507}
.sp-word .sp-word-fill{position:absolute;left:1.7%;top:11.6%;height:76.8%;width:0;
  object-fit:fill;filter:none;transition:width .12s linear}
.sp-word .sp-word-text{position:absolute;left:0;top:22.6%;width:100%;text-align:center;
  font-family:var(--f-lobby);font-weight:400;font-size:14.69cqw;line-height:12.08cqw;
  color:#17356f;text-transform:uppercase;white-space:nowrap;overflow:hidden;
  text-shadow:.612cqw .612cqw 0 rgba(255,255,255,.9)}
.sp-word .sp-word-text.fit-wrap{top:8%;height:84%;padding:0 3%;box-sizing:border-box;
  display:flex;align-items:center;justify-content:center;
  white-space:normal;text-wrap:balance;overflow-wrap:anywhere;line-height:.88}
/* Recognition urgency is continuous rather than three abrupt presets. JS
   derives these variables from the displayed target confidence, so 40%
   visibly shakes harder/faster than 30%, and the motion keeps building all
   the way to the lock threshold. warm/hot/critical remain as semantic hooks
   for tests and future sound/color treatment. */
.sp-word.tracking{
  animation:spwordtracking var(--shake-speed,220ms) linear infinite
}
@keyframes spwordtracking{
  0%,100%{
    transform:translate(var(--shake-x-neg,-.22%),var(--shake-y-neg,-.12%))
      rotate(var(--shake-r-neg,-.2deg)) scale(var(--shake-scale,1))
  }
  25%{
    transform:translate(var(--shake-x,.22%),var(--shake-y,.1%))
      rotate(var(--shake-r,.2deg)) scale(var(--shake-scale,1))
  }
  50%{
    transform:translate(var(--shake-x-neg,-.22%),var(--shake-y,.1%))
      rotate(var(--shake-r-neg,-.2deg)) scale(var(--shake-scale,1))
  }
  75%{
    transform:translate(var(--shake-x,.22%),var(--shake-y-neg,-.12%))
      rotate(var(--shake-r,.2deg)) scale(var(--shake-scale,1))
  }
}
.sp-word.locked .sp-word-fill{filter:hue-rotate(78deg) saturate(.85) brightness(1.02)}
.matchstage.you-locked #v1WordFill{width:95%!important}
.sp-word.hit{animation:spwordhit .18s cubic-bezier(.2,.82,.3,1)}
@keyframes spwordhit{
  0%{transform:scale(1);filter:none}
  45%{transform:scale(1.045);filter:brightness(1.12) saturate(1.16)}
  100%{transform:scale(.985);filter:brightness(1.04)}
}
.sp-word.miss{animation:spwordmiss .5s ease-out}
@keyframes spwordmiss{0%,100%{transform:translateX(0);filter:none}25%{transform:translateX(-1%);filter:saturate(.4)}75%{transform:translateX(1%)}}
.sp-word.locked{animation:spwordpop .5s cubic-bezier(.2,1.6,.4,1)}
@keyframes spwordpop{0%{transform:scale(1)}45%{transform:scale(1.08)}100%{transform:scale(1)}}

/* --- skip + exit: exact updated Solo component exports --- */
.sp-skip{position:absolute;left:2.847222%;top:83.827160%;
  width:17.013889%;height:13.411111%;z-index:5;
  background:none;border:none;padding:0;cursor:pointer}
.sp-skip-tape{position:absolute;left:8.055556%;top:81.111111%;
  width:6.638%;height:5.997556%;z-index:6;object-fit:fill;pointer-events:none}
.sp-skip .sp-art{position:absolute;left:-.943388%;top:0;
  width:101.886776%;height:106.087043%;max-width:none;
  display:block;object-fit:fill;pointer-events:none;filter:none}
#scrSolo:not([data-rstate="guessing"]) .sp-skip{opacity:.55;pointer-events:none}
.sp-exit{position:absolute;left:.694444%;top:14.938272%;
  width:22.564833%;height:13.285260%;z-index:7;
  background:none;border:0;padding:0;cursor:pointer;opacity:.14;
  transition:opacity .18s ease,translate .2s cubic-bezier(.2,1.2,.35,1),
    scale .2s cubic-bezier(.2,1.2,.35,1),filter .18s ease}
.sp-exit img{position:absolute;left:-.517460%;top:0;
  width:100.517460%;height:104.297251%;max-width:none;
  display:block;object-fit:fill;pointer-events:none;filter:none}
.sp-exit:hover:not([disabled]),.sp-exit:focus-visible{
  opacity:1;translate:0 calc(-1 * min(.22vw,.39vh));scale:1.018;
  filter:brightness(1.035)}
.sp-exit:focus-visible{outline:min(.208vw,.37vh) solid #fff;outline-offset:min(.208vw,.37vh)}

/* In-match volume. The header is hidden on every play screen, so each stage
   carries its own — sitting just under the LEAVE paper and wearing the same
   ghosted treatment, so it is findable without ever competing with the round.
   Deliberately NOT .pbtn: that helper sets background:none and its own
   hover/active, both of which fight the solid circle this control needs. */
.sp-sound{position:absolute;left:2.4%;top:29.8%;width:4.3%;aspect-ratio:1;z-index:7;
  display:flex;align-items:center;justify-content:center;
  background:var(--fig-cream);color:var(--fig-blue);
  border:none;border-radius:50%;padding:0;cursor:pointer;opacity:.14;
  box-shadow:0 min(.28vw,.5vh) min(.55vw,.98vh) rgba(23,53,111,.35);
  transition:opacity .18s ease,translate .2s cubic-bezier(.2,1.2,.35,1),
    scale .2s cubic-bezier(.2,1.2,.35,1),filter .18s ease}
.sp-sound svg{width:56%;height:56%;display:block;pointer-events:none}
.sp-sound:hover,.sp-sound:focus-visible{
  opacity:1;translate:0 calc(-1 * min(.22vw,.39vh));scale:1.06}
.sp-sound:active{scale:.93;filter:brightness(.96)}
.sp-sound:focus-visible{outline:min(.208vw,.37vh) solid #fff;
  outline-offset:min(.208vw,.37vh)}
/* muted is a state, not a hover — it stays legible so you can see why it is quiet */
.sp-sound.muted{opacity:.62;background:#f4622e;color:#fff}
.sp-sound.muted:hover,.sp-sound.muted:focus-visible{opacity:1}
/* 1v1 and Party no longer carry a mid-match LEAVE paper, so the volume moves
   up into the slot it used to occupy — directly under the match clock, the
   one place on both boards already proven clear of live HUD. It also sits a
   little brighter there, because it is now the only chrome on the stage. */
.matchstage .sp-sound,.pmstage .sp-sound{left:1.32%;top:15.6%;width:4%;opacity:.2}
@media (prefers-reduced-motion:reduce){
  .sp-sound,.sp-sound:hover,.sp-sound:active{
    translate:0 0!important;scale:1!important;transition:opacity .18s ease}
}

/* --- points award (Figma "POINTS AWARD", 898px) ---
   It used to land dead centre, which is exactly where the player's own camera
   image — and their face — is. For half a second the one thing you are
   watching while you hold a pose was covered by the paper telling you the
   pose worked. It now lands in the empty left band of the board, opposite the
   guess stack and the robot, and is flown from there into the score card.
   `left`/`top` here are only the fallback: JS anchors it off the STAGE rect
   (soloRound.js), because the stage letterboxes and a flat viewport
   percentage lands outside the board entirely at 5:4. */
.sp-award{position:fixed;left:26%;top:50%;transform:translate(-50%,-50%) scale(.6);z-index:95;
  width:min(44vw,640px);pointer-events:none;opacity:0}
.sp-award::before{content:"";position:absolute;left:50%;top:50%;width:122%;aspect-ratio:1;
  translate:-50% -50%;z-index:-1;opacity:0;
  background:radial-gradient(circle,rgba(255,231,150,.85) 0 26%,
    rgba(255,205,64,.42) 44%,rgba(255,190,40,.14) 62%,transparent 72%);
  filter:blur(min(.5vw,.9vh))}
.sp-award .sp-art{aspect-ratio:2.098;
  filter:drop-shadow(0 min(.7vw,1.24vh) min(.4vw,.71vh) rgba(0,0,0,.35))}
.sp-award span{position:absolute;left:0;width:100%;text-align:center;translate:0 -50%;
  font-family:var(--f-lobby);font-weight:400;line-height:1;color:#1b7532;white-space:nowrap;
  text-shadow:.6cqw .6cqw 0 rgba(0,0,0,.18)}
.sp-award .aw-got{top:19.8%;font-size:7.8cqw}
.sp-award .aw-pts{top:47.3%;font-size:18.93cqw}
.sp-award .aw-lbl{top:75.6%;font-size:7.8cqw}

/* The payoff is ONE object in TWO beats. `.show` slams the paper down centre
   stage and LEAVES IT THERE (it used to fade itself out over 1.85s, which is
   why the whole thing read as a flash). `.deliver` then drags that same paper
   into the score card, and the number only moves when it arrives — so what
   you see is literally the points being carried into your total.
   `.deliver` replaces the animation outright rather than layering on top, so
   its 0% keyframe is exactly where `awshow` came to rest. */
.sp-award.show{animation:awshow .5s cubic-bezier(.16,1.4,.32,1) both}
.sp-award.show::before{animation:awburst 1.1s ease-out both}
.sp-award.show.deliver{
  animation:awdeliver var(--fly-ms,560ms) cubic-bezier(.5,-.28,.62,1) both}
.sp-award.show.deliver::before{animation:awburstout .26s ease-in both}
@keyframes awshow{0%{opacity:0;transform:translate(-50%,-50%) scale(.18) rotate(-11deg)}
  46%{opacity:1;transform:translate(-50%,-50%) scale(1.16) rotate(3deg)}
  72%{transform:translate(-50%,-50%) scale(.965) rotate(-1.2deg)}
  100%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}}
@keyframes awdeliver{
  0%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}
  /* a short wind-up, so the paper is visibly PICKED UP before it is thrown */
  16%{opacity:1;transform:translate(-50%,-46%) scale(1.075) rotate(-3deg)}
  100%{opacity:.05;
    transform:translate(calc(-50% + var(--fly-x,0px)),calc(-50% + var(--fly-y,0px)))
      scale(var(--fly-s,.24)) rotate(var(--fly-r,14deg))}}
@keyframes awburst{0%{opacity:0;scale:.25;rotate:-18deg}18%{opacity:.85;scale:1.02}
  62%{opacity:.42;scale:1.2;rotate:22deg}100%{opacity:.22;scale:1.28;rotate:26deg}}
@keyframes awburstout{from{opacity:.22}to{opacity:0}}

/* THE PENALTY FIGURE (soloRound.js deliverScorePenalty). Skipping a word used
   to be a grey toast at the bottom edge while the score in the corner silently
   dropped; nothing on screen joined the two events. This is the award paper's
   mirror and shares its whole grammar — same fixed positioning, same stage
   anchor, same --fly-* delivery vars — so a gain and a loss arrive in the same
   place and read as one meter.

   No paper behind it, deliberately: the award is a physical thing you are
   handed, a penalty is something taken off the board, and giving it its own
   sheet would read as a second prize. The weight is carried by the red glow
   instead, which is the same #e8402a every deadline in this game uses. */
.sp-penalty{position:fixed;left:26%;top:50%;z-index:95;
  transform:translate(-50%,-50%);pointer-events:none;opacity:0;
  width:min(30vw,420px);container-type:inline-size;text-align:center}
.sp-penalty::before{content:"";position:absolute;left:50%;top:50%;
  width:150%;aspect-ratio:1;translate:-50% -50%;z-index:-1;opacity:0;
  background:radial-gradient(circle,rgba(232,64,42,.62) 0 24%,
    rgba(232,64,42,.30) 46%,rgba(232,64,42,.10) 64%,transparent 74%);
  filter:blur(min(.6vw,1.06vh))}
.sp-penalty .pen-val{display:block;
  font-family:var(--f-lobby);font-weight:400;line-height:.94;
  font-size:34cqw;color:#e8402a;white-space:nowrap;
  text-shadow:0 0 .07em rgba(232,64,42,.95),0 0 .22em rgba(232,64,42,.65),
              .022em .028em 0 rgba(60,8,0,.42)}
.sp-penalty .pen-lab{display:block;margin-top:.14em;
  font-family:var(--f-sign),var(--f-body);font-weight:800;
  font-size:9.4cqw;letter-spacing:.06em;color:#e8402a;
  text-shadow:0 0 .12em rgba(232,64,42,.7)}

/* Two beats, exactly like the award: `.show` slams the figure down and LEAVES
   it there, `.deliver` then drags that same figure into the score card and the
   total only drops when it arrives. `.deliver` replaces the animation outright
   so its 0% keyframe is where `penshow` came to rest. */
.sp-penalty.show{animation:penshow .42s cubic-bezier(.16,1.4,.32,1) both}
.sp-penalty.show::before{animation:penglow 1s ease-out both}
.sp-penalty.show.deliver{
  animation:pendeliver var(--fly-ms,540ms) cubic-bezier(.5,-.24,.62,1) both}
.sp-penalty.show.deliver::before{animation:penglowout .24s ease-in both}
@keyframes penshow{
  0%{opacity:0;transform:translate(-50%,-50%) scale(1.55) rotate(5deg)}
  40%{opacity:1;transform:translate(-50%,-50%) scale(.88) rotate(-2.6deg)}
  /* the shake is the point: a figure that lands and sits still is a readout,
     one that rattles is something that just hit you */
  62%{transform:translate(calc(-50% + 1.1%),-50%) scale(1.04) rotate(1.6deg)}
  80%{transform:translate(calc(-50% - .8%),-50%) scale(.99) rotate(-.8deg)}
  100%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}}
@keyframes pendeliver{
  0%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}
  14%{opacity:1;transform:translate(-50%,-44%) scale(1.09) rotate(3deg)}
  100%{opacity:.05;
    transform:translate(calc(-50% + var(--fly-x,0px)),calc(-50% + var(--fly-y,0px)))
      scale(var(--fly-s,.24)) rotate(var(--fly-r,-14deg))}}
@keyframes penglow{0%{opacity:0;scale:.4}16%{opacity:.95;scale:1.06}
  60%{opacity:.5;scale:1.16}100%{opacity:.3;scale:1.2}}
@keyframes penglowout{from{opacity:.3}to{opacity:0}}

/* theme.css kills every animation under reduced motion, which would leave the
   figure at opacity 0 — the whole point of it is that the loss is visible, so
   show it plainly instead. deliverScorePenalty pays the score immediately on
   that path, so nothing is waiting on a flight that never happens. */
@media (prefers-reduced-motion: reduce){
  .sp-penalty.show{opacity:1;animation:none}
  .sp-penalty.show::before{opacity:.3;animation:none}
  .sp-card.bump-hit,.v1-scorecard.bump-hit{animation:none}
}

/* theme.css kills every animation under reduced motion, which would leave the
   award paper stuck at opacity:0 — the payoff, and the points it reports,
   would silently never appear. Show it plainly instead of animating it (JS
   also pays the score out immediately in that case, so nothing waits on a
   flight that is not running). The screen flash stays suppressed: that one is
   pure motion, and unwelcome. */
@media (prefers-reduced-motion: reduce){
  .sp-award.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
}

/* =================== 1v1 LOBBY (Figma frame 111:82) =================== */
body[data-screen="lobby"] .wrap{max-width:none;padding:0}
#scrLobby{position:fixed;inset:0;overflow:hidden;display:grid;place-items:end center;background:var(--fig-cream)}
#scrLobby::before{content:"";position:absolute;inset:0;z-index:0;
  background:url(../assets/ui/figma/home-bg2.webp) center/cover}
.lobbystage{--lb-lift:calc(-1 * min(.28vw,.5vh));position:relative;z-index:1;overflow:hidden;background:var(--fig-cream);font-family:var(--f-lobby);
  width:min(100dvw, calc(100dvh * (var(--pp-ar))), var(--pp-maxw));
  aspect-ratio:var(--pp-ar)}
@media (max-aspect-ratio: 1/1){ .lobbystage{width:100dvw} }
.lb{position:absolute}
.lb.bg{inset:0;width:100%;height:100%;object-fit:cover;z-index:1}
.lb img{width:100%;display:block}
.lb.deco{pointer-events:none}
.lb.deco.lobby-tape{pointer-events:auto}
.lb.passthru{pointer-events:none}
#logoLobby{cursor:pointer}
.fig-shadow-lg{filter:drop-shadow(0 min(.486vw,.864vh) min(.278vw,.494vh) rgba(0,0,0,.35))}
.fig-shadow-sm{filter:drop-shadow(0 min(.174vw,.309vh) min(.104vw,.185vh) rgba(0,0,0,.35))}
.fig-shadow-xs{filter:drop-shadow(0 min(.104vw,.185vh) min(.063vw,.111vh) rgba(0,0,0,.35))}
.fig-shadow-card{filter:drop-shadow(0 min(.236vw,.42vh) min(.139vw,.247vh) rgba(0,0,0,.35))}

/* Paper pieces lift independently, while the two camera compositions move as
   complete stacks so their backgrounds, feeds, borders and labels stay glued. */
.lobby-float,.lobby-tape{cursor:pointer;transition:translate .2s ease,scale .2s ease,rotate .2s ease,filter .2s ease}
.lobby-float:hover,.lobby-float:focus-within{translate:0 var(--lb-lift);scale:1.012;filter:brightness(1.035) drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(23,53,111,.28))}
.lobby-tape:hover{translate:0 var(--lb-lift);rotate:-3deg;scale:1.045;filter:brightness(1.04) drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(23,53,111,.25))}
.lobby-react.lobby-bop{animation:lobbyBop .42s cubic-bezier(.2,1.55,.4,1)}
@keyframes lobbyBop{0%{transform:scale(1)}45%{transform:scale(1.07) rotate(1.5deg)}100%{transform:scale(1)}}
.lobby-you-layer,.lobby-rival-layer{transition:translate .2s ease,filter .2s ease}
.lobbystage:has(#slotLobbyYou:hover) .lobby-you-layer,
.lobbystage:has(#slotLobbyRival:hover) .lobby-rival-layer,
.lobbystage:has(#lobbyRobotWait:hover) .lobby-rival-layer{translate:0 var(--lb-lift)}
.lobbystage:has(#slotLobbyYou:hover) .lobby-you-layer.lobby-panel-art,
.lobbystage:has(#slotLobbyRival:hover) .lobby-rival-layer.lobby-panel-art,
.lobbystage:has(#lobbyRobotWait:hover) .lobby-rival-layer.lobby-panel-art{filter:brightness(1.035) drop-shadow(0 min(.28vw,.5vh) min(.2vw,.36vh) rgba(23,53,111,.24))}

/* buttons share one press/hover family */
.pbtn{background:none;border:none;padding:0;cursor:pointer;transition:translate .16s ease, scale .16s ease, filter .16s ease}
.pbtn:hover:not([disabled]){translate:0 var(--lb-lift);scale:1.02;filter:brightness(1.05) drop-shadow(0 min(.42vw,.74vh) min(.56vw,.99vh) rgba(23,53,111,.25))}
.pbtn:active:not([disabled]){translate:0 1px;scale:.97;filter:brightness(.97)}

/* video sandwich: slot sits between panel art and torn-frame overlay */
/* no handler has ever been attached to these — a pointer cursor over a video
   panel promises a click that does nothing */
.vidslot{overflow:hidden;border-radius:0}
.vidslot .camdock{width:calc(100% + 2px);height:calc(100% + 2px);margin:-1px}
body[data-screen="lobby"] .camdock .livebadge,
body[data-screen="lobby"] .camdock .aipill,
body[data-screen="lobby"] .camdock .brackets{display:none}
.namechip{pointer-events:none}
.namechip span{position:absolute;inset:8% 4% 20%;display:flex;align-items:center;justify-content:center;color:#fff;font-family:var(--f-lobby);font-weight:400;font-size:clamp(16px,min(2.25vw,4vh),100px);letter-spacing:.01em;text-shadow:min(.139vw,.247vh) min(.139vw,.247vh) 0 rgba(0,0,0,.5);text-transform:uppercase;overflow:hidden;white-space:nowrap}
.oppchip{display:none}

/* ============ THE PAPER ROBOT RIG — shared by Home and the lobby ============
   Figma "Robot RIg" SECTION 353:752, driven by js/paperRig.js. Eleven
   separately-exported cut-outs; this pack is NOT on one shared canvas the way
   assets/ui/rig/ is, so each layer's rect has to be written down, and here is
   where.

   Two boxes, and the difference between them is the whole trick. The RIG ROOT
   (`.robotwait` in the lobby, `#homeRobot` on Home) is the ART RECT — Figma's
   own 1087x1309 union of the eleven layers, and the only thing a screen has to
   position. `.pr-canvas` inside it is the SQUARE that rect is centred in
   (side 1309, so 120.4232% of the rect's width, hung 10.2116% off its left
   edge). Every rect and pivot below is a fraction of that square, and
   paperRig.js works in the same units — because a rotation composed in a
   non-square normalised space is a shear, and a sheared puppet comes apart.

   Each `.pr-part` IS the whole square, carrying the transform paperRig.js
   composes for that joint (origin 0 0, so the composed translate+rotate is
   exactly the chain's product); the `<img>` inside it is that layer's Figma
   rect. Draw order is Figma's, and it is load-bearing: arms and legs sit
   BEHIND the torso so the rotating shoulder and hip caps stay hidden, and the
   hands sit in FRONT of everything so a reach reads as a reach.

   The DROP_SHADOW is per LAYER, as Figma authors it (y 12, blur 4, 35% black
   over the 1309 square = 1.104cqw / .368cqw against the root's width) — one
   shadow on the whole silhouette instead would flatten eleven pieces of cut
   paper into one sticker, which is the look this rig exists to escape. Every
   root therefore needs `container-type:inline-size`. */
.pr-canvas{position:absolute;left:-10.2116%;top:0;width:120.4232%;aspect-ratio:1}
.pr-part{position:absolute;inset:0;transform-origin:0 0;pointer-events:none}
.pr-part>img{position:absolute;display:block;pointer-events:auto;cursor:pointer;
  filter:drop-shadow(0 1.104cqw .368cqw rgba(0,0,0,.35))}
/* Figma rects, as fractions of the square. */
.pr-part[data-j="head"] >img{left:13.2162%;top:0;      width:66.3865%;height:53.0940%}
.pr-part[data-j="eyes"] >img{left:13.2162%;top:.0764%; width:65.6990%;height:52.5592%}
.pr-part[data-j="mouth"]>img{left:14.2857%;top:1.2223%;width:64.2475%;height:51.4133%}
.pr-part[data-j="torso"]>img{left:25.8213%;top:49.1215%;width:40.1069%;height:26.7379%}
.pr-part[data-j="waist"]>img{left:30.9396%;top:69.3659%;width:29.1826%;height:11.6883%}
.pr-part[data-j="uarmL"]>img{left:54.0107%;top:45.2254%;width:28.1131%;height:18.7166%}
.pr-part[data-j="handL"]>img{left:74.7899%;top:37.8916%;width:16.7303%;height:16.7303%}
.pr-part[data-j="uarmR"]>img{left:8.4798%; top:52.6356%;width:33.9190%;height:22.6127%}
.pr-part[data-j="handR"]>img{left:10.2368%;top:71.6578%;width:18.4110%;height:16.8067%}
/* Both legs are placed by Figma's own relativeTransform rather than by a
   rotation: the left one has a NEGATIVE determinant — it is mirrored, not
   turned — and re-deriving that as an angle puts the boot on the wrong foot.
   `transform-origin:0 0` is what makes the matrix agree with the rect. */
.pr-part[data-j="legL"] >img{left:47.9195%;top:69.3659%;width:23.9114%;height:28.2659%;
  transform-origin:0 0;transform:matrix(-.9944621,.1050962,.1050962,.9944621,0,0)}
.pr-part[data-j="legR"] >img{left:43.6211%;top:73.6277%;width:20.3972%;height:24.0642%;
  transform-origin:0 0;transform:matrix(.9953252,-.0965799,.0965799,.9953252,0,0)}
.pr-part[data-j="legR"] {z-index:0}
.pr-part[data-j="uarmL"]{z-index:1}
.pr-part[data-j="uarmR"]{z-index:2}
.pr-part[data-j="legL"] {z-index:3}
.pr-part[data-j="waist"]{z-index:4}
.pr-part[data-j="head"] {z-index:5}
.pr-part[data-j="torso"]{z-index:6}
.pr-part[data-j="eyes"] {z-index:7}
.pr-part[data-j="mouth"]{z-index:8}
.pr-part[data-j="handL"]{z-index:9}
.pr-part[data-j="handR"]{z-index:10}
/* He is a CHARACTER, not eleven controls, so hovering any one layer has to
   light the whole of him — hence `:has()` on the root rather than a hover on
   the part. alphaHit.js keeps this honest: a layer whose transparent pixel is
   under the pointer is `pointer-events:none`, so it cannot match `:hover`.
   The shadow is restated because `filter` replaces, never adds. */
[data-rig] .pr-part>img{transition:filter .16s ease}
[data-rig]:has(.pr-part>img:hover) .pr-part>img{
  filter:drop-shadow(0 1.104cqw .368cqw rgba(0,0,0,.35)) brightness(1.07)}
[data-rig]:has(.pr-part>img:active) .pr-part>img{
  filter:drop-shadow(0 .74cqw .3cqw rgba(0,0,0,.4)) brightness(.98)}
/* The waiting robot's own box: the art rect, centred on the opponent panel.
   Hidden once someone joins. */
.robotwait{pointer-events:none;container-type:inline-size;aspect-ratio:1087/1309}
.waitbig{color:#FBF6EC;font-family:var(--f-lobby);font-weight:400;font-size:clamp(26px,min(3.47vw,6.17vh),160px);line-height:1.36;letter-spacing:.01em;text-align:left;text-shadow:min(.347vw,.617vh) min(.347vw,.617vh) 0 rgba(0,0,0,.5);pointer-events:none}
.waitbig i,.waitline i{font-style:normal}
.lobbystage.joined .robotwait,.lobbystage.joined .waitbig{display:none}
.lobbystage.joined .oppchip{display:block}
.vsbadge{display:none;pointer-events:none;filter:drop-shadow(0 6px 10px rgba(23,53,111,.35))}
.lobbystage.joined .vsbadge{display:block;animation:vspop .55s cubic-bezier(.2,1.6,.4,1)}
@keyframes vspop{0%{transform:scale(0) rotate(-30deg)}70%{transform:scale(1.15) rotate(6deg)}100%{transform:scale(1)}}

/* name tag */
.nametag img{width:100%}
#lobbyNameTag{left:64.166667%;top:3.827160%;width:14.513889%;height:10.003418%;z-index:3}
.lobbystage[data-mode="party"] #lobbyNameTag{left:63.819444%;top:3.950617%}
.lobby-exit{left:81.736111%;top:2.345679%;width:22.564833%;height:13.285260%;z-index:3}
.lobbystage[data-mode="party"] .lobby-exit{left:80.763889%;top:2.469136%}
.lobby-exit img{width:100%;height:100%;display:block;object-fit:fill}
.lobby-exit::before{content:"";position:absolute;left:50%;top:50%;
  width:max(100%,44px);height:max(100%,44px);translate:-50% -50%}
.lobby-exit>*{pointer-events:none}
@media (prefers-reduced-motion:reduce){
  .lobby-exit,.lobby-exit:hover,.lobby-exit:active{
    translate:0 0!important;scale:1!important;rotate:0deg!important;
    animation:none!important;transition:none!important}
}
.nametag .nt-label{position:absolute;left:7.2%;top:15.3%;color:#002a8d;font-family:var(--f-lobby);font-weight:400;font-size:clamp(11px,min(1.12vw,1.99vh),52px);line-height:.91;letter-spacing:.01em}
.nametag input{position:absolute;left:7.2%;top:48.5%;width:86.5%;background:none;border:none;border-bottom:min(.139vw,.247vh) solid rgba(0,42,141,.35);color:#002a8d;font-family:var(--f-lobby);font-weight:400;font-size:clamp(16px,min(1.79vw,3.18vh),84px);line-height:.57;letter-spacing:.01em;text-align:center;text-transform:uppercase;padding:0 20% min(.417vw,.741vh) 0}
.nametag input::placeholder{color:#90867d;font-size:.68em;text-transform:none;letter-spacing:.02em}
.nametag input:focus{outline:none;border-bottom-color:#002a8d;background:rgba(255,255,255,.35)}
.nametag .nt-pencil{position:absolute;left:74.5%;top:28%;width:19%;pointer-events:none}

/* invite friend card */
.invite .cardart{width:100%}
.invite .inv-robot{position:absolute;left:1%;top:-9%;width:17%;pointer-events:none}
.invite .inv-title{position:absolute;left:19%;top:7%;color:#fff;font-family:var(--f-lobby);font-weight:400;font-size:clamp(16px,min(1.92vw,3.41vh),90px);letter-spacing:.01em;text-shadow:min(.16vw,.284vh) min(.16vw,.284vh) 0 rgba(0,0,0,.5)}
.invite .inv-label{position:absolute;left:4%;color:#973f07;font-family:var(--f-lobby);font-weight:400;font-size:clamp(12px,min(1.28vw,2.28vh),60px);letter-spacing:.01em;text-shadow:0 min(.069vw,.123vh) min(.035vw,.062vh) rgba(0,0,0,.25)}
.invite .inv-box{position:absolute;left:38.5%;width:36%}
.invite .inv-box,.invite .inv-btn img{filter:drop-shadow(0 min(.222vw,.395vh) min(.128vw,.228vh) rgba(0,0,0,.35))}
.invite .inv-code{position:absolute;left:38.5%;top:34.5%;width:36%;text-align:center;color:#124c9d;font-family:var(--f-lobby);font-weight:400;font-size:clamp(16px,min(1.92vw,3.41vh),90px);letter-spacing:.01em}
.invite .inv-code{cursor:pointer;white-space:nowrap;
  transition:scale .16s ease,color .16s ease}
.invite .inv-code:hover{scale:1.035;color:#002a8d}
.invite .inv-code:active{scale:.97;color:#0b3fa0}
.invite input{position:absolute;left:40%;top:69%;width:33%;background:none;border:none;text-align:center;color:#124c9d;font-family:var(--f-lobby);font-weight:400;font-size:clamp(14px,min(1.6vw,2.84vh),72px);letter-spacing:.01em;text-transform:uppercase}
.invite input::placeholder{color:#90867d}
.invite input:focus{outline:none;background:rgba(255,255,255,.5);border-radius:min(.417vw,.741vh)}
.invite .inv-btn{position:absolute;left:76.5%;width:21%}
.invite .inv-btn b{position:absolute;inset:6% 0 18%;display:flex;align-items:center;justify-content:center;color:#fff;font-family:var(--f-lobby);font-weight:400;font-size:clamp(12px,min(1.28vw,2.28vh),60px);letter-spacing:.01em;text-shadow:min(.16vw,.284vh) min(.16vw,.284vh) 0 rgba(0,0,0,.5)}

/* match settings card */
.settings .cardart{width:100%}
.settings .set-title{position:absolute;left:0;top:7%;width:100%;text-align:center;color:#fff;font-family:var(--f-lobby);font-weight:400;font-size:clamp(20px,min(2.69vw,4.78vh),120px);line-height:.85;letter-spacing:.01em;text-shadow:min(.167vw,.296vh) min(.167vw,.296vh) 0 rgba(0,0,0,.5)}
.settings .set-divider{position:absolute;left:50%;top:40%;width:min(.139vw,.247vh);height:53%;background:rgba(12,65,139,.35);border-radius:min(.139vw,.247vh)}
.settings .set-label{position:absolute;top:40%;width:23.8%;text-align:center;color:#0c418b;font-family:var(--f-lobby);font-weight:400;font-size:clamp(14px,min(1.68vw,2.99vh),78px);letter-spacing:.01em;text-shadow:0 min(.069vw,.123vh) min(.035vw,.062vh) rgba(0,0,0,.25)}
.settings .set-btn{position:absolute;top:64%;width:7%}
.settings .set-badge{position:absolute;top:64%;width:18.5%;pointer-events:none}
.settings .set-btn img,.settings .set-badge img{filter:drop-shadow(min(.069vw,.123vh) min(.208vw,.37vh) min(.139vw,.247vh) rgba(0,0,0,.4))}
.settings .set-badge img{width:100%}
.settings .set-badge b{position:absolute;inset:6% 0 16%;display:flex;align-items:center;justify-content:center;color:#0c418b;font-family:var(--f-lobby);font-weight:400;font-size:clamp(16px,min(2.02vw,3.59vh),94px);letter-spacing:.01em;text-shadow:0 min(.069vw,.123vh) min(.035vw,.062vh) rgba(0,0,0,.25)}
.settings .set-btn[disabled]{opacity:.45;cursor:default}

/* start + status */
.startbtn[disabled]{opacity:.72;cursor:default;filter:drop-shadow(0 min(.264vw,.469vh) min(.153vw,.272vh) rgba(0,0,0,.35)) saturate(.72) brightness(.94)}
.startbtn.ready{animation:startpop .5s cubic-bezier(.2,1.6,.4,1)}
@keyframes startpop{0%{transform:scale(.94)}60%{transform:scale(1.07)}100%{transform:scale(1)}}
.waitline{color:#124c9d;font-family:var(--f-sign);font-weight:700;font-size:clamp(14px,min(1.67vw,2.97vh),78px);line-height:.75;letter-spacing:.04em;text-align:center;text-shadow:min(.069vw,.123vh) min(.069vw,.123vh) min(.069vw,.123vh) rgba(0,0,0,.25)}
.waitline.compact{font-size:clamp(11px,min(1vw,1.78vh),48px);line-height:1.02;
  letter-spacing:.02em;overflow-wrap:anywhere;top:auto!important;bottom:1.5%}

/* ============ LOBBY ENTRANCE — the room is dealt the way Home leaves ======
   Pressing 1V1 or PARTY pops that card off Home's board (`.home-leaving`),
   and the lobby it opens re-lands the SAME kind of component — its 1V1 /
   PARTY LOBBY title paper — with the mirror of that pop, then slaps its tape
   down after it, exactly as Home's own three cards do. `homeCardPickIn` runs
   `homeCardPick` backwards (in from big and over-close, undershooting past
   rest before settling) and `homeTapeSlap` is Home's tape beat, reused
   unchanged: it is the same gesture, so it is the same keyframes.

   Everything else is thrown onto the desk the same way the mode cards are on
   Home: `lobbyPaperIn` is `homeCardEnter`'s shape, and like it, each piece
   carries its OWN path in `--lb-in-x/y/r` — thrown in from the edge it lives
   against, so the room assembles outwards instead of every paper rising off
   the same spot. A shared path would read as one fade with an offset.

   ONE group cannot travel: the camera compositions (`.lobby-you-layer`,
   `.lobby-rival-layer`, `.party-team-layer`). Each is a stack of separately
   positioned layers — panel, feed, torn frame, name chip — held together only
   by none of them having a transform of its own, and a percentage translate
   applied to boxes of different heights moves them by different numbers of
   pixels, so the stack comes apart in flight. `lobbyStackIn` drops them on a
   single `--lb-in-drop` in VIEWPORT units, identical for every layer, so the
   composition still lands with weight and still lands as one object. Same
   reason their hover lift is a shared `--lb-lift` and never a percentage.
   The pieces that SIT on a composition rather than belong to it — the waiting
   robot, his WAITING FOR FRIEND line, the two JOIN THIS TEAM buttons — are
   pulled back out by id and given their own beat once their panel has landed.

   Fill is `backwards`, NOT `both` — this is the one place it matters. The
   count-in can afford `both` because nothing is clickable during a 3·2·1,
   but the lobby is live the instant it is on screen and a filled-forwards
   entrance out-ranks every plain declaration under it: the hover lift, the
   press, `[disabled]` — all dead until the class comes off, and dead for the
   pieces that finished a second ago too. `backwards` still holds each piece
   at its 0% state through its own delay (which is the whole point of the
   stagger) and hands it back the moment its beat lands, because every one of
   these ends at the element's natural resting state. `.lobby-entering` is
   still transient (lobbyAlive.js clears it on a timer and on the way out) so
   nothing is left mid-flight if the room is left during the entrance. */
.lobbystage{--lb-in-drop:min(3.1vw,5.5vh)}
.lobbystage.lobby-entering .lobbytitle{
  animation:homeCardPickIn .62s cubic-bezier(.16,1.38,.32,1) .06s backwards}
.lobbystage.lobby-entering .lobbytitle-tape{
  animation:homeTapeSlap .6s cubic-bezier(.16,1.5,.32,1) .5s backwards}
.lobbystage.lobby-entering
  :is(.lobby-you-layer,.lobby-rival-layer,.party-team-layer){
  animation:lobbyStackIn .58s cubic-bezier(.16,1.38,.32,1) backwards}
.lobbystage.lobby-entering :is(.lobby-rival-layer,.party-orange-layer){
  animation-delay:.18s}
/* Class selectors only, deliberately: `:is()` takes the specificity of its
   most specific argument, so a single id in this list would rate the whole
   group rule at (1,3,0) — above every per-piece rule under it. The shorthand
   resets delay to 0s and the vars to their fallbacks, so the beats would
   silently collapse into one and every paper would arrive on the same path,
   with nothing in the CSS looking wrong. `.iv2` is the lobby logo;
   `.nametag` is the name tag. */
.lobbystage.lobby-entering :is(.iv2,.nametag,.lobby-exit,
  .invite,.settings,.startbtn,.waitline){
  animation:lobbyPaperIn .66s cubic-bezier(.16,1.38,.32,1) backwards}
.lobbystage.lobby-entering #logoLobby{
  --lb-in-x:-44%;--lb-in-y:-34%;--lb-in-r:-9deg;animation-delay:.30s}
.lobbystage.lobby-entering #lobbyNameTag{
  --lb-in-x:14%;--lb-in-y:-70%;--lb-in-r:7deg;animation-delay:.38s}
.lobbystage.lobby-entering .lobby-exit{
  --lb-in-x:50%;--lb-in-y:-38%;--lb-in-r:10deg;animation-delay:.44s}
.lobbystage.lobby-entering .invite{
  --lb-in-x:-36%;--lb-in-y:50%;--lb-in-r:-8deg;animation-delay:.50s}
.lobbystage.lobby-entering .settings{
  --lb-in-x:0%;--lb-in-y:62%;--lb-in-r:5deg;animation-delay:.58s}
.lobbystage.lobby-entering .startbtn{
  --lb-in-x:42%;--lb-in-y:46%;--lb-in-r:9deg;animation-delay:.66s}
.lobbystage.lobby-entering .waitline{
  --lb-in-x:0%;--lb-in-y:46%;--lb-in-r:0deg;animation-delay:.74s}
/* the pieces that stand ON a composition, once it has landed under them */
.lobbystage.lobby-entering :is(#lobbyRobotWait,#lobbyWaitBig,
  #btnJoinBlue,#btnJoinOrange){
  animation:lobbyPaperIn .6s cubic-bezier(.16,1.38,.32,1) backwards}
/* The rig's box is the robot alone — less than half the width of the sprite
   that used to hold a sign beside him — so his path in is written as a bigger
   fraction of a smaller box to keep the DISTANCE he travels the same. A paper
   thrown from the same corner has to arrive from the same distance away, or
   the beat reads as a shorter throw than its neighbours. */
.lobbystage.lobby-entering #lobbyRobotWait{
  --lb-in-x:52%;--lb-in-y:19%;--lb-in-r:8deg;animation-delay:.56s}
.lobbystage.lobby-entering #lobbyWaitBig{
  --lb-in-x:0%;--lb-in-y:34%;--lb-in-r:-3deg;animation-delay:.66s}
.lobbystage.lobby-entering #btnJoinBlue{
  --lb-in-x:-18%;--lb-in-y:38%;--lb-in-r:-7deg;animation-delay:.50s}
.lobbystage.lobby-entering #btnJoinOrange{
  --lb-in-x:18%;--lb-in-y:38%;--lb-in-r:7deg;animation-delay:.60s}
/* START waits for a second player, so it must not FLY IN looking available:
   an entrance interpolating towards opacity 1 out-ranks
   `.startbtn[disabled]{opacity:.72}` for as long as it runs. It travels to
   its dimmed value instead — and reverts to whichever the CSS says once the
   beat lands, so a rival arriving mid-entrance still lights it up. */
.lobbystage.lobby-entering .startbtn[disabled]{animation-name:lobbyPaperInDim}
.lobbystage.lobby-entering .logo-tape,
.lobbystage.lobby-entering .party-vs{
  animation:homeTapeSlap .56s cubic-bezier(.16,1.5,.32,1) backwards}
.lobbystage.lobby-entering #lobbyLogoTapeA{animation-delay:.70s}
.lobbystage.lobby-entering #lobbyLogoTapeB{animation-delay:.78s}
.lobbystage.lobby-entering .party-vs{animation-delay:.86s}
@keyframes homeCardPickIn{
  0%  {opacity:0;translate:0 -7%;rotate:2.4deg;scale:1.42}
  56% {opacity:1;translate:0 1.6%;rotate:-1.8deg;scale:.955}
  78% {translate:0 -.6%;rotate:.8deg;scale:1.025}
  100%{opacity:1;translate:0 0;rotate:0deg;scale:1}}
/* homeCardEnter's shape, with the path left to the caller. The fallbacks are
   a plain drop so a piece that forgets its vars still arrives sensibly. */
@keyframes lobbyPaperIn{
  0%  {opacity:0;translate:var(--lb-in-x,0%) var(--lb-in-y,-40%);
       rotate:var(--lb-in-r,-6deg);scale:.8}
  64% {opacity:1;translate:0 1.5%;rotate:-1.4deg;scale:1.04}
  84% {translate:0 -.7%;rotate:.7deg;scale:.99}
  100%{opacity:1;translate:0 0;rotate:0deg;scale:1}}
@keyframes lobbyPaperInDim{
  0%  {opacity:0;translate:var(--lb-in-x,0%) var(--lb-in-y,-40%);
       rotate:var(--lb-in-r,-6deg);scale:.8}
  64% {opacity:.72;translate:0 1.5%;rotate:-1.4deg;scale:1.04}
  84% {opacity:.72;translate:0 -.7%;rotate:.7deg;scale:.99}
  100%{opacity:.72;translate:0 0;rotate:0deg;scale:1}}
@keyframes lobbyStackIn{
  0%  {opacity:0;translate:0 var(--lb-in-drop)}
  64% {opacity:1;translate:0 calc(var(--lb-in-drop) * -.13)}
  100%{opacity:1;translate:0 0}}

/* ---------- leaving: the mode paper goes back out the way it came in -------
   The exit button is the mirror of pressing 1V1 / PARTY on Home, so it is
   Home's own exit: `homeCardPick` takes the mode paper off the board towards
   the player, `homeTapePeel` releases its tape a beat ahead of it, and every
   other paper drops away underneath. The camera compositions get their own
   fall for the same reason they get their own arrival — a percentage
   translate would pull their layers apart on the way out too.
   `both` here, unlike the entrance: these end at opacity 0 and MUST hold
   there until the screen is actually swapped. lobbyAlive.js clears the
   class. */
.lobbystage.lobby-leaving{pointer-events:none}
.lobbystage.lobby-leaving .lobbytitle{
  animation:homeCardPick .44s cubic-bezier(.36,0,.24,1) .07s both}
.lobbystage.lobby-leaving .lobbytitle-tape{
  animation:homeTapePeel .3s cubic-bezier(.3,0,.4,1) both}
.lobbystage.lobby-leaving :is(.iv2,.nametag,.lobby-exit,.invite,.settings,
  .startbtn,.waitline,.logo-tape,.party-vs){
  animation:homePieceOut .3s ease-in both}
/* The robot, his waiting line and the two JOIN buttons are given their own
   arrival because they STAND on a composition — but they leave with it. A
   composition breaking up into its pieces on the way out is the one reading
   the whole stack treatment exists to prevent. */
.lobbystage.lobby-leaving
  :is(.lobby-you-layer,.lobby-rival-layer,.party-team-layer){
  animation:lobbyStackOut .32s ease-in both}
@keyframes lobbyStackOut{
  0%  {opacity:1;translate:0 0}
  100%{opacity:0;translate:0 calc(var(--lb-in-drop) * .42)}}

/* ---------- lobby mode gating (one stage, two centers) ---------- */
.lobbystage[data-mode="party"] .m1{display:none!important}
.lobbystage[data-mode="1v1"] .mp{display:none!important}

/* ---------- party team panels ---------- */
.pt-teamname{color:#FBF6EC;font-family:var(--f-lobby);font-weight:400;text-align:center;
  font-size:clamp(20px,min(2.73vw,4.85vh),126px);letter-spacing:.01em;pointer-events:none;
  text-shadow:min(.28vw,.5vh) min(.28vw,.5vh) 0 rgba(0,0,0,.45)}
.party-team-layer{transition:translate .22s cubic-bezier(.2,1.2,.35,1),filter .2s ease}
.lobbystage:has(.party-blue-hit:hover) .party-blue-layer,
.lobbystage:has(.party-blue-hit:focus-visible) .party-blue-layer,
.lobbystage:has(.party-orange-hit:hover) .party-orange-layer,
.lobbystage:has(.party-orange-hit:focus-visible) .party-orange-layer{translate:0 calc(var(--lb-lift) * 1.45)}
.lobbystage:has(.party-blue-hit:hover) .party-blue-layer.party-panel-art,
.lobbystage:has(.party-orange-hit:hover) .party-orange-layer.party-panel-art{
  filter:brightness(1.035) drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(23,53,111,.27))}
/* Every cell is the same size now, so there is no `.wide` variant and one
   rule serves all three. The tile is its window in `pt-frame-2x2.webp` plus
   ~10 component px of bleed on each side; the icon and label percentages
   below are Figma's own positions measured against THAT box, taken from the
   BOTTOM row — Figma's top-row cells are authored ~10px above their own
   windows, and the frame art is the thing the player actually sees. */
.pt-tile{overflow:hidden;background:#eee5d8 url("../assets/ui/figma/pt-inner.webp") center/cover no-repeat;
  box-shadow:0 0 0 min(.35vw,.62vh) #eee5d8}
.pt-tile video,.pt-tile .camdock{width:100%;height:100%;object-fit:cover}
.pt-tile video{display:block;transform:scaleX(-1)}
.pt-empty{position:absolute;inset:0}
.pt-empty img{position:absolute;left:50%;top:17.65%;width:19.85%;height:41.6%;min-width:26px;
  object-fit:contain;translate:-50% 0;opacity:.9}
.pt-empty span{position:absolute;left:0;top:73.6%;width:100%;text-align:center;color:#0c418b;
  font-family:var(--f-sign);font-weight:800;font-size:clamp(12px,min(1.706vw,3.034vh),78px);
  line-height:.45;letter-spacing:.04em;white-space:nowrap;opacity:.9}
.pt-name{position:absolute;left:3.2%;top:4.5%;width:min(44%,11.5vw);aspect-ratio:1200/338;
  display:block;filter:drop-shadow(0 min(.16vw,.28vh) min(.1vw,.18vh) rgba(0,0,0,.28));
  transition:translate .18s ease,scale .18s ease,filter .18s ease;pointer-events:none}
.pt-name img{position:absolute;inset:0;width:100%;height:100%}
.pt-name b{position:absolute;inset:0;display:grid;place-items:center;padding:0 6%;
  color:#fff;font-family:var(--f-lobby);font-weight:400;
  font-size:clamp(12px,min(1.52vw,2.7vh),70px);line-height:1;letter-spacing:.02em;text-align:center;
  text-transform:uppercase;white-space:nowrap;overflow:hidden;
  text-shadow:min(.1vw,.18vh) min(.1vw,.18vh) 0 rgba(0,0,0,.5)}
.pt-tile:hover .pt-name{translate:0 -7%;scale:1.025;filter:brightness(1.04) drop-shadow(0 min(.23vw,.41vh) min(.15vw,.27vh) rgba(0,0,0,.32))}

/* the JOIN card is unmistakably a button: lift, wobble hint, press.
   It owns the 2x2's bottom-right cell outright, so it carries the height of
   its authored Figma rectangle and fills it — the paper is stretched there
   the same way every other re-used paper on these boards is. */
.pt-join{cursor:pointer}
.pt-join .pt-join-art{width:100%;height:100%;display:block;object-fit:fill}
/* The JOIN cell is a WAITING cell wearing a coloured paper, so its icon and
   its label sit on the SAME two baselines — 17.65% and 73.6% of the cell —
   that `.pt-empty` uses. Figma authors all four on one line and the grid only
   reads as a grid while they stay on it. Both are centred rather than carrying
   their authored left offsets: those were measured against a join rectangle
   drawn a few px off its own window, and centring is what the design means. */
.pt-join .pt-join-icon{position:absolute;left:50%;top:17.65%;width:43.14%;
  translate:-50% 0;pointer-events:none;transition:transform .18s ease}
.pt-join span{position:absolute;left:0;top:73.6%;width:100%;height:7%;
  display:block;text-align:center;color:#fff;white-space:nowrap;
  font-family:var(--f-sign);font-weight:800;font-size:clamp(12px,min(1.706vw,3.034vh),78px);
  line-height:.45;letter-spacing:.04em;
  text-shadow:min(.128vw,.228vh) min(.128vw,.228vh) 0 rgba(0,0,0,.5);pointer-events:none}
.pt-join:hover .pt-join-icon{transform:translateY(-6%) scale(1.06)}
.pt-join:not(:hover) .pt-join-icon{animation:joinNudge 3.4s ease-in-out infinite}
@keyframes joinNudge{0%,84%,100%{transform:translateY(0)}90%{transform:translateY(-4%)}96%{transform:translateY(1%)}}
/* A FULL TEAM IS A STATE, NOT AN ABSENCE. The paper used to `display:none`
   itself, which was right when it sat ON the third tile and stepping aside
   revealed that player's video. In the 2x2 it owns a cell of its own, so
   hiding it punches a hole in the grid. It stays where it is and says TEAM
   IS FULL instead, with the pointer, the nudge and the lift all withdrawn so
   nothing left on it still reads as pressable. */
.pt-join[disabled]{cursor:default}
.pt-join[disabled] .pt-join-art{filter:saturate(.4) brightness(.95)}
.pt-join[disabled] .pt-join-icon{opacity:.5;animation:none}
.pt-join[disabled]:hover .pt-join-icon{transform:none}

/* Teams and VS still feel tactile, but party-lobby hover is motion-only. */
.party-vs{pointer-events:auto;cursor:pointer;
  transition:translate .2s ease,scale .2s cubic-bezier(.2,1.35,.35,1),rotate .2s ease,filter .2s ease}
.party-vs:hover{translate:0 calc(var(--lb-lift) * 1.8);scale:1.045;rotate:-2deg;
  filter:brightness(1.045) drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(23,53,111,.28))}

/* =================== 1v1 MATCH =================== */
body[data-screen="match"] .wrap{max-width:none;padding:0}
#scrMatch{position:fixed;inset:0;overflow:hidden;display:grid;place-items:center;background:#0e1b4a}
.matchstage{position:relative;z-index:1;overflow:hidden;background:#0e1b4a;
  width:min(100dvw,calc(100dvh * var(--pp-ar)),var(--pp-maxw));aspect-ratio:var(--pp-ar);
  font-family:var(--f-lobby)}
@media (max-aspect-ratio:1/1){.matchstage{width:100dvw}}
.v1-video-side{position:absolute;top:0;bottom:0;width:50%;overflow:hidden;background:#13245e;z-index:0}
.v1-video-side.you{left:0}.v1-video-side.rival{right:0}
.v1-video-side .slot,.v1-video-side .camdock,
.v1-video-side video,.v1-video-side canvas{position:absolute;inset:0;width:100%;height:100%}
.v1-video-side video,.v1-video-side canvas{object-fit:cover}
.v1-divider{position:absolute;left:48.333%;top:-20%;width:1.806%;height:145%;
  z-index:3;pointer-events:none;object-fit:fill;
  filter:drop-shadow(0 min(.2vw,.36vh) min(.18vw,.32vh) rgba(0,0,0,.25))}
.v1-tape{position:absolute;z-index:7;pointer-events:none}
.v1-tape-history{transition:opacity .22s ease}
.matchstage:has(#myGuessPast.empty) #myGuessPastTape,
.matchstage:has(#rivalGuessPast.empty) #rivalGuessPastTape{opacity:0}

/* Scores are the exact exported blue/orange paper. Figma centres the name
   in its top 40% and the large total in the lower half. */
.v1-scorecard{position:absolute;z-index:5;container-type:inline-size}
.v1-scorecard>img{display:block;width:100%;
  filter:drop-shadow(0 min(.292vw,.519vh) min(.167vw,.296vh) rgba(0,0,0,.35))}
.v1-scorecard>span{position:absolute;left:0;width:100%;display:flex;justify-content:center;
  text-align:center;white-space:nowrap;overflow:hidden;color:#fff;
  text-shadow:1.2cqw 1.2cqw 0 rgba(0,0,0,.5)}
.v1-player-name{top:0;height:40.5%;align-items:center;padding:0 5%;font-family:var(--f-sign);
  font-weight:800;font-size:17.15cqw;line-height:1}
.v1-player-score{top:39%;height:47%;align-items:center;font-family:'Fredoka One',var(--f-display);
  font-size:31.7cqw;line-height:1}
.v1-scorecard.bump{animation:spbump .4s cubic-bezier(.2,1.6,.4,1)}

/* Their outer bounds, text baselines and percentage positions are copied from
   Figma. The box keeps the taller Figma aspect the robot bust used to sit in,
   even though the robot is now his own element beside the stack — every text
   offset below is a percentage of THAT box, so shrinking it would move the
   label and the percentage off their paper. */
.v1-guess{z-index:4;container-type:inline-size}
.v1-guess.current{aspect-ratio:269.181/229.551}
.v1-guess.current .gcard{position:absolute;left:0;top:38.34%;width:100%}
.v1-guess.current .glabel{top:44.62%;font-size:17.21cqw}
.v1-guess.current .glabel.fit-wrap{top:40.5%;height:27%;padding:0 4%}
.v1-guess.current .gpct{top:68.61%;font-size:22.94cqw}
.v1-guess.past{opacity:.56}
.v1-guess.past .glabel,.v1-guess.past .gpct{color:#0d6ebb}
.v1-guess.rival.current .glabel,.v1-guess.rival.current .gpct{color:#fd6030}
.v1-guess.rival.past .glabel,.v1-guess.rival.past .gpct{color:#a90307}
.v1-guess.empty{opacity:0}
.v1-guess.current.pop{animation:gpop .42s cubic-bezier(.2,1.6,.4,1)}
.matchstage.you-locked .v1-guess.you.current,
.matchstage.rival-locked .v1-guess.rival.current{filter:brightness(1.09) saturate(1.1);
  animation:guessRecognized .72s cubic-bezier(.2,1.6,.4,1)}

.v1-word{z-index:5}
.v1-word .sp-word-text{font-family:'Fredoka One',var(--f-display);font-weight:400}
.v1-word.wordswap{animation:wordStamp .5s cubic-bezier(.18,1.5,.32,1)}
.matchstage[data-phase="chase"] .v1-word{animation:v1chaseword .24s ease-in-out infinite}
.matchstage.chase-urgent .v1-word{animation-duration:.16s}
.matchstage.chase-final .v1-word{animation-duration:.11s;filter:saturate(1.25) brightness(1.06)}
@keyframes v1chaseword{
  0%,100%{translate:0 0;rotate:-.35deg;scale:1}
  33%{translate:-.42% -.8%;rotate:.65deg;scale:1.014}
  66%{translate:.42% .25%;rotate:-.7deg;scale:1.008}
}
.matchstage[data-phase="sudden"] .v1-word{filter:saturate(1.25);
  animation:v1sudden .7s ease-in-out infinite alternate}
@keyframes v1sudden{to{scale:1.035;filter:saturate(1.35) brightness(1.06)}}

/* The chase reuses the existing word-timer paper and Solo clock art. Its
   actual remaining second is repeated in the paper label, while the clock
   and word physically get more urgent as the five-second window closes. */
.v1-wordtimer{transform-origin:50% 0;transition:scale .18s ease,filter .18s ease}
.v1-chase-clock{position:absolute;left:7.5%;top:36%;width:20%;z-index:2;opacity:0;
  pointer-events:none;filter:drop-shadow(1.1cqw 1.1cqw 0 rgba(0,0,0,.42))}
.matchstage[data-phase="chase"] .v1-wordtimer{z-index:10;scale:1.16;
  filter:drop-shadow(0 min(.55vw,.98vh) min(.35vw,.62vh) rgba(14,27,74,.42))}
.matchstage[data-phase="chase"] .v1-wordtimer .sp-art{
  animation:v1chasepaper .42s ease-in-out infinite}
.matchstage[data-phase="chase"] .v1-wordtimer .sp-lab{top:-1.5%;font-size:9.7cqw}
.matchstage[data-phase="chase"] .v1-wordtimer .sp-val{left:20%;width:76%;font-size:23.5cqw}
.matchstage[data-phase="chase"] .v1-chase-clock{opacity:1;
  animation:v1clockshake .22s ease-in-out infinite}
.matchstage.chase-urgent .v1-wordtimer{scale:1.24}
.matchstage.chase-final .v1-wordtimer{scale:1.32;filter:brightness(1.08)
  drop-shadow(0 min(.75vw,1.33vh) min(.48vw,.85vh) rgba(232,64,42,.48))}
.v1-wordtimer.tick-pop{animation:v1tickpop .32s cubic-bezier(.18,1.65,.32,1)}
@keyframes v1tickpop{0%{scale:1.5;rotate:-2deg}58%{rotate:1deg}100%{rotate:0}}
@keyframes v1chasepaper{
  0%,100%{translate:0 0;rotate:-.35deg}
  50%{translate:0 -1.7%;rotate:.45deg}
}
@keyframes v1clockshake{
  0%,100%{rotate:-8deg;scale:1}
  50%{rotate:8deg;scale:1.08}
}

/* The local crown follows the already-local nose landmark; the remote crown
   uses the centred camera framing as a privacy-safe fallback. No landmarks
   are transmitted across the network. */
.v1-crown{position:absolute;left:50%;top:19%;width:14%;height:auto;z-index:6;
  opacity:0;pointer-events:none;translate:-50% -50%;rotate:-11deg;scale:.45;
  filter:drop-shadow(0 min(.35vw,.62vh) min(.25vw,.44vh) rgba(0,0,0,.4));
  transition:left .055s linear,top .055s linear,opacity .18s ease,
    width .055s linear,scale .28s cubic-bezier(.2,1.65,.4,1)}
.matchstage.you-leading .v1-video-side.you .v1-crown,
.matchstage.rival-leading .v1-video-side.rival .v1-crown{opacity:1;scale:1;
  animation:v1crownarrive .3s cubic-bezier(.18,1.55,.32,1) both}
.v1-video-side.rival .v1-crown{rotate:10deg}
@keyframes v1crownarrive{
  0%{translate:-50% -40%;scale:.58}
  68%{translate:-50% -54%;scale:1.08}
  100%{translate:-50% -50%;scale:1}
}

.vsplaceholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:linear-gradient(160deg,#20348B,#111F5E);color:#C9D4F2;font-weight:900;font-size:17px;letter-spacing:1px;text-transform:uppercase;text-align:center;padding:20px}
.vsplaceholder svg{width:110px;height:110px}
.vsplaceholder small{font-size:12.5px;letter-spacing:.5px;text-transform:none;color:#8A97C8}
#videoRemote{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

/* =================== PARTY LOBBY =================== */
#scrParty .inforow{display:grid;grid-template-columns:200px 350px 470px 190px 1fr;gap:16px;align-items:stretch;margin-bottom:18px}
#scrParty .inforow .panel{display:flex;align-items:center;gap:14px;padding:12px 18px}
.partybanner h4{font-family:var(--f-display);font-weight:800;font-size:22px;letter-spacing:1px;color:var(--blue-deep);display:flex;align-items:center;gap:8px}
.partybanner small{font-size:13px;font-weight:800;color:var(--ink-soft)}
.partyname{margin-left:auto;min-width:132px}
.partyname label{display:block;font-size:9.5px;font-weight:900;letter-spacing:1px;text-transform:uppercase;color:var(--ink-soft)}
.partyname input{width:132px;margin-top:3px;border:2px solid var(--line-soft);border-radius:var(--r-sm);background:#fff;color:var(--ink);font-weight:900;padding:7px 9px}
.roomcodebig{text-align:center}
.roomcodebig .lab{font-size:12px;font-weight:900;letter-spacing:1.5px;text-transform:uppercase;color:var(--ink-soft)}
.roomcodebig .code{font-family:var(--f-display);font-weight:800;font-size:48px;letter-spacing:7px;line-height:1}
.playercount{display:flex;align-items:center;gap:10px}
.playercount b{font-family:var(--f-display);font-size:26px}
.playercount small{font-size:12px;font-weight:900;color:var(--ink-soft);letter-spacing:1px}

#scrParty .teams{display:grid;grid-template-columns:1fr auto 1fr;gap:14px;align-items:stretch}
.teampanel{border:3px solid;border-radius:var(--r-lg);box-shadow:var(--sh-big);overflow:hidden;background:var(--card)}
.teampanel.blue{border-color:var(--blue)}
.teampanel.orange{border-color:var(--orange)}
.teamhead{display:flex;align-items:center;gap:12px;padding:12px 18px;color:#fff}
.teampanel.blue .teamhead{background:var(--blue)}
.teampanel.orange .teamhead{background:var(--orange)}
.teamhead h4{font-family:var(--f-display);font-weight:800;font-size:22px;letter-spacing:1px;text-transform:uppercase}
.teamhead .cnt{background:rgba(255,255,255,.22);border-radius:var(--r-pill);padding:3px 12px;font-weight:900;font-size:13px}
.teamhead .btn{margin-left:auto;border-width:2px;padding:7px 16px;font-size:13px}
.teamgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;padding:20px 16px}
.ptile{position:relative;aspect-ratio:16/9;border-radius:var(--r-md);overflow:hidden;border:2.5px solid var(--line-soft);background:var(--panel)}
.ptile .pname{position:absolute;left:0;right:0;bottom:0;background:var(--ink);color:#fff;font-weight:900;font-size:12.5px;padding:5px 10px;display:flex;align-items:center;gap:6px}
.ptile .pname .mini{margin-left:auto}
.ptile .hostchip{position:absolute;top:8px;left:8px;z-index:3;background:var(--ink);color:#fff;border-radius:var(--r-xs);padding:3px 9px;font-size:10.5px;font-weight:900;letter-spacing:.8px}
.ptile .livechip{position:absolute;top:8px;right:8px;z-index:3;background:var(--live);color:#fff;border-radius:var(--r-xs);padding:3px 9px;font-size:10px;font-weight:900;letter-spacing:.8px}
.ptile.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--ink-soft);font-size:12px;font-weight:800;border-style:dashed}
.ptile.join{border:2.5px dashed;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;text-align:center;font-weight:900;font-size:13px;cursor:pointer}
.teampanel.blue .ptile.join{border-color:var(--blue);background:var(--blue);color:#fff}
.teampanel.orange .ptile.join{border-color:var(--orange);background:var(--orange);color:#fff}
.ptile.join svg{width:34px;height:34px}
.ptile.locked{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:#98A5CF;font-size:11.5px;font-weight:800;border-style:dashed;background:var(--tint)}
.ptile.locked svg{width:26px;height:26px}
.ptile .botfill{width:70%;height:70%;margin:6% auto 0}
.ptile .botasset{display:block;width:78%;height:84%;margin:0 auto;object-fit:contain;filter:drop-shadow(0 3px 0 rgba(18,52,119,.12))}
.vsburst{align-self:center;width:86px;height:86px;flex:none}

#scrParty .botrow{display:grid;grid-template-columns:1fr auto 1fr;gap:16px;align-items:stretch;margin-top:18px}
#scrParty .settingsgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px}
#scrParty .settingsgrid .cell{text-align:center;border-left:2px dashed var(--line-soft);padding:4px 8px}
#scrParty .settingsgrid .cell:first-child{border-left:0}
#scrParty .settingsgrid .lab{font-size:11px;font-weight:900;letter-spacing:1px;color:var(--ink-soft);text-transform:uppercase}
#scrParty .settingsgrid b{font-family:var(--f-display);font-size:22px}
#scrParty .settingsgrid small{display:block;font-size:11px;color:var(--ink-soft);font-weight:800}
#scrParty .startcol{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;min-width:360px}
#scrParty .startnote{font-size:12.5px;font-weight:800;color:var(--ink-soft)}

/* =================== TRAINING =================== */
#scrTrain .cols{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(420px,.9fr);gap:22px;flex:1;align-items:start}
#scrTrain .camcard{aspect-ratio:16/11}
.trainpanel{display:flex;flex-direction:column;gap:12px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.chips{display:flex;flex-wrap:wrap;gap:6px}
/* the word field shakes when you try to record without naming the clip */
#classInput.nudge{animation:inputNudge .34s cubic-bezier(.3,1.4,.4,1)}
@keyframes inputNudge{
  0%,100%{translate:0 0}
  25%{translate:-5px 0}
  60%{translate:4px 0}
  85%{translate:-2px 0}}
/* a freshly recorded clip is in the list but not in the model yet */
#btnTrain.stale{animation:trainStale 1.6s ease-in-out infinite}
@keyframes trainStale{
  0%,100%{box-shadow:var(--sh)}
  50%{box-shadow:0 4px 0 rgba(18,52,119,.13),0 0 0 4px rgba(255,208,74,.55)}}
@media (prefers-reduced-motion:reduce){
  #classInput.nudge,#btnTrain.stale{animation:none}
  #btnTrain.stale{box-shadow:0 4px 0 rgba(18,52,119,.13),0 0 0 4px rgba(255,208,74,.55)}
}
.wordchip{font-weight:800;font-size:13px;padding:0 14px;min-height:44px;display:inline-flex;align-items:center;border-radius:var(--r-pill);border:2px solid var(--line-soft);color:var(--ink-soft);background:var(--card)}
.wordchip{transition:border-color .14s ease,color .14s ease,transform .14s ease}
.wordchip:hover{border-color:var(--blue);color:var(--blue-deep);transform:translateY(-1px)}
.wordchip:active{transform:translateY(1px) scale(.97)}
.classlist{display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto}
.classrow{display:flex;justify-content:space-between;align-items:center;background:var(--panel);border:2px solid var(--line-soft);border-radius:var(--r-sm);padding:8px 13px;font-weight:800;font-size:14px}
.classrow .count{font-weight:900;font-size:13px;color:var(--blue-deep)}
.classrow .del{color:var(--ink-soft);font-size:14px;padding:2px 7px;font-weight:900}
.classrow .del{transition:color .14s ease,transform .14s ease}
.classrow .del:hover{color:var(--red)}
.classrow .del:active{transform:scale(.88)}
/* A word's own Record button: the big button's orange and white dot, small. */
.classrow .rec{display:inline-flex;align-items:center;gap:5px;background:var(--orange);color:#fff;
  border-radius:999px;padding:3px 10px 3px 8px;font-size:12px;font-weight:900;line-height:1.2;
  transition:transform .14s ease,filter .14s ease,opacity .14s ease}
.classrow .rec .dot{width:7px;height:7px;border-radius:50%;background:#fff}
.classrow .rec:hover{transform:translateY(-1px);filter:brightness(1.08)}
.classrow .rec:active{transform:scale(.9)}
.classlist.armed .rec{opacity:.4;pointer-events:none}
.trainreport{font-weight:800;font-size:13.5px;line-height:1.6;color:var(--ink);background:var(--panel);border-radius:var(--r-sm);padding:11px 13px;display:none}
.trainreport b{color:var(--blue-deep)} .trainreport .warn{color:var(--red)}
.hint{font-size:12.5px;font-weight:800;color:var(--ink-soft);line-height:1.5}
.hint b{color:var(--ink)}

/* =================== RESULTS =================== */
#scrResults{align-items:center;justify-content:center}
/* the old generic results card is gone — see the RESULTS block below */
#scrResults h2{font-family:var(--f-display);font-weight:800;font-size:clamp(30px,4vw,48px);text-align:center;letter-spacing:1px;text-transform:uppercase}
#scrResults .sub{text-align:center;font-size:15px;font-weight:800;color:var(--ink-soft);margin:4px 0 16px}
.resrow{display:flex;justify-content:space-between;gap:10px;border-top:2px dashed var(--line-soft);padding:10px 2px;font-weight:800;font-size:15px;flex-wrap:wrap}
.resrow .ok{color:#0E9B62;font-weight:900} .resrow .miss{color:var(--red);font-weight:900}
.resrow .thought{color:var(--ink-soft);font-size:13px;font-style:italic;width:100%}
#scrResults .actions{display:flex;gap:14px;margin-top:22px}
#scrResults .actions .bigbtn{flex:1;font-size:24px}

/* =================== camera-dock chrome per screen =================== */
/* The dock carries all its chrome; each screen shows only what its mockup has. */
body[data-screen="home"] .camdock .livebadge,
body[data-screen="home"] .camdock .aipill,
body[data-screen="home"] .camdock .brackets,
body[data-screen="solo"] .camdock .livebadge,
body[data-screen="solo"] .camdock .aipill,
body[data-screen="match"] .camdock .livebadge,
body[data-screen="match"] .camdock .aipill,
body[data-screen="pmatch"] .camdock .livebadge,
body[data-screen="pmatch"] .camdock .aipill,
body[data-screen="party"] .camdock .aipill,
body[data-screen="train"] .camdock .aipill{display:none}
body[data-screen="lobby"] .camdock .brackets,
body[data-screen="match"] .camdock .brackets,
body[data-screen="pmatch"] .camdock .brackets,
body[data-screen="party"] .camdock .brackets{display:none}
body[data-screen="lobby"] .camdock .livebadge{display:none} /* card supplies its own */
body[data-screen="party"] .camdock .livebadge{display:none}
body[data-screen="party"] .camdock .aipill{display:none}

/* the rival dock lives inside whichever slot holds it */
#rivalDock{position:absolute;inset:0;border-radius:inherit}
body[data-screen="lobby"] #rivalPlaceholder{display:none}
body[data-screen="lobby"] #rivalDock{background:transparent}
#rivalDock video{transform:scaleX(-1)}

/* =================== responsive =================== */
@media(max-width:1080px){
  header{flex-wrap:wrap}
  .pills{width:100%;order:9;justify-content:flex-start;flex-wrap:wrap}
  #scrHome .cols,#scrSolo .cols,#scrTrain .cols{grid-template-columns:1fr}
  #scrLobby .toprow{grid-template-columns:1fr}
  #scrLobby .midrow{grid-template-columns:1fr}
  #scrLobby .botrow{grid-template-columns:1fr;justify-items:center}
  #scrMatch .board,#scrMatch .panels{grid-template-columns:1fr;gap:20px}
  #scrMatch .vsword{position:relative;left:auto;top:auto;transform:none;margin:-6px auto;max-width:100%}
  #scrParty .teams{grid-template-columns:1fr}
  #scrParty .inforow{grid-template-columns:1fr 1fr}
  #scrParty .botrow{grid-template-columns:1fr}
  .vsburst{display:none}
  .howto{flex-wrap:wrap;gap:12px}
  .howto .cell{border-left:0;padding:0;flex:1 1 45%}
  .statpill{min-width:104px;padding:6px 12px 8px}
  .statpill .val{font-size:23px}
}

@media(max-width:1450px) and (min-width:1081px){
  #scrHome .cols{grid-template-columns:minmax(0,1.15fr) minmax(400px,.85fr);gap:22px}
  #scrHome .camcard{min-height:480px}
  .modecard{min-height:118px;padding:18px 22px}
  .modecard .art{width:96px}.modecard .art svg,.modecard .art img{width:92px;height:88px}
  .modecard h4{font-size:27px}.modecard p{font-size:15px;margin-top:4px}
  .howto{min-height:0;padding:14px 20px;margin-top:18px}
  #scrSolo .cols{grid-template-columns:minmax(0,1.25fr) minmax(400px,.75fr);gap:22px}
  #scrLobby .toprow{grid-template-columns:1fr 300px 1fr;gap:20px}
  #scrLobby .midrow{grid-template-columns:1fr 1fr 1fr;gap:20px}
  #scrParty .inforow{grid-template-columns:auto 1fr auto auto auto;gap:14px}
}

/* =================== PARTY MATCH (Figma "Party Mode" 189:273) ===================
   Six players, one word. Each team's featured player fills their half of the
   split; the other two ride in torn-paper tiles hugging the divider. Timers,
   guess papers, word card and score cards are the same components as Solo and
   1v1 — only the seating and the recognition feedback are new here. */
body[data-screen="pmatch"] .wrap{max-width:none;padding:0}
#scrPartyMatch{position:fixed;inset:0;overflow:hidden;display:grid;place-items:center;
  background:#0e1b4a}
.pmstage{position:relative;z-index:1;overflow:hidden;background:#13245e;
  width:min(100dvw,calc(100dvh * var(--pp-ar)),var(--pp-maxw));
  aspect-ratio:var(--pp-ar);font-family:var(--f-lobby,'Baloo 2',sans-serif)}
@media (max-aspect-ratio:1/1){.pmstage{width:100dvw}}

/* the two featured feeds are one uninterrupted split layer */
.pm-side{position:absolute;top:0;bottom:0;width:50.28%;overflow:hidden;z-index:0;
  background:#13245e;transition:box-shadow .25s ease}
.pm-side.blue{left:0}
.pm-side.orange{right:0}
.pm-side .slot,.pm-side .camdock,.pm-side video,.pm-side canvas{
  position:absolute;inset:0;width:100%;height:100%}
.pm-side video,.pm-side canvas{object-fit:cover}
.pm-divider{position:absolute;left:49.51%;top:-2.35%;width:1.53%;height:109.01%;
  z-index:6;pointer-events:none;
  filter:drop-shadow(0 min(.2vw,.36vh) min(.18vw,.32vh) rgba(0,0,0,.25))}
.pm-divider img{width:100%;height:100%;object-fit:fill;display:block}

/* --- centre tiles --- */
.pm-tile{position:absolute;z-index:5;rotate:var(--rot,0deg);container-type:inline-size;
  cursor:pointer;transform-origin:50% 54%;
  transition:translate .2s cubic-bezier(.2,1.35,.35,1),
    scale .2s cubic-bezier(.2,1.35,.35,1),filter .2s ease;
  filter:drop-shadow(0 min(.3vw,.53vh) min(.36vw,.64vh) rgba(0,0,0,.38))}
.pm-tile:hover{translate:0 calc(-1 * min(.42vw,.75vh));scale:1.027;
  filter:brightness(1.04)
    drop-shadow(0 min(.55vw,.98vh) min(.48vw,.85vh) rgba(0,0,0,.42))}
.pm-tile .slot{position:absolute;inset:1.8% 2.25%;overflow:hidden;background:#0e1b4a}
.pm-tile .slot video,.pm-tile .slot canvas,.pm-tile .camdock{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* Exact 400×500 transparent portrait borders exported from Figma. */
.pm-border{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;z-index:2;transition:opacity .18s ease}
.pm-border-got{opacity:0;z-index:3}
.pmstage[data-size="2"] [data-seat="blue2"],
.pmstage[data-size="2"] [data-seat="orange2"]{display:none}
.pm-tile.vacant{display:none}
/* A player who left keeps their seat — pulling it would reshuffle the board
   mid-round — but it stops pretending they are still in the game. */
.pm-tile.gone .slot,.pm-side.gone .slot{
  filter:grayscale(1) brightness(.42);transition:filter .5s ease}
.pm-tile.gone .pm-nametag,.pm-side.gone .pm-nametag{opacity:.45}

/* --- recognition: the frame goes green and a sticker lands on the player --- */
.pm-tile.got .pm-border-team{opacity:0}
.pm-tile.got .pm-border-got{opacity:1}
.pm-tile.got{animation:pmseatgot .52s cubic-bezier(.18,1.55,.32,1)}
@keyframes pmseatgot{
  0%{scale:.96}
  52%{scale:1.075}
  100%{scale:1}
}
.pm-side.got::after{content:"";position:absolute;inset:0;z-index:4;pointer-events:none;
  border:min(.72vw,1.28vh) solid #62b53c;
  box-shadow:inset 0 0 min(2.4vw,4.3vh) rgba(98,181,60,.5);
  animation:pmsidegot .48s cubic-bezier(.18,1.45,.32,1)}
@keyframes pmsidegot{
  0%{opacity:0;scale:1.05}
  60%{opacity:1;scale:1}
  100%{opacity:1}
}

.pm-nametag{position:absolute;z-index:3;container-type:inline-size;
  pointer-events:auto;cursor:pointer;
  filter:drop-shadow(0 min(.14vw,.25vh) min(.14vw,.25vh) rgba(0,0,0,.4));
  transition:translate .18s ease,scale .18s ease,filter .18s ease}
.pm-nametag:hover{translate:0 calc(-1 * min(.25vw,.44vh));scale:1.04;
  filter:brightness(1.08)
    drop-shadow(0 min(.3vw,.53vh) min(.22vw,.39vh) rgba(0,0,0,.34))}
.pm-nametag img{display:block;width:100%;aspect-ratio:3.55;object-fit:fill}
.pm-nametag b{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;padding:0 7%;font-family:var(--f-lobby);font-weight:400;
  font-size:20.9cqw;color:#fff;white-space:nowrap;overflow:hidden;
  letter-spacing:.01em;text-shadow:1.6cqw 1.6cqw 0 rgba(0,0,0,.45)}
/* orange seats reuse the blue chip paper, warmed to the team colour */
.pm-tile.orange .pm-nametag img,.pm-side.orange .pm-nametag img{
  filter:hue-rotate(158deg) saturate(1.5) brightness(1.12)}

/* YOUR OWN CHIP IS THE ONE LABEL ON THIS BOARD THAT ANSWERS A QUESTION.
   The other five say who a stranger is; yours says which of six unfamiliar
   crops is the mirror, and that is the thing a player actually hunts for on
   a six-way split. So it reads YOU (partyMatch.js writes the word), it is
   about a third bigger, and it leaves the two team colours entirely: the
   chip paper is warmed to the boards' yellow and the type goes navy, which
   is the highest-contrast pairing in this palette and is already how every
   yellow paper in the game is set. Team colour is carried by the seat's own
   border and the score card either way, so nothing is lost by taking it off
   this one chip — and there is only ever ONE yellow chip on the board.
   Font-size is `cqw`, so the size follows the width above.

   THE CHAIN IS SOLVED, NOT GUESSED. `hue-rotate` is a matrix approximation,
   not a rotation in any perceptual space, so 188deg off the chip's #0044d5
   lands on #e66a00 — orange, i.e. the OTHER TEAM'S colour, which is the exact
   confusion this chip exists to remove. Going through grey and sepia gives a
   hue that does not depend on the source at all; these five terms were fitted
   against #f5c542, the yellow the tapes and the how-to-play squares already
   use, and reproduce it to 3/255. Re-solve, do not nudge, if the chip art is
   ever re-exported. */
.pm-tile.mine .pm-nametag img,.pm-side.mine .pm-nametag img{
  filter:grayscale(1) brightness(2.9) sepia(1) saturate(3.25)
    hue-rotate(-12deg) brightness(.95)}
.pm-tile.mine .pm-nametag b,.pm-side.mine .pm-nametag b{
  color:#17356f;letter-spacing:.03em;
  text-shadow:1.4cqw 1.4cqw 0 rgba(255,255,255,.55)}

/* tile seating: blue tags hug the outer edge, orange mirror them; the score
   sign takes the opposite corner so name and total read as one strip */
.pm-tile .pm-nametag{top:3.4%;width:35%}
/* 42, not more: the crown's UNTRACKED fallback is centred at 35-65% of the
   tile and draws over the chip (z-index 6 vs 3). A team-mate's 35% chip
   already runs 4pp into it and always has; 42% is a fifth wider without
   turning that into a collision, and on a tile the yellow is doing the
   identifying anyway. The featured half has no such neighbour — its crown
   fallback is only 13% wide — so it takes the full third. */
.pm-tile.mine .pm-nametag{width:42%}
.pm-tile.blue .pm-nametag{left:4%}
.pm-tile.orange .pm-nametag{right:4%}
/* the featured players get the frame-level Figma positions */
.pm-side .pm-nametag{top:13.33%;width:15.75%}
.pm-side.mine .pm-nametag{width:21%}
.pm-side.blue .pm-nametag{left:2.35%}
.pm-side.orange .pm-nametag{right:3.05%}

/* THE HANDOVER IS A MOVE, NOT A CUT. Once you are in, the featured half goes
   to a team-mate still hunting the word and you take their tile — see
   partyMatch.js. Two feeds changing size in one frame reads as a glitch, so
   each seat that was re-tenanted plays a short settle on its new box. The
   seats themselves never move: it is the tenant that changed. */
.pm-tile.reseated,.pm-side.reseated{animation:pmReseat .42s cubic-bezier(.2,1.3,.35,1)}
@keyframes pmReseat{
  0%  {opacity:.35;scale:.965}
  62% {opacity:1;scale:1.012}
  100%{opacity:1;scale:1}}
@media (prefers-reduced-motion:reduce){
  .pm-tile.reseated,.pm-side.reseated{animation:none}}

/* ---- the seat award: the GOT IT paper, stamped on whoever earned it ----
   This slot used to hold a standing score sign — a green box in every seat
   holding a running total for the whole match. Six boxes of six numbers is a
   spreadsheet, and it made the one figure that matters (what THIS word was
   worth) a flash inside a permanent fixture. It is a stamp again, and it is
   Solo's actual POINTS AWARD paper: it slams onto the seat at recognition,
   holds there for the rest of the word, then is dragged into the score card
   it pays. See js/seatAward.js — including why it is fixed to the viewport
   rather than parented to the seat it is measured from. */
.pp-award{position:fixed;z-index:95;container-type:inline-size;
  pointer-events:none;opacity:0;translate:-50% -50%}
.pp-award .sp-art{display:block;width:100%;aspect-ratio:2.098;object-fit:fill;
  filter:drop-shadow(0 min(.5vw,.89vh) min(.32vw,.57vh) rgba(0,0,0,.42))}
.pp-award span{position:absolute;left:0;width:100%;text-align:center;
  translate:0 -50%;font-family:var(--f-lobby);font-weight:400;line-height:1;
  color:#1b7532;white-space:nowrap;text-shadow:.6cqw .6cqw 0 rgba(0,0,0,.18)}
.pp-award .aw-got{top:19.8%;font-size:7.8cqw}
.pp-award .aw-pts{top:47.3%;font-size:18.93cqw}
.pp-award .aw-lbl{top:75.6%;font-size:7.8cqw}

/* Two beats, one object — the same contract `.sp-award` documents. `.show`
   slams the paper onto the seat and LEAVES IT THERE; `.deliver` then replaces
   the animation outright (rather than layering on it) so its 0% keyframe is
   exactly where the slam came to rest. Centring lives on `translate`, so the
   keyframes own `transform` alone and never have to restate it. */
.pp-award.show{animation:ppawshow .52s cubic-bezier(.16,1.4,.32,1) both}
.pp-award.show.deliver{
  animation:ppawdeliver var(--fly-ms,560ms) cubic-bezier(.5,-.28,.62,1) both}
@keyframes ppawshow{
  0%{opacity:0;transform:scale(.22) rotate(-13deg)}
  46%{opacity:1;transform:scale(1.15) rotate(3.5deg)}
  72%{transform:scale(.96) rotate(-1.4deg)}
  100%{opacity:1;transform:scale(1) rotate(0)}}
@keyframes ppawdeliver{
  0%{opacity:1;transform:translate(0,0) scale(1) rotate(0)}
  /* a short wind-up, so the paper is visibly PICKED UP before it is thrown */
  16%{opacity:1;transform:translate(0,-6%) scale(1.07) rotate(-3deg)}
  100%{opacity:.05;
    transform:translate(var(--fly-x,0px),var(--fly-y,0px))
      scale(var(--fly-s,.24)) rotate(var(--fly-r,14deg))}}

/* theme.css kills every animation under reduced motion, which would leave the
   paper — and the points it reports — stuck at opacity:0. Show it plainly;
   seatAward.js pays the score out immediately in that case, so nothing is
   waiting on a flight that is not running. */
@media (prefers-reduced-motion:reduce){
  .pp-award.show{opacity:1;transform:none}
}

/* ---- the crown: FIRST place, not "recognized" ----
   Party used to land the GOT IT paper on the forehead of everyone who got
   the word, which said nothing about who won it. One crown per word now goes
   to whoever the AI recognized first. The local player's rides their head
   through the same face track the 1v1 crown uses; remote players keep this
   authored head-band position, because their poses are never transmitted. */
.pp-crown{position:absolute;left:50%;top:5%;width:30%;height:auto;z-index:6;
  opacity:0;pointer-events:none;translate:-50% 0;rotate:-6deg;scale:.45;
  filter:drop-shadow(0 min(.3vw,.53vh) min(.22vw,.39vh) rgba(0,0,0,.42));
  transition:opacity .18s ease,scale .3s cubic-bezier(.2,1.65,.4,1)}
.pm-side .pp-crown{width:13%;top:3.5%}
.pm-tile.first .pp-crown,.pm-side.first .pp-crown{opacity:1;scale:1;
  animation:ppcrownarrive .44s cubic-bezier(.18,1.6,.32,1) both}
@keyframes ppcrownarrive{
  0%{opacity:0;translate:-50% -70%;rotate:-24deg;scale:.5}
  62%{opacity:1;translate:-50% 6%;rotate:5deg;scale:1.12}
  100%{opacity:1;translate:-50% 0;rotate:-6deg;scale:1}
}
/* Head-tracked: JS writes left/top/width every frame, so those must ease
   rather than animate. The arrival keyframe animates `translate`, which would
   otherwise keep re-slamming the paper as the head moves — and it has to beat
   the `.first` rule on specificity, not just on source order. */
.pm-tile.first .pp-crown.tracked,.pm-side.first .pp-crown.tracked{
  animation:none;opacity:1;scale:1;translate:-50% 0;rotate:-6deg;
  transition:opacity .18s ease,left .07s linear,top .07s linear,
    width .07s linear}

.pm-tape{position:absolute;z-index:9;pointer-events:none;
  transition:opacity .2s ease,translate .18s ease,
    scale .18s cubic-bezier(.2,1.35,.35,1),filter .18s ease;
  filter:drop-shadow(0 min(.21vw,.37vh) min(.28vw,.49vh) rgba(0,0,0,.26))}
.pmstage:has(#pmMyPast.empty) #pmMyPastTape,
.pmstage:has(#pmFoePast.empty) #pmFoePastTape{opacity:0}
.pmstage:has(#pmMatchTimerCard:hover) #pmMatchTimerTape,
.pmstage:has(#pmScoreBlue:hover) #pmBlueScoreTape,
.pmstage:has(#pmWordTimerCard:hover) #pmWordTimerTape,
.pmstage:has(#pmScoreOrange:hover) #pmOrangeScoreTape,
.pmstage:has(#pmMyTop:hover) #pmMyTopTape,
.pmstage:has(#pmMyPast:hover) #pmMyPastTape,
.pmstage:has(#pmFoeTop:hover) #pmFoeTopTape,
.pmstage:has(#pmFoePast:hover) #pmFoePastTape,
.pmstage:has(#pmWordCard:hover) #pmWordRightTape,
.pmstage:has(#pmWordCard:hover) #pmWordLeftTape{
  translate:0 calc(-1 * min(.3vw,.53vh));scale:1.025;
  filter:brightness(1.04)
    drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(23,53,111,.28))}
.pm-score{z-index:8}
#pmWordTimerCard{z-index:8}
:is(.solostage,.matchstage,.pmstage)
  :is(.sp-card,.v1-scorecard,.pm-score,.sp-guess,.sp-word){
  transition:translate .18s ease,scale .18s cubic-bezier(.2,1.35,.35,1),
    rotate .18s ease,filter .18s ease}
@media (hover:hover){
  :is(.solostage,.matchstage,.pmstage)
    :is(.sp-card,.v1-scorecard,.pm-score,.sp-guess,.sp-word):hover{
    translate:0 calc(-1 * min(.3vw,.53vh));scale:1.025;
    filter:brightness(1.04)
      drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(23,53,111,.28))}
}
/* guess papers: your read on the left, the rival team's featured player right */
.pm-guess{z-index:7;container-type:inline-size}
.pm-guess.mine.current .glabel,.pm-guess.mine.current .gpct{color:#1b7532}
.pm-guess.mine.past .glabel,.pm-guess.mine.past .gpct{color:#0d6ebb}
.pm-guess.theirs.current .glabel,.pm-guess.theirs.current .gpct{color:#fd602f}
.pm-guess.theirs.past .glabel,.pm-guess.theirs.past .gpct{color:#a90307}
/* the previous guess is HISTORY; at .92 it competed with the live one,
   unlike 1v1 (.56) and Solo (.38) */
.pm-guess.past{opacity:.58}
.pm-guess.empty{opacity:0}
.pm-word{z-index:8}

/* the word clock turns into the chase clock once a team is complete */
.pm-chase-clock{position:absolute;left:7.5%;top:36%;width:20%;z-index:2;opacity:0;
  pointer-events:none;filter:drop-shadow(1.1cqw 1.1cqw 0 rgba(0,0,0,.42))}
.pmstage[data-phase="chase"] #pmWordTimerCard{z-index:12;scale:1.16;
  filter:drop-shadow(0 min(.55vw,.98vh) min(.35vw,.62vh) rgba(14,27,74,.42))}
.pmstage[data-phase="chase"] #pmWordTimerCard .sp-art{
  animation:v1chasepaper .42s ease-in-out infinite}
.pmstage[data-phase="chase"] #pmWordTimerCard .sp-lab{
  top:-1.5%;font-size:9.7cqw}
.pmstage[data-phase="chase"] #pmWordTimerCard .sp-val{
  left:20%;width:76%;font-size:23.5cqw;color:#e8402a}
.pmstage[data-phase="chase"] .pm-chase-clock{
  opacity:1;animation:v1clockshake .22s ease-in-out infinite}
.pmstage[data-phase="chase"] .pm-word{
  animation:v1chaseword .24s ease-in-out infinite}
.pmstage.chase-urgent #pmWordTimerCard{scale:1.24}
.pmstage.chase-urgent .pm-word{animation-duration:.16s}
.pmstage.chase-final #pmWordTimerCard{scale:1.32;
  filter:brightness(1.08)
    drop-shadow(0 min(.75vw,1.33vh) min(.48vw,.85vh) rgba(232,64,42,.48))}
.pmstage.chase-final .pm-word{animation-duration:.11s;
  filter:saturate(1.25) brightness(1.06)}
#pmWordTimerCard.tick-pop{animation:v1tickpop .32s cubic-bezier(.18,1.65,.32,1)}

/* The rival placeholder is shared by the lobby and the match. Only the lobby
   has a code to share, so that line must not follow it into gameplay. */
#rivalPlaceholder .ph-match{display:none}
body[data-screen="match"] #rivalPlaceholder .ph-lobby{display:none}
body[data-screen="match"] #rivalPlaceholder .ph-match{display:block}

/* =================== GAME FEEL =================== */

/* Win confetti: real torn-paper pieces thrown from the word card, arcing up
   and then falling. Built in soloRound.js; sized/spun per piece. */
.burstwrap{position:absolute;inset:0;z-index:40;pointer-events:none;overflow:hidden}
.burstwrap.viewport{position:fixed}
.burstwrap i{position:absolute;width:var(--sz);aspect-ratio:1;
  background-size:contain;background-repeat:no-repeat;
  background-position:center;transform:translate(-50%,-50%);
  filter:drop-shadow(0 2px 2px rgba(23,53,111,.28));
  animation:confFly var(--dur) cubic-bezier(.16,.62,.3,1) var(--delay) forwards}
/* Hold full opacity almost the whole way — the old ramp faded the pieces out
   while they were still the celebration. */
@keyframes confFly{
  0%  {opacity:0;transform:translate(-50%,-50%) scale(.3) rotate(0)}
  7%  {opacity:1}
  46% {transform:translate(calc(-50% + var(--dx) * .88),calc(-50% + var(--dy)))
       scale(1) rotate(calc(var(--rot) * .55))}
  82% {opacity:1}
  100%{opacity:0;
       transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy) + var(--fall)))
       scale(.94) rotate(var(--rot))}}

/* Score anticipation: the points chip is in flight and the card has not been
   credited yet, so it braces instead of sitting inert on a stale number. */
.sp-card.awaiting,.v1-scorecard.awaiting{animation:scoreBrace .42s ease-in-out infinite alternate}
@keyframes scoreBrace{from{transform:scale(1)}to{transform:scale(1.035) rotate(-.5deg)}}

/* Streak escalation: a run should visibly build rather than just increment. */
.sp-streak[data-tier="1"] .sp-art{filter:drop-shadow(0 0 min(.7vw,1.2vh) rgba(255,196,64,.55))}
.sp-streak[data-tier="2"]{animation:streakSway 1.5s ease-in-out infinite}
.sp-streak[data-tier="2"] .sp-art{filter:drop-shadow(0 0 min(1.1vw,2vh) rgba(255,170,40,.75))}
.sp-streak[data-tier="3"]{animation:streakSway .85s ease-in-out infinite}
.sp-streak[data-tier="3"] .sp-art{filter:drop-shadow(0 0 min(1.6vw,2.8vh) rgba(255,120,30,.9)) saturate(1.15)}
.sp-streak[data-tier="3"] .sp-val{text-shadow:1.553cqw 1.553cqw 0 rgba(0,0,0,.5),0 0 .8cqw rgba(255,230,150,.9)}
@keyframes streakSway{0%,100%{rotate:-1.4deg}50%{rotate:1.4deg}}

/* Chase urgency: five seconds of maximum tension needs to read at a glance,
   not just as a label swap. A breathing red vignette on the whole stage. */
.matchstage[data-phase="chase"]::after,.pmstage[data-phase="chase"]::after{
  content:"";position:absolute;inset:0;z-index:11;pointer-events:none;
  background:radial-gradient(ellipse at 50% 50%,transparent 42%,rgba(214,40,24,.30) 88%);
  animation:chasePulse .82s ease-in-out infinite}
@keyframes chasePulse{0%,100%{opacity:.45}50%{opacity:1}}

/* Solo word change: the new word stamps down instead of silently swapping. */
.sp-word.wordswap{animation:wordStamp .5s cubic-bezier(.18,1.5,.32,1)}
@keyframes wordStamp{
  0%  {transform:scale(1.18) rotate(-1.8deg);opacity:.25}
  40% {opacity:1}
  62% {transform:scale(.975) rotate(.7deg)}
  100%{transform:scale(1) rotate(0)}}

/* A recognition event outranks the looping chase treatment. Without this
   state precedence, a successful chaser kept wobbling in chase mode and
   never visibly hit/locked even though the server had accepted the pose. */
:is(.matchstage,.pmstage) .sp-word.hit{
  animation:spwordhit .18s cubic-bezier(.2,.82,.3,1)}
:is(.matchstage,.pmstage) .sp-word.locked{
  animation:spwordpop .5s cubic-bezier(.2,1.6,.4,1)}
/* …and closing in on the word outranks the ambient chase/sudden wobble too.
   Those phase rules are more specific than `.sp-word.tracking`, so the
   confidence shake — the single best "you're nearly there" cue — silently
   disappeared exactly when the tension peaked. Scoped to those phases only:
   during a normal round `.sp-word.tracking` already wins on its own, and
   re-declaring it here would restart the shake on every repaint. */
.matchstage[data-phase="chase"] .sp-word.tracking:not(.hit):not(.locked),
.matchstage[data-phase="sudden"] .sp-word.tracking:not(.hit):not(.locked),
.pmstage[data-phase="chase"] .sp-word.tracking:not(.hit):not(.locked){
  animation:spwordtracking var(--shake-speed,220ms) linear infinite}

@media (prefers-reduced-motion: reduce){
  .burstwrap{display:none}
  .sp-streak[data-tier]{animation:none}
  .matchstage[data-phase="chase"]::after,.pmstage[data-phase="chase"]::after{animation:none;opacity:.7}
}

/* An empty room code is "being created", not "hidden" — say so quietly. */
.invite .inv-code.loading{opacity:.55;letter-spacing:.06em}
/* the settings value pops when a stepper actually moves it */
.set-val.bump,#setMatch.bump,#setWord.bump{
  animation:spbump .32s cubic-bezier(.2,1.6,.4,1)}

/* =================== RESULTS (Figma 203:367 / 203:581 / 211:635) ===================
   One screen, two beats. data-page="stage" is the shared moment (live faces,
   who won, rematch); data-page="breakdown" is the personal record (captured
   stills, award, stats). Seats hold LIVE video on the stage page and are
   replaced by still carousels on the breakdown page. */
body[data-screen="results"] .wrap{max-width:none;padding:0}
body[data-screen="results"] header{display:none}
.resstage{position:relative;z-index:1;overflow:hidden;font-family:var(--f-lobby);
  width:min(100dvw,calc(100dvh * var(--pp-ar)),var(--pp-maxw));aspect-ratio:var(--pp-ar);
  background:var(--fig-cream,#f4ece0);
  --rs-fast:140ms;--rs-base:180ms;--rs-emphasis:320ms;--rs-page:340ms;
  --rs-hover:cubic-bezier(.2,1.35,.35,1);--rs-pop:cubic-bezier(.2,1.6,.4,1)}
@media (max-aspect-ratio:1/1){.resstage{width:100dvw}}
.rs-bg{position:absolute;left:-.97%;top:-2.35%;width:102.08%;height:102.35%;
  object-fit:cover;z-index:0}
.rs-divider{position:absolute;left:50.69%;top:-37.41%;width:2.08%;height:147.53%;
  object-fit:fill;z-index:3;pointer-events:none}
.rs-title{position:absolute;left:-.08669%;top:.14574%;width:13.39747%;height:15.96958%;
  rotate:-2.52119deg;z-index:6;
  background:none;border:0;padding:0;
  transform-origin:48% 48%;touch-action:manipulation;
  transition:translate .2s ease,scale .2s cubic-bezier(.2,1.45,.35,1),
    rotate .2s ease,filter .2s ease;
  filter:drop-shadow(0 min(.48612vw,.8642vh) min(.27778vw,.49383vh) rgba(0,0,0,.35))}
.rs-title-art{width:100%;height:100%;display:block;object-fit:fill}
.rs-exit{position:absolute;left:84.583333%;top:2.222222%;width:18.333333%;
  height:10.793925%;z-index:12;background:none;border:0;padding:0;cursor:pointer;
  transition:translate .18s cubic-bezier(.2,1.3,.35,1),scale .18s cubic-bezier(.2,1.3,.35,1),
    rotate .18s ease,filter .18s ease}
.rs-exit img{position:absolute;inset:0;width:100%;height:100%;display:block;object-fit:fill}
.rs-exit:hover{translate:0 calc(-1 * min(.32vw,.57vh));scale:1.035;rotate:-.5deg;
  filter:brightness(1.055) drop-shadow(0 min(.35vw,.62vh) min(.24vw,.43vh) rgba(0,0,0,.25))}
.rs-exit:active{translate:0 min(.1vw,.18vh);scale:.965;rotate:0deg;filter:brightness(.97)}
.rs-exit:focus-visible{outline:min(.208vw,.37vh) solid #17356f;
  outline-offset:calc(-1 * min(.278vw,.494vh))}
.rs-tape{position:absolute;display:block;z-index:7;pointer-events:none;object-fit:fill}
/* These PNGs are the REST-rendered Figma nodes. Their canvases already bake
   rotation, alpha bounds, and (for the logo tapes) the authored shadows. */
.rs-tape-logo-yellow{left:-.972222%;top:-1.140837%;width:5.607639%;height:9.907407%}
.rs-tape-logo-cream{left:10.232273%;top:8.954911%;width:4.444444%;height:7.407407%}
.rs-tape-score{left:25.208333%;top:-4.197531%;width:10.550421%;height:13.051290%}
.resstage[data-size="2"] .rs-tape-score{left:25.138889%;top:-4.938272%}

/* ---- score board ---- */
/* Layer order inside the Score Board is the same in all five Figma results
   frames: score-card-yellow FIRST (backmost), then secret-word-card-wide-cream,
   then the type. So the yellow chip tucks BEHIND the cream headline paper —
   we had them inverted, and the chip's top edge cut the headline's torn
   bottom off. Band: bg 0 · divider/seats 3 (hover 4) · chip 5 · board 6. */
.rs-board{position:absolute;left:29.38%;top:1.85%;width:45.56%;z-index:6;
  container-type:inline-size;pointer-events:auto;
  transition:translate var(--rs-base) var(--rs-hover),scale var(--rs-base) var(--rs-hover),
    filter var(--rs-base) ease}
.rs-board-art{width:100%;display:block;aspect-ratio:6.92;object-fit:fill;
  filter:drop-shadow(0 .5323cqw .3042cqw rgba(0,0,0,.35))}
.rs-headline{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;
  font-family:var(--f-lobby);font-size:9.886cqw;line-height:.75862;color:#17356f;
  letter-spacing:.01em;white-space:nowrap;translate:.152cqw .152cqw;
  text-shadow:.3802cqw .3802cqw 0 rgba(255,255,255,.9)}
.rs-scorechip{position:absolute;left:37.93%;top:10.72%;width:28.44%;z-index:5;
  container-type:inline-size;pointer-events:auto;
  transition:translate var(--rs-base) var(--rs-hover),scale var(--rs-base) var(--rs-hover),
    filter var(--rs-base) ease}
.rs-scorechip img{width:100%;display:block;aspect-ratio:4.28;object-fit:fill;
  filter:drop-shadow(0 .8526cqw .4872cqw rgba(0,0,0,.35))}
.rs-scorechip span{position:absolute;left:8.77%;top:33.85%;width:82.58%;height:51.56%;
  display:flex;align-items:center;justify-content:center;gap:0;
  font-family:var(--f-sign);font-weight:800;font-size:18.270cqw;line-height:.65747;
  letter-spacing:.01em;color:#17356f;white-space:pre;
  text-shadow:.609cqw .609cqw 0 rgba(255,255,255,.9)}
.rs-scorechip .rs-score-sep{font-style:normal;color:#17356f;white-space:pre}
.rs-scorechip b.blue{color:#17356f}
.rs-scorechip b.orange{color:#f4622e}

/* ---- seats: video BEHIND the torn border ---- */
.rs-seat{position:absolute;z-index:3;rotate:var(--rot,0deg);container-type:inline-size;
  transition:translate .18s ease,scale .18s cubic-bezier(.2,1.35,.35,1),filter .18s ease;
  filter:drop-shadow(0 min(.34722vw,.61728vh) min(.27778vw,.49383vh) rgba(0,0,0,.35))}
/* Figma reports an axis-aligned bounding box after rotation. These are the
   original rectangles recovered from those bounds, with the inverse Figma
   angle applied in CSS. This avoids rotating an already-expanded box twice. */
.rs-seat.blue{--rot:-4.39592deg}
.rs-seat.orange{--rot:3.42776deg}
.rs-seat[data-seat="blue0"]{left:1.482685%;top:15.189221%;width:22.765257%;aspect-ratio:.908226}
.rs-seat[data-seat="blue1"]{left:26.0549%;top:17.0229%;width:22.7653%;aspect-ratio:.7109}
.rs-seat[data-seat="blue2"]{left:2.1901%;top:56.5178%;width:27.0262%;aspect-ratio:1.1537}
.rs-seat[data-seat="orange0"]{left:76.138527%;top:11.719037%;width:21.541731%;aspect-ratio:.794489}
.rs-seat[data-seat="orange1"]{left:55.1537%;top:15.4146%;width:21.5845%;aspect-ratio:.6412}
.rs-seat[data-seat="orange2"]{left:73.113371%;top:55.478052%;width:25.084237%;aspect-ratio:1.045491}
.rs-seat .slot{position:absolute;inset:1.65%;z-index:1;overflow:hidden;isolation:isolate;background:#0e1b4a;
  border-radius:min(.4vw,.7vh)}
.rs-seat .slot video,.rs-seat .slot canvas,.rs-seat .slot img{
  position:absolute;inset:0;width:100%;height:100%;min-width:100%;min-height:100%;
  max-width:none;display:block;object-fit:cover;object-position:50% 50%}
.rs-seat .slot .camdock{position:absolute;inset:0;width:100%;height:100%;margin:0;
  border-radius:0;overflow:hidden}
.rs-seat .slot .camdock video,.rs-seat .slot .camdock canvas{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:50% 50%}
.rs-seat .rs-border{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;z-index:2}
.rs-seat .rs-nametag{position:absolute;z-index:3;left:var(--tag-x);top:var(--tag-y);
  width:var(--tag-w);height:var(--tag-h);rotate:var(--tag-turn);
  container-type:inline-size;pointer-events:none}
.rs-seat[data-seat="blue0"]{--tag-x:2.808169%;--tag-y:2.150716%;--tag-w:24.752331%;--tag-h:9.728235%;--tag-turn:4.395915deg}
.rs-seat[data-seat="blue1"]{--tag-x:4.414934%;--tag-y:2.810678%;--tag-w:24.752331%;--tag-h:7.614483%;--tag-turn:4.395915deg}
.rs-seat[data-seat="blue2"]{--tag-x:2.576397%;--tag-y:2.296815%;--tag-w:20.849871%;--tag-h:10.409516%;--tag-turn:4.395915deg}
.rs-seat[data-seat="orange0"]{--tag-x:62.186517%;--tag-y:2.919649%;--tag-w:33.654827%;--tag-h:9.660269%;--tag-turn:-3.427757deg}
.rs-seat[data-seat="orange1"]{--tag-x:62.218260%;--tag-y:2.759166%;--tag-w:33.588065%;--tag-h:7.781334%;--tag-turn:-3.427757deg}
.rs-seat[data-seat="orange2"]{--tag-x:65.753657%;--tag-y:1.690222%;--tag-w:28.901945%;--tag-h:10.916945%;--tag-turn:-3.427757deg}
.rs-seat .rs-ready{position:absolute;z-index:4;left:61.759%;top:88.031%;width:33.556%;
  aspect-ratio:2.7021;container-type:inline-size;opacity:0;scale:.6;rotate:.0736deg;pointer-events:none;
  transition:opacity .2s ease,scale .3s cubic-bezier(.2,1.6,.4,1)}
/* the pop is a REACTION to readying up, so it rides a class results.js adds
   only on the false->true edge — never on the seat merely reappearing */
.rs-seat.ready .rs-ready{opacity:1;scale:1.025;filter:none}
.rs-seat.ready .rs-ready.pop{animation:rsSeatReadyIn 360ms var(--rs-pop)}
@keyframes rsSeatReadyIn{
  0%{scale:.96}
  58%{scale:1.07}
  100%{scale:1.025}
}
.rs-seat .rs-ready img,.rs-nametag img{width:100%;height:100%;display:block;object-fit:fill}
.rs-nametag-art{position:absolute;inset:0;
  filter:drop-shadow(0 var(--tag-fx) var(--tag-fx) rgba(0,0,0,.4))}
.rs-nametag b{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  padding:0 7%;font-family:var(--f-lobby);font-weight:400;color:#fff;letter-spacing:.01em;
  line-height:1.36;white-space:nowrap;overflow:hidden;rotate:var(--tag-text-rot);
  font-size:var(--tag-font);text-shadow:var(--tag-shadow) var(--tag-shadow) 0 rgba(0,0,0,.5)}
.rs-seat.blue .rs-nametag,.rs-strip.blue .rs-nametag,.rs-focus .rs-nametag{
  --tag-text-rot:-5.258087deg;--tag-font:20.31cqw;--tag-shadow:1.255cqw;
  --tag-fx:3.697cqw}
.rs-seat.orange .rs-nametag,.rs-strip.orange .rs-nametag,.rs-focus .rs-nametag.orange{
  --tag-text-rot:2.94deg;--tag-font:20.58cqw;--tag-shadow:1.271cqw;
  --tag-fx:3.457cqw}
.rs-seat .rs-ready b{position:absolute;inset:0 0 6.4%;display:flex;align-items:center;
  justify-content:center;font-family:var(--f-lobby);font-weight:400;color:#fff;
  letter-spacing:.01em;line-height:.7586;white-space:nowrap;font-size:20.47cqw;
  text-shadow:.787cqw .787cqw 0 rgba(0,0,0,.5)}
.resstage[data-size="2"] [data-seat="blue2"],
.resstage[data-size="2"] [data-seat="orange2"]{display:none}
.rs-seat.vacant{display:none}
/* 2v2 has its own four portrait rectangles and tag offsets in Figma. */
.resstage[data-size="2"] [data-seat="blue0"]{left:1.3651%;top:17.5316%;width:24.1857%;aspect-ratio:.6617;
  --tag-x:4.008651%;--tag-y:2.116149%;--tag-w:23.298632%;--tag-h:6.671767%}
.resstage[data-size="2"] [data-seat="blue1"]{left:26.1287%;top:17.0179%;width:22.6914%;aspect-ratio:.7086;
  --tag-x:4.103836%;--tag-y:2.810578%;--tag-w:24.832898%;--tag-h:7.614483%}
.resstage[data-size="2"] [data-seat="orange0"]{left:77.2530%;top:13.3020%;width:21.5417%;aspect-ratio:.5262;
  --tag-x:61.980331%;--tag-y:2.959848%;--tag-w:33.654827%;--tag-h:6.398144%}
.resstage[data-size="2"] [data-seat="orange1"]{left:55.1537%;top:15.4146%;width:21.5845%;aspect-ratio:.6412;
  --tag-x:62.218260%;--tag-y:2.759166%;--tag-w:33.588065%;--tag-h:7.781334%}
/* Solo has no opposing team, so the one portrait gets the otherwise empty
   centre instead of looking stranded on the far-left edge. */
.resstage[data-mode="solo"][data-page="stage"] .rs-divider{display:none}
.resstage[data-mode="solo"][data-page="stage"] [data-seat="blue0"]{
  left:37.9072%;top:17.5316%;width:24.1857%;aspect-ratio:.6617;
  --rot:1.5deg;--counter-rot:-1.5deg}
@media (hover:hover){
  /* The lift has to beat the neighbouring seats it overlaps — never the
     score board above them. At z-index 5 it tied `.rs-board` and won on DOM
     order, so hovering a 1v1 half-screen seat painted its navy video over
     that half of the headline paper. */
  .rs-seat:not(.vacant):hover{translate:0 calc(-1 * min(.28vw,.5vh));scale:1.018;
    z-index:4;filter:brightness(1.035)
      drop-shadow(0 min(.48vw,.85vh) min(.46vw,.82vh) rgba(0,0,0,.32))}
}

/* ---- rematch + breakdown ---- */
.rs-rematch{position:absolute;left:26.18%;top:77.28%;width:51.50%;z-index:8;
  background:none;border:0;padding:0;cursor:pointer;container-type:inline-size;
  transform-origin:50% 56%;touch-action:manipulation;
  transition:translate .18s ease,scale .18s cubic-bezier(.2,1.45,.35,1),filter .18s ease}
.rs-rematch img{width:100%;display:block;aspect-ratio:6.78;object-fit:fill;
  filter:drop-shadow(0 .4845cqw .2768cqw rgba(0,0,0,.35))}
.rs-rematch b{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;font-family:var(--f-sign);font-weight:800;font-size:10.381cqw;
  line-height:.65747;color:#17356f;white-space:pre;letter-spacing:.01em;
  translate:.93cqw .14cqw;
  text-shadow:.346cqw .346cqw 0 rgba(255,255,255,.9)}
.rs-rematch.mine img{filter:drop-shadow(0 .4845cqw .2768cqw rgba(0,0,0,.35))
  saturate(1.05) brightness(1.02)}
.rs-rematch.mine{scale:1.025}
.rs-rematch.pending{cursor:progress}
.rs-rematch-status{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.rs-rematch.count-up{animation:rsReadyGrow 360ms var(--rs-pop)}
.rs-rematch.count-down{animation:rsReadyShrink 240ms var(--rs-hover)}
@keyframes rsReadyGrow{
  0%{scale:.96}
  58%{scale:1.07}
  100%{scale:1.025}
}
@keyframes rsReadyShrink{
  0%{scale:1.025}
  55%{scale:.94}
  100%{scale:1}
}
.rs-tape-rematch{z-index:9}
.rs-tape-rematch-stage{left:47.98611%;top:74.19753%;width:8.26389%;height:6.04938%}
.rs-tape-rematch-breakdown{display:none;left:45.833333%;top:82.839506%;
  width:8.578840%;height:6.878289%}

.rs-breakdown,.rs-back,.rs-save,.ps-back,.ps-save{position:absolute;z-index:8;background:none;border:0;
  padding:0;cursor:pointer;container-type:inline-size;touch-action:manipulation;
  transition:translate .18s ease,scale .18s cubic-bezier(.2,1.45,.35,1),
    rotate .18s ease,filter .18s ease}
.rs-breakdown{left:33.95%;top:87.99%;width:34.28%}
.rs-bd-art{width:100%;display:block;aspect-ratio:5.06;object-fit:fill;
  filter:drop-shadow(0 .7276cqw .4158cqw rgba(0,0,0,.35))}
.rs-breakdown b,.rs-back b,.rs-save b,.ps-back b,.ps-save b{position:absolute;inset:0;display:flex;
  align-items:center;justify-content:center;font-family:var(--f-sign);font-weight:800;
  color:#17356f;white-space:pre;letter-spacing:.01em}
.rs-breakdown b{font-size:9.355cqw;line-height:1.09578;padding-right:12%;
  translate:-.5cqw 1.2cqw;
  text-shadow:.520cqw .520cqw 0 rgba(255,255,255,.9)}
.rs-bd-arrow{position:absolute;left:85.133%;top:30%;width:10.411%;height:52.71%;
  object-fit:fill}
.rs-back,.ps-back{left:3.33%;top:88.95%;width:22.15%;aspect-ratio:319/63}
:is(.rs-back,.ps-back) .rs-bd-art{position:absolute;left:11.91%;top:0;width:88.09%;height:100%;
  filter:drop-shadow(0 min(.1612vw,.2866vh) min(.0921vw,.1638vh) rgba(0,0,0,.35))}
:is(.rs-back,.ps-back) b{left:27.90%;top:22.22%;width:82.10%;height:57.14%;font-size:9.404cqw;
  line-height:1.2067;justify-content:flex-start;translate:0 .313cqw;
  text-shadow:.575cqw .575cqw 0 rgba(255,255,255,.9)}
.rs-back-arrow{position:absolute;left:15.047%;top:24.596%;width:11.007%;height:55.73%;
  object-fit:fill}
.rs-save,.ps-save{left:74.79%;top:88.89%;width:20.69%;aspect-ratio:298/63}
:is(.rs-save,.ps-save) .rs-bd-art{height:100%;aspect-ratio:auto;
  filter:drop-shadow(0 min(.1612vw,.2866vh) min(.0921vw,.1638vh) rgba(0,0,0,.35))}
:is(.rs-save,.ps-save) b{left:5.705%;top:22.222%;width:87.919%;height:57.143%;font-size:10.067cqw;
  line-height:1.2067;justify-content:flex-start;
  text-shadow:.616cqw .616cqw 0 rgba(255,255,255,.9)}
.rs-cam{position:absolute;left:82.55%;top:28.571%;width:12.416%;height:44.444%;
  object-fit:fill}

/* ---- breakdown page ---- */
/* Figma overlaps the thumbnails like a stack of prints rather than spacing
   them out — three square thumbs at full height would overrun the buttons. */
.rs-strip{position:absolute;inset:0;z-index:4;pointer-events:none}
.rs-thumb{position:absolute;aspect-ratio:1;cursor:pointer;
  container-type:inline-size;rotate:var(--rot,0deg);translate:0 0;scale:1;
  border:0;padding:0;background:none;color:inherit;touch-action:manipulation;
  pointer-events:auto;
  transform-origin:50% 52%;will-change:translate,scale,rotate,filter;
  transition:translate var(--rs-base) var(--rs-hover),scale var(--rs-base) var(--rs-hover),
    rotate var(--rs-base) var(--rs-hover),filter var(--rs-base) ease}
.rs-strip.blue .rs-thumb{width:18.038%;--rot:-5.27954deg}
.rs-strip.orange .rs-thumb{width:18.4722%;--rot:4.48021deg}
.rs-strip.blue .rs-thumb:nth-child(1){left:2.3194%;top:16.3456%}
.rs-strip.blue .rs-thumb:nth-child(2){left:2.1805%;top:34.7406%}
.rs-strip.blue .rs-thumb:nth-child(3){left:.99995%;top:55.7283%}
.rs-strip.orange .rs-thumb:nth-child(1){left:78.818257%;top:11.108996%}
.rs-strip.orange .rs-thumb:nth-child(2){left:79.512702%;top:36.788008%}
.rs-strip.orange .rs-thumb:nth-child(3){left:79.9294%;top:55.677%}
.rs-thumb .shotwrap{position:absolute;inset:2.75%;z-index:1;overflow:hidden;background:#0e1b4a}
.rs-thumb .shotwrap img{width:100%;height:100%;object-fit:cover;display:block}
.rs-thumb .rs-border{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;z-index:2}
.rs-thumb .rs-nametag{position:absolute;left:var(--tag-x);top:var(--tag-y);
  width:var(--tag-w);height:var(--tag-h);rotate:var(--tag-turn);
  z-index:3;container-type:inline-size;pointer-events:none}
.rs-strip.blue .rs-nametag{--tag-w:31.239355%;--tag-h:13.518421%;--tag-turn:5.279545deg}
.rs-strip.orange .rs-nametag{--tag-w:39.247212%;--tag-h:14.179555%;--tag-turn:-4.480210deg}
.rs-strip.blue .rs-thumb:nth-child(1) .rs-nametag{--tag-x:-4.831515%;--tag-y:-3.961980%}
.rs-strip.blue .rs-thumb:nth-child(2) .rs-nametag,
.rs-strip.blue .rs-thumb:nth-child(3) .rs-nametag{--tag-x:-7.237939%;--tag-y:-3.024456%}
.rs-strip.orange .rs-thumb:nth-child(1) .rs-nametag{--tag-x:-3.376631%;--tag-y:-3.890933%}
.rs-strip.orange .rs-thumb:nth-child(2) .rs-nametag{--tag-x:-11.423443%;--tag-y:-5.522984%}
.rs-strip.orange .rs-thumb:nth-child(3) .rs-nametag{--tag-x:-2.913741%;--tag-y:-2.795926%}
/* Later prints keep their authored overlap. Only selection may promote a card. */
.rs-strip .rs-thumb:nth-child(1){z-index:1}
.rs-strip .rs-thumb:nth-child(2){z-index:2}
.rs-strip .rs-thumb:nth-child(3){z-index:3}
.rs-thumb:nth-child(odd){--riffle:-1.15deg}
.rs-thumb:nth-child(even){--riffle:1.05deg}
.rs-strip:has(.rs-thumb.on){z-index:5}
.rs-strip .rs-thumb.on{z-index:8;scale:1.08;
  filter:brightness(1.045) contrast(1.025);
  animation:rsThumbSelect var(--rs-emphasis) var(--rs-pop)}
@keyframes rsThumbSelect{
  0%{scale:1}
  58%{scale:1.11}
  100%{scale:1.08}
}
.rs-strip.blue .rs-thumb.on{
  translate:min(1.25vw,2.22vh) calc(-1 * min(.55vw,.98vh))}
.rs-strip.orange .rs-thumb.on{
  translate:calc(-1 * min(1.25vw,2.22vh)) calc(-1 * min(.55vw,.98vh))}
.rs-strip .rs-thumb:focus-visible{outline:min(.18vw,.32vh) solid #ffc440;
  outline-offset:min(.12vw,.21vh)}
.rs-strip .rs-thumb:focus-visible:not(.on){scale:1.02;filter:brightness(1.055)}
.rs-thumb .shotwrap img{min-width:100%;min-height:100%;max-width:none;object-position:50% 50%}

@media (hover:hover){
  body[data-screen="results"] .rs-title:hover{
    translate:0 calc(-1 * min(.28vw,.5vh));scale:1.035;rotate:-3.35deg;
    filter:brightness(1.05)
      drop-shadow(0 min(.56vw,.99vh) min(.42vw,.75vh) rgba(0,0,0,.38))}
  body[data-screen="results"] .rs-rematch:hover:not(:disabled){
    translate:0 calc(-1 * min(.3vw,.53vh));scale:1.025;filter:brightness(1.045) saturate(1.04)}
  body[data-screen="results"] .rs-breakdown:hover:not(:disabled){
    translate:0 calc(-1 * min(.28vw,.5vh));scale:1.035;rotate:-.45deg;filter:brightness(1.045)}
  body[data-screen="results"] :is(.rs-back,.ps-back):hover:not(:disabled){
    translate:calc(-1 * min(.16vw,.28vh)) calc(-1 * min(.24vw,.43vh));
    scale:1.035;rotate:-.65deg;filter:brightness(1.045)}
  body[data-screen="results"] :is(.rs-save,.ps-save):hover:not(:disabled){
    translate:min(.16vw,.28vh) calc(-1 * min(.24vw,.43vh));
    scale:1.035;rotate:.65deg;filter:brightness(1.045)}
  .rs-thumb:hover:not(.on){scale:1.035;
    rotate:calc(var(--rot) + var(--riffle));
    filter:brightness(1.035) contrast(1.01)}
  .rs-thumb.on:hover{scale:1.09}
  .rs-arrow.prev:hover{translate:calc(-1 * min(.24vw,.43vh)) 0;scale:1.15;
    filter:brightness(1.08)}
  .rs-arrow.next:hover{translate:min(.24vw,.43vh) 0;scale:1.15;
    filter:brightness(1.08)}
  .rs-board:hover{translate:0 calc(-1 * min(.1vw,.18vh));scale:1.006;
    filter:brightness(1.015)}
  .rs-scorechip:hover{translate:0 calc(-1 * min(.16vw,.28vh));scale:1.015;
    filter:brightness(1.025)}
  .rs-award:hover{translate:0 calc(-1 * min(.24vw,.43vh));scale:1.035;
    rotate:-6.55deg;filter:brightness(1.025)}
  .rs-stat:hover{translate:0 calc(-1 * min(.18vw,.32vh));scale:1.04;
    filter:brightness(1.025)}
}

body[data-screen="results"] .rs-title:active{
  translate:0 min(.12vw,.21vh);scale:.965;rotate:-1.8deg;filter:brightness(.97)}
body[data-screen="results"] .rs-rematch:active:not(:disabled),
body[data-screen="results"] .rs-breakdown:active:not(:disabled),
body[data-screen="results"] .rs-back:active:not(:disabled),
body[data-screen="results"] .rs-save:active:not(:disabled),
body[data-screen="results"] .ps-back:active:not(:disabled),
body[data-screen="results"] .ps-save:active:not(:disabled){
  translate:0 min(.14vw,.25vh);scale:.965;filter:brightness(.96)}
.rs-thumb:active{scale:.975}
.rs-thumb.on:active{scale:1.035}
.rs-arrow:active{translate:0 min(.12vw,.21vh);scale:.9;filter:brightness(.94)}

.rs-focus{position:absolute;left:21.53%;top:12.84%;width:57.08%;aspect-ratio:1.427;z-index:6;
  container-type:inline-size}
.rs-focus-frame{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;z-index:2;transition:filter var(--rs-base) ease}
.rs-focus-shots{position:absolute;z-index:1;left:1.6%;right:1.6%;top:2%;bottom:2%;overflow:hidden;
  background:#0e1b4a;border-radius:min(.3vw,.55vh)}
/* HARD CUT — no cross-fade, no scale, no animation of any kind. The old
   .22s opacity transition ran on the outgoing still while the incoming one
   animated up from opacity:.45, so both were partly visible every swap and
   the reel read as a flash rather than a cut. A flipbook cuts. */
.rs-focus-shots img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:50% 50%;min-width:100%;min-height:100%;display:block;
  opacity:0}
.rs-focus-shots img.on{opacity:1}
.rs-focus-shots .rs-empty{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;text-align:center;padding:0 8%;color:#cfd9ef;
  font-family:var(--f-sign);font-weight:800;font-size:4.2cqw;line-height:1.4}
.rs-focus .rs-nametag.big{position:absolute;left:.12165%;top:1.73611%;
  width:28.2585%;height:17.4511%;z-index:4;container-type:inline-size;
  pointer-events:none}
.rs-focus .rs-nametag b{translate:-1.29cqw -1.08cqw}
.rs-tape-focus{left:3.163017%;top:-28.298611%;width:21.986706%;height:23.745830%;
  z-index:3}
.rs-arrow{position:absolute;top:41.8403%;width:7.3%;z-index:5;background:none;border:0;
  padding:0;cursor:pointer;touch-action:manipulation;
  transition:translate .16s ease,scale .16s cubic-bezier(.2,1.5,.35,1),filter .16s ease}
.rs-arrow img{width:100%;display:block;
  object-fit:fill}
.rs-arrow::before,.rs-back::before,.rs-save::before,.rs-breakdown::before,.rs-title::before,
.rs-rematch::before,.rs-exit::before,.ps-back::before,.ps-save::before{
  content:"";position:absolute;left:50%;top:50%;
  width:max(100%,44px);height:max(100%,44px);
  translate:-50% -50%}
.rs-arrow>*,.rs-back>*,.rs-save>*,.rs-breakdown>*,.rs-title>*,
.rs-rematch>*,.rs-exit>*,.ps-back>*,.ps-save>*{
  pointer-events:none}
.rs-arrow.prev{left:3%}
.rs-arrow.prev img{rotate:180deg}
.rs-arrow.next{right:3%}
.rs-shot-word{display:none;position:absolute;left:0;bottom:5.5%;width:100%;text-align:center;z-index:4;
  font-family:var(--f-sign);font-weight:800;font-size:3.6cqw;color:#fff;
  text-shadow:0 .18cqw .5cqw rgba(9,22,62,.85);pointer-events:none}

.rs-award{position:absolute;left:20.4840%;top:64.2065%;width:16.8527%;aspect-ratio:1.6376;
  z-index:7;rotate:-5.89592deg;
  container-type:inline-size;pointer-events:auto;
  transition:translate var(--rs-base) var(--rs-hover),scale var(--rs-base) var(--rs-hover),
    rotate var(--rs-base) var(--rs-hover),filter var(--rs-base) ease}
.rs-award img{width:100%;height:100%;display:block;object-fit:fill;
  filter:drop-shadow(0 min(.48612vw,.8642vh) min(.27778vw,.49383vh) rgba(0,0,0,.35))}
.rs-award span{position:absolute;left:0;width:100%;text-align:center;color:#17356f;
  letter-spacing:.01em;overflow:visible;transform-origin:50% 50%}
.rs-award .aw-kicker{top:-2.855%;font-family:var(--f-sign);font-weight:800;
  font-size:17.333cqw;line-height:1.517;
  translate:-2.91cqw 1.76cqw;
  text-shadow:1.333cqw 1.333cqw 0 rgba(255,255,255,.9)}
.rs-award .aw-title{top:23.996%;font-family:var(--f-lobby);font-size:20.603cqw;
  line-height:1.2764;translate:.96cqw .41cqw;white-space:nowrap;
  text-shadow:1.333cqw 1.333cqw 0 rgba(255,255,255,.9)}
.rs-award .aw-sub{top:54.791%;font-family:var(--f-sign);font-weight:800;
  font-size:12.362cqw;line-height:2.127;translate:1.33cqw .86cqw;white-space:nowrap}
.rs-award .aw-sub.fit-wrap{left:5%;top:57%;width:90%;height:38%;display:flex;
  align-items:center;justify-content:center;white-space:normal;text-wrap:balance;
  line-height:.9}

.rs-stat{position:absolute;z-index:7;container-type:inline-size;pointer-events:auto;
  transition:translate var(--rs-base) var(--rs-hover),scale var(--rs-base) var(--rs-hover),
    filter var(--rs-base) ease}
.rs-stat img{width:100%;height:100%;display:block;object-fit:fill;
  filter:drop-shadow(0 min(.54748vw,.97329vh) min(.31284vw,.55617vh) rgba(0,0,0,.35))}
.rs-stat b{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;font-family:var(--f-sign);font-weight:800;font-size:26.181cqw;
  line-height:1.517;letter-spacing:.01em;color:#17356f;white-space:nowrap;
  rotate:var(--text-rot);translate:var(--text-x) 1.66cqw;
  text-shadow:2.014cqw 2.014cqw 0 rgba(255,255,255,.9)}
#rsStat1{left:38.0273%;top:70.2143%;width:12.5655%;aspect-ratio:2.4;
  rotate:-4.33875deg;--text-rot:1.3583deg;--text-x:.55cqw}
#rsStat2{left:50.8331%;top:70.6332%;width:12.5655%;aspect-ratio:2.4;
  rotate:2.21815deg;--text-rot:-.6443deg;--text-x:2.21cqw}
#rsStat3{left:63.5815%;top:70.6332%;width:12.5655%;aspect-ratio:2.4;
  rotate:-.62868deg;--text-rot:.9738deg;--text-x:-1.66cqw}

/* ---- which page is showing ---- */
.resstage[data-page="stage"] .rs-strip,
.resstage[data-page="stage"] .rs-focus,
.resstage[data-page="stage"] .rs-award,
.resstage[data-page="stage"] .rs-stat,
.resstage[data-page="stage"] .rs-back,
.resstage[data-page="stage"] .rs-save{display:none}
.resstage[data-page="breakdown"] .rs-seat,
.resstage[data-page="breakdown"] .rs-breakdown,
.resstage[data-page="breakdown"] .rs-divider{display:none}
.resstage[data-page="stage"] .rs-tape-rematch-breakdown{display:none}
.resstage[data-page="breakdown"] .rs-tape-score,
.resstage[data-page="breakdown"] .rs-tape-rematch-stage,
.resstage[data-page="breakdown"] .rs-scorechip{display:none}
.resstage[data-page="breakdown"] .rs-tape-rematch-breakdown{display:block}
.resstage[data-page="breakdown"] .rs-rematch{left:27.43%;top:85.93%;width:45.28%}
.resstage[data-page="breakdown"] .rs-board{left:28.68%;top:1.11%}
.resstage[data-page="breakdown"] .rs-bg{top:-1.23457%}
.resstage[data-page="breakdown"] .rs-exit{left:84.722222%;top:1.728395%}

/* the page swap is a beat, not a cut */
.resstage[data-page] .rs-focus,.resstage[data-page] .rs-strip{animation:rsIn .34s cubic-bezier(.2,1.35,.35,1) both}
@keyframes rsIn{from{opacity:0;transform:translateY(2.2%) scale(.985)}to{opacity:1;transform:none}}

@media (prefers-reduced-motion:reduce){
  .resstage[data-page] .rs-focus,.resstage[data-page] .rs-strip{
    animation:none!important}
  .rs-seat,.rs-seat:hover{
    translate:0 0!important;scale:1!important;animation:none!important;
    transition:none!important}
  .rs-seat .rs-ready{
    scale:1!important;animation:none!important;transition:none!important}
  .rs-exit,.rs-exit:hover,.rs-exit:active{
    translate:0 0!important;scale:1!important;rotate:0deg!important;
    animation:none!important;transition:none!important}
  .rs-thumb,.rs-thumb.on,.rs-thumb:hover,.rs-thumb:focus-visible,.rs-thumb:active{
    translate:0 0!important;scale:1!important;rotate:var(--rot,0deg)!important;
    animation:none!important;transition:none!important}
  .rs-rematch,.rs-rematch.mine,.rs-rematch:hover,.rs-rematch:active,
  .rs-title,.rs-title:hover,.rs-title:active,
  .rs-breakdown,.rs-breakdown:hover,.rs-breakdown:active,
  .rs-back,.rs-back:hover,.rs-back:active,
  .rs-save,.rs-save:hover,.rs-save:active,
  .ps-back,.ps-back:hover,.ps-back:active,
  .ps-save,.ps-save:hover,.ps-save:active,
  .rs-arrow,.rs-arrow:hover,.rs-arrow:active,
  .rs-board,.rs-scorechip,.rs-award,.rs-stat{
    translate:0 0!important;scale:1!important;animation:none!important;
    transition:none!important}
  .rs-title,.rs-title:hover,.rs-title:active{rotate:-2.52119deg!important}
  .rs-award,.rs-award:hover{rotate:-5.89592deg!important}
}

/* dev shortcuts — deliberately plain so they never read as game UI. The BAR
   carries the fixed position; each button is a flow item inside it, so a new
   one stacks instead of landing on the one before it. */
/* Players never see the owner's tools: the Training gear, the sound buttons
   and this DEV bar need body[data-dev] (main.js applyDevMode, ?dev). */
body:not([data-dev]) :is(.devbar,#btnHomeGear,#btnHomeSound,#btnSoloSound,
  #btnMatchSound,#btnPartySound){display:none!important}
.devbar{position:fixed;right:10px;bottom:10px;z-index:200;
  display:flex;flex-direction:column;align-items:flex-end;gap:6px}
/* DEV CHROME MUST NOT SIT ON A REAL CONTROL. The bar's own corner is where
   START MATCH lives in both lobbies, and stacking a second button made it tall
   enough to cover that button's centre at smaller windows — a click on START
   then opened Training and quietly took the host out of the room, which is a
   miserable thing to hit while a friend is waiting. No corner is free on every
   screen (bottom-right is START, top-right is the header gear), so rather than
   move it somewhere else that collides, it simply stands down wherever a real
   game is being set up or played. Home, Training and Results keep it, which is
   everywhere it is actually used from. */
body[data-state="lobby"] .devbar,
body[data-state="party"] .devbar,
body[data-state="countdown"] .devbar,
body[data-state="playing"] .devbar,
body[data-state="word-won"] .devbar{display:none}
/* the photo strips put SAVE in the bar's own corner */
body:has(#resStage[data-strips]) .devbar{display:none}
.devbtn{font-family:var(--f-label);font-weight:800;font-size:clamp(9px,.72vw,13px);
  letter-spacing:.06em;color:#fff;background:rgba(9,22,62,.72);
  border:1px solid rgba(255,255,255,.35);border-radius:var(--r-xs);
  padding:.5em .8em;cursor:pointer}
.devbtn:hover{background:rgba(9,22,62,.92)}

/* the dock's own chrome is noise on a results wall of faces */
body[data-screen="results"] .camdock .livebadge,
body[data-screen="results"] .camdock .aipill,
body[data-screen="results"] .camdock .brackets{display:none}

/* =================== RESULTS: 1v1 + SOLO LAYOUTS ===================
   Figma "1v1 Results" 231:419, "1v1 breakdown" 232:534 and
   "Single Player breakdown + reuslts" 233:711. Same components as the party
   results, re-placed per mode — every left/top/width is the Figma rectangle,
   and every aspect-ratio is that rectangle (W*1440 / H*810), not the asset's
   natural size. */

/* ---- 1v1 RESULTS: two full-half seats either side of the divider ---- */
.resstage[data-mode="1v1"] [data-seat="blue1"],
.resstage[data-mode="1v1"] [data-seat="blue2"],
.resstage[data-mode="1v1"] [data-seat="orange1"],
.resstage[data-mode="1v1"] [data-seat="orange2"]{display:none}
.resstage[data-mode="1v1"] [data-seat="blue0"]{
  left:0;top:0;width:48.333333%;height:100%;aspect-ratio:auto;rotate:0deg}
.resstage[data-mode="1v1"] [data-seat="orange0"]{
  left:50.138889%;top:0;width:49.861111%;height:100%;aspect-ratio:auto;rotate:0deg}
/* full-bleed halves carry no torn frame — the divider does that job */
.resstage[data-mode="1v1"] .rs-seat .rs-border{display:none}
.resstage[data-mode="1v1"] .rs-seat .slot{inset:0;border-radius:0}
.resstage[data-mode="1v1"] [data-seat="blue0"] .rs-nametag{
  left:1.982136%;top:18.996299%;width:23.079936%;height:7.037037%;
  rotate:-10.275430deg}
.resstage[data-mode="1v1"] [data-seat="orange0"] .rs-nametag{
  left:75.689604%;right:auto;top:17.715029%;width:22.493036%;height:6.296296%;
  rotate:11.426281deg}
.resstage[data-mode="1v1"] .rs-nametag{
  --tag-text-rot:0deg;--tag-font:20.886cqw;--tag-shadow:1.290cqw}
.resstage[data-mode="1v1"] [data-seat="blue0"] .rs-ready{
  left:28.479900%;right:auto;top:17.604917%;bottom:auto;
  width:23.811715%;height:7.571972%;aspect-ratio:auto;rotate:.415306deg}
.resstage[data-mode="1v1"] [data-seat="orange0"] .rs-ready{
  left:49.334%;right:auto;top:17.604917%;bottom:auto;
  width:23.811715%;height:7.571972%;aspect-ratio:auto;rotate:-.415306deg}
.resstage[data-mode="1v1"] .rs-seat.ready .rs-ready{scale:1}
.resstage[data-mode="1v1"][data-page="stage"]{
  background:#0e1b4a}
.resstage[data-mode="1v1"][data-page="stage"] .rs-bg,
.resstage[data-mode="1v1"][data-page="stage"] .rs-tape-score{display:none}
.resstage[data-mode="1v1"][data-page="stage"] .rs-title{
  left:.886856%;top:.545889%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-tape-logo-yellow{
  left:.001323%;top:-.740741%;width:5.038125%;height:8.532716%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-tape-logo-cream{
  left:11.205817%;top:9.355061%;width:4.428403%;height:6.602840%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-exit{
  left:85.556878%;top:2.622372%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-board{
  left:26.666667%;top:2.222222%;width:45.555560%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-scorechip{
  left:35.223490%;top:11.090856%;width:28.441912%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-rematch{
  left:24.236111%;top:74.814815%;width:51.498371%}
.resstage[data-mode="1v1"] .rs-rematch.mine{scale:1}
.resstage[data-mode="1v1"][data-page="stage"] .rs-tape-rematch-stage{
  left:46.041667%;top:71.728395%;width:8.263889%;height:6.049383%}
.resstage[data-mode="1v1"][data-page="stage"] .rs-breakdown{
  left:32.005412%;top:85.522401%;width:34.284729%}
/* the 1v1 divider is the solid centre paper, not the two-tone team strip */
.resstage[data-mode="1v1"] .rs-divider,
.resstage[data-mode="solo"] .rs-divider{
  left:48.333333%;top:-19.753086%;width:1.805556%;height:139.506173%}

/* The 1v1 halves are the BACKGROUND, not cards — they never lift or rise. */
@media (hover:hover){
  .resstage[data-mode="1v1"][data-page="stage"] .rs-seat:hover{
    translate:0 0;scale:1;filter:none;z-index:3}
}

/* ---- 1v1 BREAKDOWN: both players side by side, no thumbnails ---- */
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-strip,
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-arrow,
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-divider{display:none}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-bg{
  left:-.972222%;top:-1.234568%;width:102.083333%;height:102.345679%}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-tape-score{
  display:block;left:22.77552%;top:-9.60748%;width:14.65636%;height:21.66449%}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-focus{
  left:.902778%;top:16.666667%;width:49.236111%;height:66.790123%;
  aspect-ratio:auto}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-focus.b{
  display:block;left:50.138889%;top:16.913580%;width:49.444444%;height:67.037037%;
  aspect-ratio:auto}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-focus-shots{
  background:transparent}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-empty{opacity:0}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-award{
  left:.417478%;top:66.841084%;width:14.536426%;aspect-ratio:1.637555}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-award.b{
  display:block;left:50.139700%;top:66.841084%;width:14.536426%;
  aspect-ratio:1.637555}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStat1{
  left:14.977620%;top:70.858410%;width:11.945771%;height:9.463232%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:1.300006%;--paper-y:7.580578%;--paper-w:90.730899%;--paper-h:84.839117%;
  --paper-rot:-4.338747deg;--text-rot:-2.980198deg;--text-x:.55cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStat2{
  left:26.095412%;top:71.566961%;width:12.133421%;height:8.791611%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:.686880%;--paper-y:4.207177%;--paper-w:89.327694%;--paper-h:91.320258%;
  --paper-rot:2.218150deg;--text-rot:1.573755deg;--text-x:2.21cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStat3{
  left:37.150489%;top:71.831597%;width:11.895248%;height:8.328029%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:.205541%;--paper-y:1.266997%;--paper-w:91.116266%;--paper-h:96.403644%;
  --paper-rot:-.628684deg;--text-rot:.345125deg;--text-x:-1.66cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-stat.b{display:block}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStatB1{
  left:64.699843%;top:70.858410%;width:11.945771%;height:9.463232%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:1.300006%;--paper-y:7.580578%;--paper-w:90.730899%;--paper-h:84.839117%;
  --paper-rot:-4.338747deg;--text-rot:-2.980198deg;--text-x:.55cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStatB2{
  left:75.817634%;top:71.566961%;width:12.133421%;height:8.791611%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:.686880%;--paper-y:4.207177%;--paper-w:89.327694%;--paper-h:91.320258%;
  --paper-rot:2.218150deg;--text-rot:1.573755deg;--text-x:2.21cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] #rsStatB3{
  left:86.872711%;top:71.831597%;width:11.895248%;height:8.328029%;
  aspect-ratio:auto;rotate:0deg;
  --paper-x:.205541%;--paper-y:1.266997%;--paper-w:91.116266%;--paper-h:96.403644%;
  --paper-rot:-.628684deg;--text-rot:.345125deg;--text-x:-1.66cqw}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-stat img{
  position:absolute;left:var(--paper-x);top:var(--paper-y);
  width:var(--paper-w);height:var(--paper-h);rotate:var(--paper-rot)}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-stat b{
  rotate:var(--text-rot);
  font-size:min(2.837715vw,5.044828vh);
  text-shadow:min(.218286vw,.388064vh) min(.218286vw,.388064vh) 0 rgba(255,255,255,.9)}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-focus .rs-nametag.big{
  left:-.903083%;top:3.286887%;width:27.481468%;height:12.779678%;
  rotate:-5.258087deg}
.resstage[data-mode="1v1"][data-page="breakdown"] .rs-focus.b .rs-nametag.big{
  left:75.403215%;right:auto;top:2.496894%;width:25.135995%;height:10.408158%;
  rotate:11.699311deg}

/* The photo reels auto-advance and are not interactive: no tap, no swipe,
   no keys, so no pointer cursor, press state or focus ring either. Each new
   still HARD CUTS in — no cross-fade, no scale. There is therefore nothing
   for the reduced-motion blocks below to suppress on the reel, and they no
   longer mention it. */

/* A restrained, one-time assembly beat, gated on [data-enter] rather than
   [data-page]: results.js sets it once per page per match, so flipping
   stage <-> breakdown no longer re-slams every paper on screen. */
.resstage[data-mode="1v1"][data-enter="stage"]
  :is(.rs-title,.rs-tape-logo-yellow,.rs-tape-logo-cream,.rs-exit,.rs-board,.rs-scorechip){
  animation:rs1v1PaperIn 320ms 30ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="stage"] :is(.rs-divider,.rs-seat){
  animation:rs1v1PaperIn 360ms 90ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="stage"] .rs-rematch>*,
.resstage[data-mode="1v1"][data-enter="stage"] .rs-tape-rematch-stage{
  animation:rs1v1PaperIn 340ms 170ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="stage"] .rs-breakdown{
  animation:rs1v1PaperIn 320ms 230ms var(--rs-pop) both}
/* The brand, LEAVE and the headline board do not move between the two pages,
   so they are deliberately absent here — only the papers this page actually
   introduces get assembled. */
.resstage[data-mode="1v1"][data-enter="breakdown"] .rs-tape-score{
  animation:rs1v1PaperIn 320ms 30ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="breakdown"] .rs-focus{
  animation:rs1v1PaperIn 360ms 90ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="breakdown"] :is(.rs-award,.rs-stat){
  animation:rs1v1PaperIn 350ms 150ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="breakdown"] .rs-rematch>*,
.resstage[data-mode="1v1"][data-enter="breakdown"] .rs-tape-rematch-breakdown{
  animation:rs1v1PaperIn 340ms 210ms var(--rs-pop) both}
.resstage[data-mode="1v1"][data-enter="breakdown"] :is(.rs-back,.rs-save){
  animation:rs1v1PaperIn 320ms 260ms var(--rs-pop) both}
@keyframes rs1v1PaperIn{
  from{opacity:0;transform:translateY(min(.7vw,1.24vh)) scale(.978)}
  70%{opacity:1;transform:translateY(calc(-1 * min(.07vw,.12vh))) scale(1.003)}
  to{opacity:1;transform:none}
}

@media (prefers-reduced-motion:reduce){
  .resstage[data-mode="1v1"] :is(.rs-title,.rs-tape-logo-yellow,.rs-tape-logo-cream,
    .rs-exit,.rs-board,.rs-scorechip,.rs-divider,.rs-seat,.rs-tape-rematch-stage,
    .rs-tape-rematch-breakdown,.rs-breakdown,.rs-focus,.rs-award,.rs-stat,.rs-back,.rs-save),
  .resstage[data-mode="1v1"] .rs-rematch>*{
    animation:none!important}
}

/* the mirrored set only exists for 1v1 */
.rs-focus.b,.rs-award.b,.rs-stat.b{display:none}

/* ---- SOLO: results and breakdown on one screen ---- */
.resstage[data-page="solo"] .rs-strip,
.resstage[data-page="solo"] .rs-arrow,
.resstage[data-page="solo"] .rs-breakdown,
.resstage[data-page="solo"] .rs-back,
.resstage[data-page="solo"] .rs-focus.b,
.resstage[data-page="solo"] .rs-award.b,
.resstage[data-page="solo"] .rs-stat.b,
.resstage[data-page="solo"] [data-seat="blue1"],
.resstage[data-page="solo"] [data-seat="blue2"],
.resstage[data-page="solo"] [data-seat="orange0"],
.resstage[data-page="solo"] [data-seat="orange1"],
.resstage[data-page="solo"] [data-seat="orange2"]{display:none}
/* Figma places a full-width Home paper at x=711 and clips it at the stage
   edge. Keeping that source width preserves the texture scale on the right. */
.resstage[data-page="solo"] .rs-bg{
  left:49.375%;top:0;width:99.930556%;height:100%}
.resstage[data-page="solo"] .rs-title{
  left:.879203%;top:.567149%}
.resstage[data-page="solo"] .rs-tape-logo-yellow{
  left:0;top:-.740741%;width:5.038125%;height:8.532716%}
.resstage[data-page="solo"] .rs-tape-logo-cream{
  left:11.205833%;top:9.355062%;width:4.428403%;height:6.602840%}
.resstage[data-page="solo"] .rs-exit{
  left:85.556875%;top:2.622346%;width:18.333333%;height:10.793925%}
.resstage[data-page="solo"] .rs-tape-score{display:none}
.resstage[data-page="solo"] .rs-tape-focus{display:none}
.resstage[data-page="solo"] [data-seat="blue0"]{
  left:0;top:0;width:48.33%;height:100%;aspect-ratio:auto;rotate:0deg}
.resstage[data-page="solo"] .rs-seat .rs-border{display:none}
.resstage[data-page="solo"] .rs-seat .slot{inset:0;border-radius:0}
.resstage[data-page="solo"] .rs-seat .rs-ready{display:none}
.resstage[data-page="solo"] [data-seat="blue0"] .rs-nametag{
  left:.852961%;top:20.327595%;width:23.080%;height:7.037037%;
  rotate:-10.884041deg;
  --tag-text-rot:0deg;--tag-font:20.886cqw;--tag-shadow:1.290cqw;
  --tag-fx:2.632cqw}
/* Solo lifts both papers over the live camera half, but keeps the Figma
   order between them: cream headline in front, yellow chip behind. */
.resstage[data-page="solo"] .rs-board{
  left:32.222222%;top:0;width:35.555556%;z-index:10}
.resstage[data-page="solo"] .rs-scorechip{
  left:35.154028%;top:8.868642%;width:28.441944%;z-index:9}
/* the reel plays itself — nothing to point at, nothing to swipe */
.resstage[data-page="solo"] .rs-focus{
  display:block;left:52.222222%;top:13.209877%;width:45.833333%;
  aspect-ratio:660/408}
.resstage[data-page="solo"] .rs-focus .rs-nametag.big{display:none}
.resstage[data-page="solo"] .rs-save{
  left:80%;top:54.821111%;width:16.736111%}
.resstage[data-page="solo"] .rs-award{
  left:.074831%;top:80.929274%;width:14.695762%;aspect-ratio:1.637555}
.resstage[data-page="solo"] #rsStat1{
  left:14.951580%;top:85.715836%;width:10.957326%}
.resstage[data-page="solo"] #rsStat2{
  left:26.118485%;top:86.080971%;width:10.957336%}
.resstage[data-page="solo"] #rsStat3{
  left:37.235204%;top:86.081193%;width:10.957301%}
.resstage[data-page="solo"] .rs-stat b{
  font-size:min(2.86882vw,5.10012vh);
  text-shadow:min(.22068vw,.39232vh) min(.22068vw,.39232vh) 0 rgba(255,255,255,.9)}
.resstage[data-page="solo"] .rs-rematch{
  left:51.548379%;top:85.809114%;width:47.083312%;rotate:-.347283deg}
.resstage[data-page="solo"] .rs-rematch img{aspect-ratio:6.784044}
.resstage[data-page="solo"] .rs-tape-rematch-breakdown{display:none}
.resstage[data-page="solo"] .rs-tape-rematch-stage{
  left:50%;top:85.185134%;width:6.750820%;height:4.941777%;
  rotate:-15.253273deg}

/* every pose of the run and what it paid */
/* Figma fits exactly five rows a column between the frame and REMATCH, so the
   block is height-capped and the type shrinks to stay inside it. */
.resstage[data-page="solo"] .rs-poselist{
  position:absolute;left:54.375%;top:66.049383%;width:39.583333%;height:17.901235%;z-index:7;
  display:grid;grid-template-columns:44.912281% 44.912281%;column-gap:10.175438%;
  pointer-events:auto}
.resstage[data-page="solo"] .rs-posecol{
  list-style:none;margin:0;padding:0;overflow:visible}
.resstage[data-page="solo"] .rs-posecol li{
  display:flex;align-items:baseline;gap:.34em;white-space:nowrap;
  font-family:var(--f-lobby);font-weight:400;color:#17356f;
  font-size:calc(min(1.905285vw,3.387174vh) * var(--pl-row-fs,1));
  line-height:1.093449;letter-spacing:.01em;
  text-shadow:min(.073280vw,.130276vh) min(.073280vw,.130276vh) 0 rgba(255,255,255,.9);
  transition:translate var(--rs-base) var(--rs-hover),filter var(--rs-base) ease}
.resstage[data-page="solo"] .rs-posecol .pl-word,
.resstage[data-page="solo"] .rs-posecol .pl-dash,
.resstage[data-page="solo"] .rs-posecol .pl-pts{flex:0 0 auto}
.resstage[data-page="solo"] .rs-posecol .pl-word{
  overflow:visible;text-overflow:clip}
.resstage[data-page="solo"] .rs-posecol .pl-dash{opacity:1}
.resstage[data-page="solo"] .rs-posecol .pl-pts.got{color:#176f2d}
.resstage[data-page="solo"] .rs-posecol .pl-pts.missed{color:#b01313}
.resstage[data-page="solo"] .rs-posecol .pl-empty{opacity:.6}
.resstage:not([data-page="solo"]) .rs-poselist{display:none}

/* solo's headline paper is a narrower Figma rectangle than the party board */
.resstage[data-page="solo"] .rs-board-art{aspect-ratio:5.39}
/* Text frames are wider than their paper components in the authored file. */
.resstage[data-page="solo"] .rs-headline{
  inset:auto;left:-9.097409%;top:23.629474%;width:118.186796%;height:51.986393%;
  display:flex;align-items:flex-start;justify-content:center;
  font-size:12.666351cqw;line-height:.758619;translate:0 0;
  text-shadow:.487167cqw .487167cqw 0 rgba(255,255,255,.9)}
.resstage[data-page="solo"] .rs-scorechip span{
  left:6.294986%;top:33.584126%;width:91.805224%;height:52.202042%;
  justify-content:flex-start;
  font-size:min(4.861111vw,8.641975vh)!important;line-height:.702823}

/* Solo's reel auto-advances like the others — the still hard cuts. */

/* The combined screen assembles in readable beats while the camera remains
   live underneath. All rectangles finish at their authored resting values. */
.resstage[data-page="solo"] :is(.rs-board,.rs-scorechip){
  animation:rsSoloPaperIn 320ms 40ms var(--rs-pop) both}
.resstage[data-page="solo"] .rs-focus{
  animation:rsSoloPaperIn 360ms 100ms var(--rs-pop) both}
.resstage[data-page="solo"] :is(.rs-poselist,.rs-save){
  animation:rsSoloPaperIn 330ms 170ms var(--rs-pop) both}
.resstage[data-page="solo"] :is(.rs-award,.rs-stat){
  animation:rsSoloPaperIn 350ms 230ms var(--rs-pop) both}
.resstage[data-page="solo"] .rs-rematch{
  animation:rsSoloPaperIn 360ms 290ms var(--rs-pop) both}
@keyframes rsSoloPaperIn{
  from{opacity:0;transform:translateY(min(.75vw,1.33vh)) scale(.975)}
  70%{opacity:1;transform:translateY(calc(-1 * min(.08vw,.14vh))) scale(1.004)}
  to{opacity:1;transform:none}
}

@media (hover:hover){
  .resstage[data-page="solo"] .rs-seat:hover{
    translate:0 0;scale:1;filter:brightness(1.018)}
  .resstage[data-page="solo"] .rs-poselist:hover li{
    translate:min(.08vw,.14vh) 0;filter:brightness(1.04)}
  .resstage[data-page="solo"] .rs-poselist:hover li:nth-child(even){
    translate:min(.13vw,.23vh) 0}
}

@media (prefers-reduced-motion:reduce){
  .resstage[data-page="solo"] :is(.rs-board,.rs-scorechip,.rs-focus,.rs-poselist,
    .rs-save,.rs-award,.rs-stat,.rs-rematch){animation:none!important}
}
/* the logo sits on top of the breakdown frames, as Figma layers it */
.rs-title{z-index:12}

/* =================== SEE PHOTO STRIPS: the button that asks to be pressed ===
   The photo strips are the best thing on this screen and the easiest to walk
   past, so the button HOPS and SHAKES: two bounces with a landing squash, a
   hard side-to-side rattle, then a short rest, on a loop, inside a warm
   GLOW that swells on the same beat. It runs on an inner span, never on the
   button, because the button's entrance, hover lift and press all own
   `translate`/`scale`, and Solo's entrance holds `transform` with fill:both
   for as long as the page is up. The glow is a filter on the same span, so
   it hugs the torn paper rather than drawing a box round it. */
/* The board behind it is CREAM, so a yellow glow barely registers: the halo
   is orange, stacked three deep, and the paper brightens as it swells. */
.rs-see-hop{position:absolute;inset:0;display:block;transform-origin:50% 92%;
  filter:drop-shadow(0 0 1.2cqw rgba(255,170,30,.9))
    drop-shadow(0 0 3cqw rgba(255,140,20,.65)) drop-shadow(0 0 6cqw rgba(255,120,10,.35));
  animation:rsSeeHop 2.2s cubic-bezier(.3,.7,.4,1) .9s infinite,
    rsSeeGlow 1.1s ease-in-out infinite alternate}
@keyframes rsSeeHop{
  0%,70%,100%{transform:none}
  6%{transform:translateY(-42%) rotate(-5deg)}
  12%{transform:translateY(0) scale(1.08,.9)}
  18%{transform:translateY(-22%) rotate(4deg)}
  24%{transform:translateY(0) scale(1.04,.95)}
  30%{transform:translateX(-4%) rotate(-7deg)}
  36%{transform:translateX(4%) rotate(7deg)}
  42%{transform:translateX(-3.5%) rotate(-6deg)}
  48%{transform:translateX(3%) rotate(5deg)}
  54%{transform:translateX(-2%) rotate(-3deg)}
  60%{transform:translateX(1%) rotate(1.5deg)}
  65%{transform:rotate(-.6deg)}
}
@keyframes rsSeeGlow{
  from{filter:brightness(1) drop-shadow(0 0 .8cqw rgba(255,170,30,.7))
    drop-shadow(0 0 2cqw rgba(255,140,20,.4)) drop-shadow(0 0 4cqw rgba(255,120,10,.15))}
  to{filter:brightness(1.07) drop-shadow(0 0 1.6cqw rgba(255,180,40,1))
    drop-shadow(0 0 4cqw rgba(255,140,20,.85)) drop-shadow(0 0 8cqw rgba(255,110,10,.5))}
}
/* Reduced motion keeps the glow, standing still: light is not movement. */
@media (prefers-reduced-motion:reduce){
  .rs-see-hop{animation:none}
}

/* =================== PHOTO STRIPS (Figma 377:498 / 381:616 / 381:757) ===
   A board over the WHOLE stage, built by js/photoStrips.js from the Figma
   matrices. Each piece is three boxes, and each box owns one thing:
     .ps-piece  the rectangle at its Figma origin, UNROTATED. It owns the
                world-space hover lift, which is straight up on screen
                however the paper is turned.
     .ps-turn   the Figma rotation (and the mirror, for Party's flipped
                strips), about the origin corner, exactly as the matrix is.
     .ps-live   the reactions and the arrival, about the paper's own centre.
   Every cqw here is the board's: nothing inside it is a container, so a
   Figma px value is always n / 14.4 cqw. */
.rs-strips{display:none;position:fixed;inset:0;z-index:30;overflow:hidden;
  background:#1a3c86;--ps-x:0px;--ps-y:0px;--ps-w:100vw}
.resstage[data-strips] .rs-strips{display:block}
/* the page under it stays laid out (its fits keep measuring) but is gone:
   not painted, not clickable, not in the tab order. The descendant half is
   there because the carousel arrows carry an INLINE visibility:visible. */
.resstage[data-strips] > :not(.rs-strips),
.resstage[data-strips] > :not(.rs-strips) *{visibility:hidden!important}
/* The composition keeps the stage's own 16:9 box (photoStrips.js fitBoard
   writes --ps-x/y/w off #resStage), and nothing clips it there: the layer
   is the whole window, and the papers Figma runs off the frame edge carry
   on into whatever space the window has. */
.ps-board{position:absolute;left:var(--ps-x);top:var(--ps-y);width:var(--ps-w);
  aspect-ratio:1440/810;container-type:inline-size;overflow:visible}
.rs-strips{touch-action:manipulation}
.rs-strips.ps-pointing{cursor:pointer}
@property --ps-lift{syntax:"<number>";inherits:true;initial-value:1}
.ps-piece{position:absolute;pointer-events:none;--ps-lift:1;
  transition:translate .18s var(--rs-hover),--ps-lift .18s ease}
.ps-turn{position:absolute;inset:0;transform-origin:0 0;
  transform:rotate(var(--ps-r,0deg)) scaleY(var(--ps-f,1))}
.ps-live{position:absolute;inset:0;transform-origin:50% 50%;
  transition:scale .18s var(--rs-hover),rotate .18s var(--rs-hover),filter .18s ease}
.ps-art{position:absolute;inset:0;width:100%;height:100%;max-width:none;display:block;
  user-select:none;-webkit-user-drag:none}
/* Figma's DROP_SHADOW, y 7 / blur 4 / 35%, turned back into the piece's own
   frame by photoStrips.js so it still falls straight down on screen. On the
   ART only: a strip's frame casts it onto its own photos, as paper would. */
.ps-piece[data-shadow] .ps-art{
  filter:drop-shadow(calc(var(--sx) * var(--ps-lift)) calc(var(--sy) * var(--ps-lift))
    calc(var(--sb) * var(--ps-lift)) rgba(0,0,0,var(--sa)))}
.ps-shot{position:absolute;overflow:hidden;background:#0e1b4a;
  filter:drop-shadow(var(--px) var(--py) var(--pb) rgba(0,0,0,var(--pa)))}
.ps-shot img{width:100%;height:100%;max-width:none;display:block;
  object-fit:cover;object-position:50% 40%}
/* a mirrored strip still holds its photos the right way round */
.ps-piece[data-flip] .ps-shot img{transform:scaleX(-1)}
/* Figma authors no type on these tags, so they wear the house name tag:
   white Fredoka with a hard dark offset. Size and the .5% optical drop are
   shared with the canvas that draws the saved picture. */
.ps-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-family:var(--f-lobby);font-weight:400;line-height:1;color:#fff;
  letter-spacing:0;white-space:nowrap;translate:0 .5%;
  text-shadow:.06em .06em 0 rgba(0,0,0,.5);pointer-events:none}

/* ---- every paper answers the pointer (the board decides which one) ---- */
.ps-piece.hot{translate:0 -.3cqw;--ps-lift:1.7}
.ps-piece.hot .ps-live{filter:brightness(1.05)}
.ps-piece[data-kind="strip"].hot .ps-live{scale:1.012}
.ps-piece[data-kind="title"].hot .ps-live,
.ps-piece[data-kind="tag"].hot .ps-live{scale:1.03}
.ps-piece[data-kind="tape"].hot .ps-live{scale:1.08}
.ps-piece[data-kind="conf"].hot .ps-live{scale:1.14;rotate:9deg}

/* ---- the arrival: strips down first, then the tags and the tape that
   pins them, then the scraps. `backwards`, never `both`: every piece is
   live the moment it lands. ---- */
.rs-strips.ps-entering .ps-board{animation:psBoardIn .26s ease-out backwards}
.rs-strips.ps-entering .ps-piece[data-kind] .ps-live{
  animation:psPaperIn .44s var(--ps-in,0ms) var(--rs-pop) backwards}
.rs-strips.ps-entering .ps-piece[data-kind="strip"] .ps-live{animation-name:psStripIn}
.rs-strips.ps-entering .ps-piece[data-kind="tape"] .ps-live{
  animation:psTapeIn .36s var(--ps-in,0ms) cubic-bezier(.2,1.4,.4,1) backwards}
.rs-strips.ps-entering .ps-piece[data-kind="conf"] .ps-live{animation-name:psConfIn}
.rs-strips.ps-entering :is(.ps-back,.ps-save){
  animation:psPaperIn .4s .7s var(--rs-pop) backwards}
@keyframes psBoardIn{from{opacity:0}}
@keyframes psStripIn{from{opacity:0;translate:0 -9%;scale:1.03}55%{opacity:1}}
@keyframes psPaperIn{from{opacity:0;translate:0 -28%;scale:1.06}60%{opacity:1}}
@keyframes psTapeIn{from{opacity:0;scale:1.5}}
@keyframes psConfIn{from{opacity:0;scale:.2;rotate:-50deg}}

/* The two buttons keep the breakdown's corners and sizes in board terms,
   but sit on the WINDOW's bottom edge: where the window is taller than
   16:9 they drop into the extra felt and cover fewer photos. */
.ps-back,.ps-save{z-index:2;top:auto}
.rs-strips .ps-back{left:calc(var(--ps-x) + var(--ps-w) * .0333);
  bottom:calc(var(--ps-w) * .5625 * .0327);width:calc(var(--ps-w) * .2215)}
.rs-strips .ps-save{left:calc(var(--ps-x) + var(--ps-w) * .7479);
  bottom:calc(var(--ps-w) * .5625 * .0333);width:calc(var(--ps-w) * .2069)}
.ps-save[aria-busy="true"]{cursor:progress;filter:brightness(.94)}

@media (prefers-reduced-motion:reduce){
  .rs-strips.ps-entering :is(.ps-board,.ps-live,.ps-back,.ps-save){animation:none!important}
  .ps-live.shook,.ps-live.spun,.ps-live.popped{animation:none}
  .ps-piece,.ps-piece.hot{translate:0 0;transition:none}
  .ps-piece.hot .ps-live{scale:1;rotate:0deg}
}

/* =================== TIME'S UP (Figma "TIMES UP" 245:907) ===================
   Running out of time is the one moment the game has to be unmistakable, so
   the paper slams in, shakes, and burns red before it leaves. */
.timesup{position:fixed;left:50%;top:42%;z-index:97;pointer-events:none;
  width:min(46vw,660px);translate:-50% -50%;opacity:0;scale:.4}
.timesup img{width:100%;display:block;
  filter:drop-shadow(0 min(.6vw,1.06vh) min(.42vw,.74vh) rgba(0,0,0,.36))}
/* the red is a glow behind the paper, so the torn edge stays paper */
.timesup::before{content:"";position:absolute;inset:-52% -26%;z-index:-1;
  border-radius:50%;
  background:radial-gradient(ellipse at 50% 50%,rgba(255,86,54,.95),
    rgba(232,64,42,.72) 42%,rgba(214,40,24,.3) 66%,transparent 78%);
  filter:blur(min(1.6vw,2.9vh));opacity:0}
.timesup.fire{animation:tuIn 1.5s cubic-bezier(.16,1.4,.32,1) both}
.timesup.fire img{animation:tuShake .09s linear 6 .16s}
.timesup.fire::before{animation:tuGlow 1.5s ease-out both}
@keyframes tuIn{
  0%  {opacity:0;scale:.4;rotate:-9deg}
  12% {opacity:1;scale:1.14;rotate:2.5deg}
  26% {scale:.97;rotate:-1.2deg}
  38% {scale:1.03;rotate:.6deg}
  50% {scale:1;rotate:0deg}
  80% {opacity:1;scale:1}
  100%{opacity:0;scale:.92;translate:-50% -62%}}
/* a short hard rattle right after the slam, not a permanent wobble */
@keyframes tuShake{
  0%{translate:-2.2% 0}25%{translate:1.8% -1%}50%{translate:-1.6% .8%}
  75%{translate:2% .4%}100%{translate:0 0}}
@keyframes tuGlow{
  0%{opacity:0;scale:.7}
  14%{opacity:1;scale:1.06}
  55%{opacity:.72;scale:1}
  100%{opacity:0;scale:1.12}}
/* The red has to read out of the corner of your eye — the paper alone is easy
   to miss mid-pose. This is its own layer, not an inset shadow on the stage:
   an inset shadow paints UNDER the stage's video child and never shows. */
.timesup-flash{position:fixed;inset:0;z-index:96;pointer-events:none;opacity:0;
  background:radial-gradient(ellipse at 50% 45%,transparent 34%,
    rgba(226,48,28,.24) 66%,rgba(196,28,16,.6) 100%)}
.timesup-flash.fire{animation:tuFlash 1.15s ease-out}
@keyframes tuFlash{
  0%{opacity:0}
  12%{opacity:.92}
  46%{opacity:.5}
  70%{opacity:.8}
  100%{opacity:0}}

@media (prefers-reduced-motion: reduce){
  .timesup.fire{animation:none;opacity:1;scale:1}
  .timesup.fire img{animation:none}
  .timesup.fire::before{animation:none;opacity:.8}
  .timesup-flash.fire{animation:none;opacity:.5}
}
