/* ============================================================
   INKVILLE — battle.css
   victory & defeat receipts, the rival showstopper and the ledger panel

   Part of the game.css split. Loaded as <link> tags from index.html in
   THIS ORDER — the cascade depends on it. Do not reorder the links.
   ============================================================ */
/* ===== VICTORY / DEFEAT — the big BATTLE-START header + an old-fashioned match receipt (v3.02) ===== */
.vicWrap{position:relative; overflow:hidden; padding-top:2px;}
.vicJp{font-family:var(--fontJ); font-size:clamp(30px,11vw,50px); color:#f4c542; line-height:1;
  text-shadow:3px 3px 0 #1b1510,-3px -3px 0 #1b1510,3px -3px 0 #1b1510,-3px 3px 0 #1b1510,0 0 22px #f4c542; animation:bsSlam .5s var(--pop) both;}
.vicEn{font-family:var(--fontD); font-size:clamp(22px,8vw,38px); font-weight:bold; color:#fff; letter-spacing:2px; line-height:1; margin-top:4px;
  text-shadow:3px 3px 0 #2e9e4f,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510,4px 4px 0 #1b1510; animation:bsSlam .6s var(--pop) both;}
.defJp{font-family:var(--fontJ); font-size:clamp(28px,10vw,46px); color:#c93a22; line-height:1;
  text-shadow:3px 3px 0 #1b1510,-3px -3px 0 #1b1510,3px -3px 0 #1b1510,-3px 3px 0 #1b1510; animation:bsSlam .5s var(--pop) both;}
.defEn{font-family:var(--fontD); font-size:clamp(20px,7vw,34px); font-weight:bold; color:#fff; letter-spacing:2px; line-height:1; margin-top:4px;
  text-shadow:3px 3px 0 #c93a22,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510; animation:bsSlam .6s var(--pop) both;}
/* ⭐⭐ v0.508 — THE COMPACT VICTORY / DEFEAT BANNER, for the solo run-end cards.
   Odaroo asked for a win and a loss to ANNOUNCE themselves in every mode, solo included. The big
   .vicJp/.vicEn stack above is the match banner and it is deliberately enormous — dropping it on top
   of a solo results card, which already carries four or five figures and two buttons, ran straight
   off the bottom of the folded screen. So the solo card gets the same slam, the same colours and the
   same two words on ONE line at a fraction of the height.
   ⚠ IT IS SIZED IN px, NOT vw. Every figure under it is already vw-clamped; a second vw-scaled line
   is what tipped the 375-wide card over the edge when this was first tried. */
.endTop{display:flex; align-items:baseline; justify-content:center; gap:9px; margin:0 0 6px;
  animation:bsSlam .45s var(--pop) both;}
.endTop b{font-family:var(--fontD); font-size:19px; font-weight:bold; letter-spacing:2px; color:#fff; line-height:1;}
.endTop i{font-family:var(--fontJ); font-style:normal; font-size:22px; line-height:1;}
.endTop.win b{text-shadow:2px 2px 0 #2e9e4f,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
.endTop.win i{color:#f4c542; text-shadow:2px 2px 0 #1b1510,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
.endTop.lose b{text-shadow:2px 2px 0 #c93a22,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
.endTop.lose i{color:#c93a22; text-shadow:2px 2px 0 #1b1510,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
/* ⭐⭐ v0.534 — CURTAIN CALL: the SOLO end of a run. Not a defeat, so not red — the house gold, the
   same colour the game pays you in.
   ⚠⚠ IT IS SET SMALLER THAN THE OTHER TWO ON PURPOSE, AND THAT IS NOT A STYLE CHOICE. "CURTAIN
   CALL" is twelve characters where VICTORY is seven, and Press Start 2P advances ~1.5em per glyph
   — measured, not guessed: at 14px on one line the pair came out 420px wide inside a 291px dialog
   and ran off BOTH edges of the folded screen. So this one banner STACKS (Japanese over English,
   the way the big match banner does) and drops a size. 12 characters × 13px = 242px, which fits
   the narrowest dialog this can ever appear in with room to spare. */
.endTop.done{flex-direction:column; align-items:center; gap:3px;}
.endTop.done b{font-size:13px; letter-spacing:1px;
  text-shadow:2px 2px 0 #d9a232,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
.endTop.done i{font-size:16px; color:#f4c542; text-shadow:2px 2px 0 #1b1510,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510;}
@media (max-height:760px){ .endTop b{font-size:16px;} .endTop i{font-size:18px;} .endTop{margin-bottom:4px;}
  .endTop.done b{font-size:12px;} .endTop.done i{font-size:14px;} }

/* ⭐⭐ v0.534 — THE SOLO RUN'S OWN BILL. The wanted poster's paper, its rules and its stamp, in
   markup — so the screen the player is looking at and the picture they share are the same object.
   ⚠ IT REUSES .ovlist / .ovr / .ovk / .ovv (tetrink.css) DELIBERATELY: those are the class names
   `inkShareRead()` scrapes to build the PNG. A private set of names here would have made every
   solo card share as an empty poster. */
.runCard{background:#fbf6e6; color:#2a2018; border:2px solid #2a2018; font-family:var(--fontD);
  padding:11px 13px 13px; width:262px; max-width:92%; margin:9px auto 4px; position:relative;
  box-shadow:4px 5px 0 #0005; text-align:center;
  background-image:radial-gradient(#2a201814 1px, transparent 1px); background-size:7px 7px;}
#modalBox .runCard .rcMast{font-size:9px; letter-spacing:1px; line-height:1.5; word-break:break-word;}
#modalBox .runCard .rcSub{font-size:7px; letter-spacing:.5px; opacity:.62; margin-top:3px; line-height:1.6;}
#modalBox .runCard .rcRule{border-top:2px dashed #2a201855; margin:7px 0 5px;}
/* ⚠ THE ROWS ARE TIGHTER HERE THAN ON A BARE .ovlist, AND THAT IS MEASURED. Tetr-Ink's bill is the
   tallest one in the game — six run figures over three all-time bests — and at the shared 5px gap
   the dialog needed 23px of scroll on the folded screen. 3px gaps and 2px row padding bring the
   worst card back inside the box without shrinking a single figure. */
#modalBox .runCard .ovlist{width:100%; margin:0 auto; gap:3px;}
#modalBox .runCard .ovr{background:#2a20180c; padding:2px 6px;}
#modalBox .runCard .ovk{opacity:.7;}
#modalBox .runCard .ovv{color:#2a2018;}
#modalBox .runCard .ovv.hot{color:#a8761a;}
#modalBox .runCard .ovsep{opacity:.6; margin:8px 0 3px;}
#modalBox .runCard .ovbest .ovr{background:#2a201814;}
#modalBox .runCard .rcStamp{display:inline-block; font-size:9px; letter-spacing:2px; color:#37756d;
  border:2px solid #37756d; padding:3px 8px; margin-top:6px; transform:rotate(-4deg); opacity:.88;}
@media (max-height:760px){ .runCard{padding:9px 11px 11px; margin-top:6px;}
  .runCard .rcRule{margin:5px 0 4px;} }

.receipt{background:#fbf6e6; color:#2a2018; border:2px solid #2a2018; font-family:var(--fontD); padding:13px 15px 15px; width:236px; max-width:88%; margin:14px auto 2px; position:relative; box-shadow:4px 5px 0 #0005; text-align:center;
  /* the perforated top & bottom edges of a real till receipt */
  -webkit-mask:radial-gradient(circle 5px at 5px 0,#0000 98%,#000) repeat-x top/12px 6px, radial-gradient(circle 5px at 5px 100%,#0000 98%,#000) repeat-x bottom/12px 6px, linear-gradient(#000,#000) no-repeat center/100% calc(100% - 10px);
  mask:radial-gradient(circle 5px at 5px 0,#0000 98%,#000) repeat-x top/12px 6px, radial-gradient(circle 5px at 5px 100%,#0000 98%,#000) repeat-x bottom/12px 6px, linear-gradient(#000,#000) no-repeat center/100% calc(100% - 10px);}
.rc-top{font-size:9px; letter-spacing:1px; margin-top:4px;}
.rc-sub{font-size:7px; letter-spacing:1px; opacity:.6; margin:3px 0 2px;}
.rc-rule{border-top:2px dashed #2a201855; margin:7px 0;}
.rc-line{display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; font-size:11px; letter-spacing:1px; padding:3px 1px;}
/* ⚠ v0.508 — A RIVAL'S NAME IS AS LONG AS THEY TYPED IT. "WINNER · ODAROOOOOO" overran this line at
   BOTH folds and had done since the receipt was built — a flex row whose children cannot shrink
   simply pushes past its box. The label gives way first (it is one known word), the value shrinks
   only as far as it must, and neither may wrap. */
/* ⚠ IT WRAPS, IT DOES NOT TRUNCATE. Ellipsis was the first answer and it was the wrong one: the
   value on these lines is usually a PLAYER'S NAME, and "ODAROOOOO…" on the card somebody is about
   to post is worse than a second line. A too-long value drops underneath its own label, still
   right-aligned, and the receipt simply grows — which it has the room to do at both folds. */
.rc-line span{opacity:.8; min-width:0;}
.rc-line b{font-size:12px; min-width:0; flex:0 1 auto; text-align:right; padding-left:6px; word-break:break-word;}
.rc-stamp{display:inline-block; margin-top:8px; padding:4px 12px; font-size:12px; letter-spacing:3px; color:#2e7d46;
  border:2px solid #2e7d46; border-radius:3px; transform:rotate(-8deg); opacity:.86; box-shadow:0 0 0 2px #2e7d4633; text-shadow:0 0 1px #2e7d46;}
.rc-stamp.red{color:#c33; border-color:#c33; box-shadow:0 0 0 2px #cc3333330; text-shadow:none;}
.receipt.def{background:#efe6d2; transform:rotate(-1.4deg);}
.receipt.def::after{content:''; position:absolute; left:-2px; right:-2px; top:46%; height:0; border-top:3px dashed #2a201866; transform:rotate(2.5deg);}   /* the RIP across a broken ledger */
.receipt.def .rc-top,.receipt.def .rc-line{opacity:.9;}
/* confetti raining over the VICTORY */
.confetti{position:absolute; inset:-10px 0 auto 0; height:0; pointer-events:none; z-index:3;}
.confetti i{position:absolute; top:-14px; width:7px; height:11px; opacity:.95; animation:confFall linear forwards;}
@keyframes confFall{0%{transform:translateY(-20px) rotate(0)}100%{transform:translateY(360px) rotate(540deg); opacity:.2}}
/* ⭐⭐⭐ v0.719 — THE CELEBRATION STARTS FIRST, AND THE RESULT ARRIVES INTO IT.
   (Odaroo, 2026-08-06: "the screen should have confetti already before the actual stuff, and then
   you should see WIN. Why are you retro-looking to say you win or you lose before knowing what's
   happening? Let's revamp that everywhere.")

   This is the full-screen layer `inkCelebrate()` drops the instant a win is banked — while the
   player is still looking at the board they just won on, before any dialog exists. The receipt's
   own `.confetti` then carries the moment on inside the card, so the two read as one shower.

   ⚠⚠ IT IS NOT `.confetti`, AND IT COULD NOT BE. That one is sized for a dialog — `confFall`
   travels a flat 360px, which is a third of a folded phone and less than half an unfolded one, so
   re-using it would have rained confetti down the top third of the screen and stopped dead.
   ⚠⚠ z-index 405 CLEARS THE MODAL CURTAIN (`.modal-bg` is 400). It has to: the curtain is
   `#1b1510cc` over the whole screen, and confetti painted underneath it is confetti nobody can see
   the moment the wheel opens. Being above the card as well is deliberate — paper falls in FRONT of
   the bill — and it is `pointer-events:none`, so nothing on the card becomes unreachable.
   ⚠ It fades as it goes (`inkCeleDim`) so the figures on the bill are never competing with it for
   the eye by the time they are the thing being read.
   ⚠ position:FIXED, not absolute: it is appended to <body>, so an absolute box would resolve
   against the body's own scroll box rather than the viewport — the v0.713 toast trap exactly. */
.inkCele{position:fixed; inset:0; z-index:405; pointer-events:none; overflow:hidden;
  animation:inkCeleDim 5.2s linear forwards;}
@keyframes inkCeleDim{0%{opacity:1} 55%{opacity:1} 100%{opacity:.42}}
.inkCele i{position:absolute; top:-24px; width:9px; height:14px; opacity:.95;
  animation:inkCeleFall linear forwards;}
@keyframes inkCeleFall{
  0%{transform:translateY(-24px) rotate(0); opacity:.95}
  100%{transform:translateY(108vh) rotate(900deg); opacity:.55}}
@media (prefers-reduced-motion:reduce){ .inkCele{display:none;} }
/* ===== RIVAL SHOWSTOPPER — you WATCH the rival auto-play the minigame (v3.02) ===== */
.foeSS{position:absolute; left:50%; top:10px; transform:translateX(-50%); z-index:58; width:min(88%,300px);
  background:#141019f2; border:3px solid #ff8a76; box-shadow:5px 6px 0 #0008; padding:7px 8px 7px; text-align:center; animation:foeSSIn .3s var(--pop) both;}
/* the RIVAL-IS-PLAYING banner that rides over the auto-played minigame (hands off) */
.hpan.hclr{animation:hclrPop .3s steps(4) forwards; z-index:3;}   /* the matched trio FLASHES white and pops — the big league's clear, in miniature */
@keyframes hclrPop{0%{filter:brightness(3) saturate(0);}45%{filter:brightness(3) saturate(0); transform:scale(1.12);}100%{transform:scale(0); opacity:0;}}
/* ⭐ v0.809 — THE RED `.foe-playlbl` RIBBON IS DELETED, not merely unused. Odaroo asked for the
   CONNECTION SIGN on the rival's turn — the film reel, semi-transparent, with the gameplay showing
   through — so the sign is built with `cr-linebadge` now (foeSSOpen in js/brawl.js) and dressed by
   the twinned rules in css/crown.css.
   ⚠ IT IS REMOVED RATHER THAN LEFT LYING AROUND. A styled class nobody applies is the loaded gun
   this file warns about elsewhere: the next person to add a rival label would find it, use it, and
   quietly put a second, differently-dressed sign back on the one screen that must have exactly one.
   `.fpl-sub` survives — it is the detail line INSIDE the new sign, and is styled there. */
@keyframes foeSSIn{0%{opacity:0; transform:translateX(-50%) scale(1.22)}100%{opacity:1; transform:translateX(-50%) scale(1)}}   /* keeps the panel CENTERED throughout — plain ssIn's scale() clobbered the translateX */
.foeSS-wait{font-family:var(--fontD); font-size:15px; color:#f4c542; letter-spacing:2px; margin-bottom:5px; text-shadow:2px 2px 0 #1b1510;}
.foeSS-wait span{color:#ff8a76; animation:warnBlink 1.1s steps(2) infinite;}
.foeSS-nm{font-family:var(--fontD); font-size:10px; color:#ff8a76; letter-spacing:1px; margin-bottom:6px;}
.foeSS-play{position:relative; height:158px; background:#0c0a12; border:2px solid #3a3348; overflow:hidden;}
.foeSS-hands{font-family:var(--fontD); font-size:8px; color:#9aa0a0; letter-spacing:1px; margin-top:6px; animation:warnBlink 1s steps(2) infinite;}
.fss-a{position:absolute; font-family:var(--fontD); font-size:15px; color:#fdf6e3; transition:top .82s linear, left .82s linear, transform .3s;}
.fss-flash{position:absolute; border-radius:2px; animation:fssFlash .4s ease-out forwards;}
@keyframes fssFlash{0%{opacity:.9; transform:scale(1)}100%{opacity:0; transform:scale(1.9)}}
.fss-pop{position:absolute; font-family:var(--fontD); font-size:12px; animation:fssPop .5s ease-out forwards;}
@keyframes fssPop{0%{opacity:1; transform:translateY(0) scale(1)}100%{opacity:0; transform:translateY(-18px) scale(1.4)}}
.fss-cell{position:absolute;}
.fss-cell.gb{background:#0f380f;} .fss-cell.gbA{background:#306230;}
.fss-ship{position:absolute; bottom:6px; width:16px; height:16px; transition:left .18s;}
@keyframes readyJump{to{transform:translateY(-3px) scale(1.12)}}
.sstar{position:absolute; width:58px; height:34px; z-index:44; cursor:pointer; touch-action:manipulation;
  transform:rotate(var(--rot,0deg)); transition:opacity .25s; filter:drop-shadow(0 0 6px #ffd24a88);}   /* a SHOOTING star — it dives, it doesn't stroll */
#stage.thm-sun{background:linear-gradient(#f7c17a,#e88a5d 60%,transparent);}
#stage.thm-mid{background:linear-gradient(#2c2a4a,#5a5480 65%,transparent);}
#stage.thm-neon{background:linear-gradient(#123a3a,#1d5f57 65%,transparent);}
#stage.thm-gar{background:linear-gradient(#bfe3f7,#dff0c8 65%,transparent);}
#clouds svg{animation:cloudDrift 46s ease-in-out infinite alternate;}
@keyframes cloudDrift{to{transform:translateX(-26px)}}
#foots svg{animation:footGlow 2.9s ease-in-out infinite alternate;}
@keyframes footGlow{from{opacity:.85}to{opacity:.55}}
#ground{ position:absolute; left:0; right:0; bottom:0; height:172px; }   /* the boulevard starts right at the card dock — no squished map */
.unit.selu{outline:3px dashed var(--gold); outline-offset:2px;}
@keyframes hopRow{0%{transform:translateY(0)}40%{transform:translateY(-16px) scale(1.06)}100%{transform:translateY(0)}}
.unit.hop{animation:hopRow .32s var(--pop);}
.unit{
  position:absolute; bottom:30px; width:64px; height:74px; margin-left:-32px;
  transition:none; z-index:32; pointer-events:none; filter:drop-shadow(2px 3px 0 #1b151033);   /* fighters render ABOVE the floating deck (dock z30) but never eat its taps */
}
.unit svg{width:100%;height:78%;}
/* ⭐⭐⭐ v0.830 — THIS BAR IS THE DAMAGE READOUT NOW, SO IT HAS TO BE READABLE. (Odaroo,
   2026-08-14: "completely remove [the numbers]… I want to see that the HP bar is going down and
   he's being hit and receives the damage.") 26x6 was a garnish beside a figure that did the real
   talking; with the figures gone it is the only thing on the field saying how a fight is going, so
   it is wider, taller and drawn in the same ink as the frame around a card. */
.unit .hpb{
  position:absolute; top:-11px; left:50%; right:auto; width:38px; margin-left:-19px; height:8px;
  background:var(--cream); border:2px solid var(--ink);
  opacity:0; transition:opacity .25s;   /* the bar keeps quiet until first blood */
}
.unit.hurt .hpb, .unit.hurt .ulv{opacity:1;}
.unit .hpf{height:100%; width:100%; transform-origin:left center; background:var(--teal); transition:scale .18s ease-out;}   /* ⚠ v0.834 — see .leader .hpf */
/* ⭐ v0.830 — and it goes RED as it empties, so a piece about to fall is readable at a glance
   without counting pixels. Two thirds gone is the warning; the drain itself carries the rest. */
.unit .hpb.lowhp .hpf{background:#ff3b21;}
/* ⭐ v0.816 — the rival's own pink, the same one their hologram and their turn-sign wear
   (see `.unit.foe .holo` in css/holo.css). It was the generic red, which is also the colour of
   every damage number on the field. ⚠ This rule must stay in THIS file: holo.css loads first, so
   a copy over there would lose to whatever battle.css said last. */
.unit.foe .hpf{background:#ff5ad0;}
.unit.foe svg{transform:scaleX(-1);}
/* ⚠⚠ v0.814 — THE BACKWARDS LETTERING ON A FIELD PIECE IS NOT FIXED HERE, AND CANNOT BE.
   The rule above does not reach a piece on the field at all: `.unit .holo svg` (css/holo.css:84)
   sets `transform:none` over the top of it, deliberately, because a field piece is a HOLOGRAM
   turning on its axis and a static mirror would fight the turn. What Odaroo saw mirrored is the
   REAR HALF OF THAT TURN. The counter-flip therefore belongs to the turn, and lives beside it —
   see `holoTextFace` in css/holo.css. */
.unit .blinkf{position:absolute; top:0; left:0; width:100%; height:100%; opacity:0; pointer-events:none; animation:blinky 5s linear infinite;}
.unit .blinkf svg{position:absolute; top:0; left:0;}
@keyframes blinky{0%,94%{opacity:0} 95%,97%{opacity:1} 98%,100%{opacity:0}}

/* ⭐⭐⭐ v0.830 — THE FLINCH IS THE HIT NOW. With the figures gone (see hurt() in js/brawl.js)
   this 4px twitch was the only thing saying a blow had landed on a piece, and at fold size it was
   invisible. It recoils properly, squashes, and takes a red wash on the same class — `animation` is
   a shorthand, so a second rule would have REPLACED this one rather than adding to it, and both
   halves have to live in the one keyframe set.
   ⚠ `filter` inside the keyframes, not on the element: on the element it would sit there for the
   life of the piece and cost a re-rasterise on every frame of the turning hologram underneath. */
.unit.hitfx{animation:hitShake .3s ease-out;}
@keyframes hitShake{
  0%{transform:none; filter:none}
  14%{transform:translateX(-7px) scale(1.14,.86) rotate(-6deg); filter:brightness(2.2) saturate(.2)}
  38%{transform:translateX(5px) rotate(5deg); filter:sepia(1) saturate(7) hue-rotate(-30deg)}
  64%{transform:translateX(-3px) rotate(-2deg); filter:none}
  100%{transform:none; filter:none}}
.unit.spawn{animation:spawnIn .4s var(--pop);}
@keyframes spawnIn{from{transform:scale(0) rotate(-30deg)}to{transform:none}}
.unit .ulv{position:absolute; top:-12px; left:50%; margin-left:-19px; font-family:var(--fontD); font-size:8px; line-height:9px; color:#fff; z-index:3; pointer-events:none; opacity:0; transition:opacity .25s;
  text-shadow:1px 1px 0 #1b1510, -1px -1px 0 #1b1510, 1px -1px 0 #1b1510, -1px 1px 0 #1b1510;}   /* the bare level number, right beside the short bar — visible only once hit */
.unit.lunge{animation:lungeA .24s ease-out;}   /* the ATTACK reads — a snap toward the victim */
@keyframes lungeA{35%{transform:translateX(var(--lx,6px)) }100%{transform:none}}
.unit.kb{animation:kbA .3s ease-out;}   /* knocked back by a heavier hitter */
@keyframes kbA{40%{transform:rotate(-7deg) translateY(-3px)}100%{transform:none}}
.ddr-wrap.ddr-tall{min-height:240px; flex:1;}
.ddr-wrap.ddr-tall .ddr-field{min-height:200px;}   /* the arrows own the whole window now — the button row is gone */
.unit.die{animation:dieOut .4s ease forwards; z-index:2;}
@keyframes dieOut{to{transform:scale(0) rotate(60deg) translateY(20px); opacity:0}}
.leader{
  position:absolute; bottom:56px; width:78px; height:110px; z-index:5;   /* v0.378 — BOTH fighters READABLE on the big open field (JS re-sizes them to the measured field) */
  display:flex; flex-direction:column; align-items:center;
  filter:drop-shadow(3px 4px 0 #1b151033);
}
.leader svg{width:100%;height:100%;}
.leader .ldr-ink{width:100%; height:100%; display:flex; align-items:flex-end; justify-content:center;}
#skyline{opacity:.55;}   /* the rolling MEADOW backdrop — a random field every battle */
.leader .hpb{position:absolute; top:-14px; left:0; right:0; height:9px; background:var(--cream); border:2px solid var(--ink); display:flex;}
.leader .hpf{height:100%; width:100%; transform-origin:left center; background:var(--teal); transition:scale .2s;}   /* ⚠ v0.834 — drains on `scale`, not `width`: a flat fill, so it is pixel-identical and costs no layout */
.leader.foe .hpf{background:var(--red); margin-left:auto;}   /* the field bar too — drains left → right */
.leader.hitfx{animation:hitShake .3s ease-out;}   /* v0.830 — the same recoil the pieces take */
/* ⭐⭐⭐ v0.830 — WHAT OCTATHON SAYS, AND WHERE. See bbSay (js/brawl.js) and the redirect at the
   top of toast() (js/ui.js).
   ⚠⚠ NO `position`, NO `z-index`, NO `background` SHORTHAND, and above all NO film. It is a row in
   the flow directly under the timer/HP furniture, which is the one arrangement that cannot lay
   itself over the showstopper's instruction line — the fault this replaces. A positioned sign at
   z-index 0 would paint over every non-positioned sibling on this screen (CLAUDE.md).
   ⚠ THE ROW KEEPS ITS HEIGHT WHEN IT IS EMPTY. Collapsing it would shift the whole battlefield up
   and down every time the game said anything, mid-bout.
   ⚠ One line, clipped rather than wrapped: bbSay caps the text at 58 characters, so the ellipsis is
   a backstop for a fold narrower than any this game supports, not the normal case. This is the one
   deliberate exception to the house no-ellipsis rule (v0.746), and it is allowed because the sign
   is transient live-match chatter, not a notification the player has to be able to read in full. */
#bbSay{flex:0 0 auto; min-height:16px; margin:0; padding:0 8px; text-align:center;
  /* ⚠ `background-color`, NEVER the `background` shorthand — the shorthand resets background-image
     and this project has lost perforated strips to exactly that three times (CLAUDE.md). The band
     is the same flat ink as #battleHP above it so the two read as one piece of furniture, and flat
     so crReadable can resolve what the text is sitting on. */
  background-color:#211a14; border-bottom:2px solid #000;
  font-family:var(--fontD); font-size:clamp(9px,2.6vw,11px); letter-spacing:.6px; line-height:15px;
  color:var(--cream);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none;
  opacity:0; transition:opacity .14s;}
#bbSay.on{opacity:1; animation:bbSayIn .22s ease-out;}
/* ⚠ IT FADES; IT DOES NOT SLIDE. A `translateY(-4px)` entrance put the sign 4px INTO the HUD bar
   above it for the first fifth of a second of every message — measured, not theorised — which is
   the exact overlap this row exists to make impossible. Nothing that hangs off a bar may animate
   toward it. */
@keyframes bbSayIn{from{opacity:0} to{opacity:1}}

/* ⭐⭐⭐ v0.830 — THE HUD BAR ANSWERS EVERY BLOW. See bbBarHit (js/brawl.js). One hit moves this
   bar by a couple of pixels, which is precisely why v0.824 reached for numbers; the shake and the
   flash make that small movement impossible to miss without asking the player to read anything.
   ⚠ The transform is on the BAR, never on `#battleHP` — the row is the top furniture every pop-up
   in the game measures itself against (inkNoteTop, CLAUDE.md), and a shaking row would drag every
   notification on screen along with it. */
.bhp-bar.barhit{animation:bhpHit .4s ease-out;}
@keyframes bhpHit{
  0%{transform:none; filter:none}
  16%{transform:translateX(-3px) scaleY(1.35); filter:brightness(2.4)}
  44%{transform:translateX(3px) scaleY(1.15); filter:brightness(1.5)}
  100%{transform:none; filter:none}}
.dmgpop{
  position:absolute; z-index:20; font-family:var(--fontD); font-size:12px; color:var(--red); opacity:.8;
  text-shadow:1px 1px 0 var(--cream); pointer-events:none; animation:dmgUp .62s ease-out forwards;
}
.dmgpop.heal{color:var(--teal);}
/* ⭐⭐⭐ v0.828 — THE NAMED RED HIT ON AN INKLET. See ldrHitFX (js/brawl.js).
   ⚠ `transform:translateX(-50%)` with a FIXED width: the box is anchored by a percentage transform
   and its contents change on every hit, so an `auto` width would re-measure and slide the figure
   across the field — the same trap as the v0.447 coin toss. */
.ldrhit{position:absolute; z-index:22; width:96px; margin-left:-48px; text-align:center;
  pointer-events:none; font-family:var(--fontD); line-height:1.05;
  animation:ldrHitUp .9s ease-out forwards;}
.ldrhit i{display:block; font-style:normal; font-size:8px; color:var(--cream);
  text-shadow:1px 1px 0 var(--ink),-1px -1px 0 var(--ink),1px -1px 0 var(--ink),-1px 1px 0 var(--ink);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
/* ⚠ v0.830 — `.ldrhit b` (the red figure) is no longer BUILT. The rule is kept because nothing
   else in the file styles a <b> inside this box and a future caller would otherwise inherit the
   page default; do not read it as licence to print a number again — see ldrHitFX. */
.ldrhit b{display:none;}
/* the attacker's name is the whole sign now, so it is said properly rather than as an 8px caption */
.ldrhit i{font-size:11px; letter-spacing:.6px; color:#ff8a70;}
@keyframes ldrHitUp{from{transform:translateY(6px) scale(.55); opacity:0}
  18%{transform:translateY(0) scale(1.2); opacity:1}
  32%{transform:translateY(-2px) scale(1)}
  to{transform:translateY(-30px) scale(1); opacity:0}}
/* the inklet that took it flashes red — the eye goes to the victim, not just to the number */
.leader.redhit{animation:ldrRed .42s steps(1) 3;}
@keyframes ldrRed{0%,49%{filter:none} 50%,100%{filter:sepia(1) saturate(6) hue-rotate(-32deg) brightness(1.15)}}
@keyframes dmgUp{from{transform:translateY(0) scale(.6)} 30%{transform:translateY(-14px) scale(1.25)} to{transform:translateY(-34px) scale(1); opacity:0}}
/* ⭐⭐⭐ v0.824 — YOU CAN SEE THE SHOT NOW. (Odaroo, 2026-08-14: "when cards attack the opponent's
   inklet from across the field, there should be a visible attack animation… like archers shooting
   projectiles across the field. I want to SEE the attack happening, not just watch HP drain.")

   A ranged act used to announce a hit with `lunge` alone — a 7px twitch — and then the health bar
   moved somewhere else on the screen. Over a field the width of the stage those two events do not
   read as one thing, which is exactly the complaint: HP draining with nothing visibly causing it.
   The bolt travels the real distance between the two, in real time, and lands ON the flinch.
   ⚠⚠ IT IS A `--tx` TRANSLATION, NOT AN ANIMATED `left`. The shot crosses most of the screen, so
   animating a layout property would relayout `#units` — which holds every act, every health bar and
   every damage number — on every frame of every shot, with several in flight at once. A transform
   is composited and costs nothing. Same rule as the v0.785 transition note.
   ⚠ The travel TIME is written from JS (`--bt`) because it is derived from the distance: a bolt
   that takes as long to cross two inches as it does the whole field reads as floating, not flying. */
.bolt{position:absolute; z-index:19; width:10px; height:4px; margin:-2px 0 0 -5px; pointer-events:none;
  background:var(--cream); border:1px solid var(--ink); box-shadow:0 0 6px var(--gold);
  animation:boltFly var(--bt,.3s) linear forwards;}
.bolt.foe{background:var(--red);}
@keyframes boltFly{from{transform:translateX(0) scaleX(.7); opacity:.35}
  18%{opacity:1} to{transform:translateX(var(--tx,0)) scaleX(1.35); opacity:.9}}
.splat{position:absolute; z-index:18; width:54px; height:54px; margin:-27px; pointer-events:none; animation:splatFx .5s ease-out forwards;}
@keyframes splatFx{from{transform:scale(.3) rotate(0)} to{transform:scale(1.4) rotate(28deg); opacity:0}}
/* hand & energy */
#dock{
  /* ⭐ v0.807 — 248 IS ONLY THE FALLBACK NOW. bbFitDock() (js/brawl.js) measures the D-pad cross and
     writes a real `bottom` on every entry and every fold; this is what a harness with no pad gets. */
  position:absolute; left:0; right:0; bottom:248px; z-index:30; background:transparent; padding:0;
  pointer-events:none;   /* the deck FLOATS on the map — the energy bar runs the FULL width */
}
#dock{display:flex; flex-direction:column;}
#dock #hand{pointer-events:auto; order:1;}   /* CARTS on top, the energy bar beneath them, the D-pad below that */
#energyRow{display:flex; align-items:center; gap:8px; margin-bottom:5px; padding:0 4px;}
#ebar{flex:1; height:18px; border:2px solid var(--paper); background:#3a322a; position:relative; overflow:hidden;}
/* ⚠⚠⚠ v0.834 — `width:100%` AND CLIPPED, NOT `width:0%` AND GROWN. updateEnergy() empties this bar
   with `clip-path` now (a 90deg gradient must not be squashed by a scale), and a clip-path over a
   ZERO-WIDTH box shows nothing at all — the tank would simply never appear. The starting state is
   supplied here as a full-width bar clipped fully away. */
#efill{height:100%; width:100%; clip-path:inset(0 100% 0 0); background:linear-gradient(90deg,#5fa8d6,#9fd2ef); box-shadow:0 0 10px #7fd4ff88;}   /* BLUE energy flows forward */
#eticks{position:absolute; inset:0; display:flex; pointer-events:none;}
#eticks i{flex:1; border-right:1px solid #1b1510aa;}
#eticks i:last-child{border-right:0;}
#enum{color:var(--gold); font-family:var(--fontD); font-size:17px; min-width:22px; text-align:center;}
#superBtn{
  width:56px;height:56px;border-radius:50%;border:3px solid var(--paper);
  background:conic-gradient(var(--gold) calc(var(--sq)*1%), #3a322a 0);
  color:var(--cream); font-family:var(--fontD); font-size:18px; cursor:pointer; flex:none;
  display:flex;align-items:center;justify-content:center; position:relative;
}
#superBtn.charging{animation:ssBreath var(--breath,1.6s) ease-in-out infinite;}   /* breathes, faster as it charges */
@keyframes ssBreath{0%,100%{transform:scale(1); filter:none}50%{transform:scale(1.16); filter:brightness(1.3) drop-shadow(0 0 8px #f4c542aa)}}
#superBtn.ready::before{content:''; position:absolute; inset:-10px; border-radius:50%; border:3px solid var(--gold);
  animation:readyRing .8s ease-out infinite; pointer-events:none;}
@keyframes readyRing{from{transform:scale(.6); opacity:1}to{transform:scale(1.75); opacity:0}}
#superBtn.ready{font-size:9px; letter-spacing:-.5px; animation:pulse .45s infinite, readyBlink .4s steps(2) infinite; background:var(--gold); color:var(--ink); box-shadow:0 0 20px #f4c542;}
@keyframes readyBlink{0%{box-shadow:0 0 14px #d9a232}50%{box-shadow:0 0 30px #fff,0 0 12px #f4c542}}
#superBtn:not(.ready):not(.charging){opacity:.85;}   /* still tappable when dark — the 10-tap cheat listens */
/* ---- cinematic SHOWSTOPPER (freeze + on-screen damage + slow drain) ---- */
#stage.ssSlow{filter:saturate(1.35) contrast(1.06) brightness(.92);}
#stage.ssSlow #ground,#stage.ssSlow #skyline,#stage.ssSlow #clouds{filter:blur(1px);}
.ssOverlay{position:fixed; inset:0; z-index:9990;   /* ALWAYS on top — never behind the card hand */ display:flex; flex-direction:column; align-items:center; justify-content:flex-start; padding-top:100px; text-align:center; pointer-events:none;
  background:radial-gradient(circle at 50% 22%, #1b151000 30%, #1b1510cc 100%);}   /* pinned to the TOP under the versus bar — the message + damage never clip below the cards */
.ss-jp{font-family:var(--fontJ); font-size:clamp(22px,8vw,34px); color:#f4c542; text-shadow:2px 2px 0 #1b1510,0 0 16px #f4c542; animation:ssIn .4s var(--pop) both;}
.ss-en{font-family:var(--fontD); font-size:clamp(13px,5vw,20px); color:#fff; letter-spacing:1px; text-shadow:2px 2px 0 #c93a22; animation:ssIn .5s var(--pop) both;}
.ss-dmg{font-family:var(--fontD); font-size:clamp(32px,12vw,58px); color:#fff; text-shadow:3px 3px 0 #1b1510,0 0 20px #f4c542; margin-top:8px; animation:ssDmgPop .5s var(--pop) both;}
.ss-pct{font-family:var(--fontD); font-size:11px; color:#f4c542; opacity:.85; margin-top:4px;}
@keyframes ssIn{0%{opacity:0; transform:scale(2.3) rotate(-8deg)}100%{opacity:1; transform:scale(1) rotate(0)}}
/* ⭐ v3.88 — THE ONE TRUE GAME SPLASH. Every minigame opens with the same card: JP name on top, EN name
   under it, dead centre. It drops in from the top, settles in the middle, then continues down toward the
   game HUD and melts away. NO background, NO box — nothing to leave a square edge as it fades. */
/* ============================================================
   ⭐ v0.462 — 3 · 2 · 1 · GO! The curtain-raiser on a real-time online match, so both players start
   on the same beat with empty boards. Sits above everything, takes no taps.
   ⚠ pointer-events:none is not optional — the count runs over a live playfield whose controls are
   already listening, and a full-screen layer that ate taps would swallow the first move.
   ============================================================ */
.inkcount{position:fixed; inset:0; z-index:9992; display:flex; align-items:center; justify-content:center;
  flex-direction:column; gap:10px; pointer-events:none; background:#1b151044;}
/* ⚠⚠ v0.786 REVERSED v0.779: the .ic-hint line under the count ("GET READY TO PRESS A") is gone at
   Odaroo's request. The count shows the digits and nothing else — do not re-add a hint rule here. */
.inkcount .ic-n{font-family:var(--fontD); font-size:clamp(74px, 26vw, 190px); line-height:1;
  color:var(--cream); text-shadow:4px 5px 0 #1b1510, 0 0 26px #1b151088; letter-spacing:2px;}
.inkcount .ic-n.go{color:#f4c542; font-size:clamp(52px, 18vw, 132px);}
.inkcount .ic-n.pop{animation:icPop .5s cubic-bezier(.2,.9,.3,1) both;}
@keyframes icPop{0%{transform:scale(.5); opacity:0} 28%{transform:scale(1.12); opacity:1}
  60%{transform:scale(1)} 100%{transform:scale(.94); opacity:.75}}

/* ⭐⭐⭐ v0.606 — THE SPLASH IS A BLACK SCREEN, AND THE NAME SITS AT THE TOP OF IT.
   (Odaroo: "a splash screen with a complete black, and then it goes away … always centered at the
   TOP, not the middle … the checkered flags right next to the letters, waving like wind.")
   Three things changed and each of them is load-bearing:

   1. ⚠⚠ THE CARD CARRIES ITS OWN BLACK. It used to be `background:none`, riding on the separate
      `.inkcurtain` (z9985). The curtain is still there and still right, but a card that is itself
      black cannot ever be caught announcing a game over a half-drawn playfield, whatever order the
      two layers happen to arrive in.
   2. ⚠⚠ THE BLACK MAY NOT RIDE. The overlay is what fades; the WORDS are what travel. Animating the
      overlay's own transform (which is what `gsplashRide` used to do) would drag a full-screen black
      rectangle down the screen and expose a bright strip along the top edge as it went. So the
      overlay animates `opacity` only and the inner `.gs-card` does the falling.
   3. ⚠ TOP-CENTRED IS NOW THE ONLY POSITION. There is no middle variant left. `gs-top` is kept as a
      harmless alias so the handful of callers that pass 'top' (Superstars, Crown, the rink) keep
      working and get exactly the same card as everyone else. */
/* ⭐⭐⭐ v0.640 — AND NOW IT IS DEAD CENTRE AGAIN, BIG, AND GOLD. (Odaroo: "game title centered on
   screen, dead center, BIG, like it's announcing a new game. Shiny gold text.") Point 3 above is
   therefore reversed: `justify-content:center` and no top padding. `gs-top` stays a harmless alias —
   there is still only ONE position, it is just the middle one now. Points 1 and 2 are untouched and
   still load-bearing. */
.gsplash{position:fixed; left:0; right:0; top:0; bottom:0; z-index:9990; display:flex; flex-direction:column;
  align-items:center; justify-content:center; text-align:center; pointer-events:none;
  background:#000; padding:0 0 calc(env(safe-area-inset-bottom,0px)); box-sizing:border-box;
  animation:gsplashFade 2.05s ease-out both;}
.gsplash.gs-top{}                       /* the old middle/top split is retired — every card is top-centred */
@keyframes gsplashFade{
  0%{opacity:0;} 10%{opacity:1;} 78%{opacity:1;} 100%{opacity:0;}}
/* the words drop in from above, hold, and lift away — the black stays perfectly still behind them */
.gsplash .gs-card{animation:gsplashRide 2.05s cubic-bezier(.25,.6,.35,1) both; width:100%;}
@keyframes gsplashRide{
  0%{transform:translateY(-26vh);}
  16%{transform:translateY(0);}
  74%{transform:translateY(0);}
  100%{transform:translateY(-9vh);}}
/* ⭐⭐ v0.606 — THE FLAGS FLANK THE LETTERS, NOT THE BLOCK.
   They used to sit in a row beside the WHOLE words column (English + Japanese), so they centred
   themselves against the gap between the two lines and read as floating off on their own. They are
   now inside the English line's own row, and — this is the part that keeps them there at every size
   — **they are sized in `em`, off that line's font-size**. A flag is therefore always the same
   height as the letters it stands next to, on a folded phone and on an unfolded one alike, with no
   second breakpoint to drift out of step. The Japanese line sits underneath the whole arrangement.
   ⚠⚠ THE TITLE IS ONE LINE, AND THAT IS WHY THE FLAGS STAY BESIDE IT. A wrapping title is a flex
   item that grows to the full width of the row, so DANCE DANCE INK stacked into three lines and
   pushed both flags out to the screen edges — the exact "they feel a little bit off" Odaroo
   described. `white-space:nowrap` keeps the name on one line and `gsSplashFit()` (js/minigames.js)
   shrinks the row's font-size by measurement until the whole flag·NAME·flag arrangement fits.
   The em-sized flags come down with it, so the proportion never changes. */
/* ⭐⭐ v0.640 — THE NAME IS THE WHOLE ROW, AND THE WINGS SIT BEHIND IT.
   ⚠⚠ THE ROW IS `inline-flex`, SO IT IS EXACTLY AS WIDE AS THE NAME. That is what lets the wings —
   which are absolutely positioned to `inset:0` — span the title from left to right without anybody
   measuring anything: a full-width row would have thrown them out to the screen edges, which is the
   fault the v0.606 flags had.
   ⚠ The title is still ONE line (`nowrap`) and still fitted by measurement in `gsSplashFit()`;
   a wrapping title would change the row's width and take the wings with it. */
.gsplash .gs-row{position:relative; display:inline-flex; align-items:center; justify-content:center;
  max-width:100%; padding:0 8px; box-sizing:border-box;
  font-size:clamp(30px,12vw,72px);}
.gsplash .gs-wings{position:absolute; left:0; right:0; top:50%; width:100%; height:1.5em;
  transform:translateY(-50%); opacity:.5; pointer-events:none; z-index:0;
  shape-rendering:crispEdges; filter:drop-shadow(0 0 6px #f4c54255);
  animation:gsWingBeat 2.6s ease-in-out infinite;}
@keyframes gsWingBeat{0%,100%{transform:translateY(-50%) scaleY(1);} 50%{transform:translateY(-50%) scaleY(1.08);}}
/* ⭐⭐ SHINY GOLD. The fill is a moving gradient clipped to the letters; the black rim comes from
   `-webkit-text-stroke` + `paint-order:stroke`, because a `text-shadow` cannot draw an outline on
   text whose own colour is transparent (which is what background-clip:text requires). */
.gsplash .gs-en{position:relative; z-index:1;
  font-family:var(--fontD); font-size:1em; font-weight:bold; letter-spacing:.05em;
  line-height:1.1; white-space:nowrap; flex:0 0 auto;
  background:linear-gradient(100deg,#8a5c12 0%,#f4c542 18%,#fff6c8 32%,#f4c542 46%,#c9922a 62%,#f4c542 78%,#fff6c8 90%,#f4c542 100%);
  background-size:260% 100%; -webkit-background-clip:text; background-clip:text;
  color:transparent; -webkit-text-fill-color:transparent;
  -webkit-text-stroke:.055em #1b1510; paint-order:stroke fill;
  filter:drop-shadow(3px 4px 0 #1b1510) drop-shadow(0 0 16px #f4c54277);
  animation:gsGoldShine 3.2s linear infinite;}
@keyframes gsGoldShine{0%{background-position:180% 0;} 100%{background-position:-80% 0;}}
.gsplash .gs-jp{font-family:var(--fontJ); font-size:clamp(15px,5.4vw,30px); color:#f4c542; margin-top:10px;
  line-height:1.2; overflow-wrap:anywhere; padding:0 8px;
  text-shadow:2px 2px 0 #1b1510,-2px -2px 0 #1b1510,2px -2px 0 #1b1510,-2px 2px 0 #1b1510,0 0 20px #f4c542;}
/* ⭐⭐⭐ v0.640 — THE CROSSING FLAGS. Two poles crossed as an X under the words.
   ⚠ The pair is a fixed-size box so nothing here depends on what the words say (the v0.447 rule).
   ⚠ The cloth hangs from the TOP of each rotated pole and is clipped frame by frame; `steps(1,end)`
   is what makes it ten POSES rather than one long smooth slide. */
/* ⚠ 240px, not 190px: hanging the cloths OUTWARD (v0.656) throws each one ~110px off centre
   (58px of pole lean + ~51px of rotated cloth), where hanging them inward kept them over the middle. */
.gsplash .gs-flags{position:relative; width:240px; max-width:96vw; height:104px; margin:20px auto 0; flex:0 0 auto;}
/* ⭐⭐⭐ v0.705 — THEY ACTUALLY CROSS NOW. (Odaroo: "the sticks are side by side — they need to
   intersect and form an X, the classic racing crossed-flags look.")
   Both poles used to pivot on the SAME bottom point at left:50%, which is a V, not an X: two sticks
   leaving one spot and splaying apart. A real X needs the butts APART and the tops swapped over —
   the pole whose flag flies LEFT has its butt on the RIGHT.
   ⚠ THE 29px IS GEOMETRY, NOT TASTE. For a pole of length L leaning by A, crossing dead centre
   means each butt sits (L/2)·sin(A) off centre: 52 × sin34° = 29.1. Change the angle or the pole
   length and this number has to be recomputed, or the X drifts off the middle. */
.gsplash .gs-xf{position:absolute; left:50%; bottom:0; width:6px; height:104px; transform-origin:bottom center;}
.gsplash .gs-xf.l{transform:translateX(calc(-50% + 29px)) rotate(-34deg);}
.gsplash .gs-xf.r{transform:translateX(calc(-50% - 29px)) rotate(34deg);}
.gsplash .gs-pole{position:absolute; inset:0; background:linear-gradient(90deg,#6b4a22,#c89a55 45%,#6b4a22);
  border:2px solid #1b1510; box-sizing:border-box; border-radius:1px;}
.gsplash .gs-cloth{position:absolute; top:2px; width:62px; height:44px;
  background:repeating-conic-gradient(#fff 0 25%,#1b1018 0 50%) 0 0/ 15.5px 11px;
  border:2px solid #1b1510; box-sizing:border-box; box-shadow:2px 3px 0 #0007;
  animation:gsCloth10 1.1s steps(1,end) infinite;}
/* ⚠⚠ v0.656 — THE FLAGS FACE OUTWARD, LIKE CROSSED AXES (Odaroo). They used to hang INWARD off
   their poles, so the two cloths pointed at each other over the middle of the card. Each cloth now
   hangs on the OUTER side of its own pole.
   ⚠ The wave keyframes nail the hoist edge at x=0, so the left flag — whose pole is now on its
   RIGHT — must be mirrored with scaleX(-1), or the cloth would ripple where it is nailed and stay
   stiff where it is free. */
.gsplash .gs-xf.l .gs-cloth{right:4px; transform:scaleX(-1);}
.gsplash .gs-xf.r .gs-cloth{left:4px; animation-delay:.55s;}
/* the ten frames — the free edge ripples up and down while the hoist edge stays nailed to the pole */
@keyframes gsCloth10{
  0%  {clip-path:polygon(0 0,100% 6%,100% 88%,0 94%);}
  10% {clip-path:polygon(0 0,100% 0,100% 82%,0 94%);}
  20% {clip-path:polygon(0 0,100% 8%,100% 92%,0 94%);}
  30% {clip-path:polygon(0 0,100% 16%,100% 100%,0 94%);}
  40% {clip-path:polygon(0 0,100% 20%,100% 98%,0 94%);}
  50% {clip-path:polygon(0 0,100% 14%,100% 90%,0 94%);}
  60% {clip-path:polygon(0 0,100% 6%,100% 80%,0 94%);}
  70% {clip-path:polygon(0 0,100% 0,100% 74%,0 94%);}
  80% {clip-path:polygon(0 0,100% 4%,100% 80%,0 94%);}
  90% {clip-path:polygon(0 0,100% 10%,100% 86%,0 94%);}
  100%{clip-path:polygon(0 0,100% 6%,100% 88%,0 94%);}}
/* ⭐ v0.606 — THE WIND. `nimFlagWave` was referenced by these flags for eight builds and its
   @keyframes only ever existed in the pre-split monolith, so the flags have been standing dead
   still this whole time — which is exactly the "off" Odaroo was seeing. This is the real thing: the
   cloth pivots on the pole it hangs from, so it skews and foreshortens rather than sliding sideways.
   (`nimFlagWave` is restored just below, because home.css's Bullet Ballet flags still ask for it.) */
@keyframes gsFlagWave{
  0%{transform:skewY(0deg) scaleX(1);}
  25%{transform:skewY(-6deg) scaleX(.9);}
  50%{transform:skewY(0deg) scaleX(1.02);}
  75%{transform:skewY(6deg) scaleX(.9);}
  100%{transform:skewY(0deg) scaleX(1);}}
@keyframes nimFlagWave{to{transform:skewY(-4deg) scaleX(.94);}}
@keyframes ssDmgPop{0%{opacity:0; transform:scale(0)}60%{transform:scale(1.28)}100%{opacity:1; transform:scale(1)}}
.leader.ssZoom{animation:ssZoomA 1.4s ease-in-out; z-index:20;}
@keyframes ssZoomA{0%{transform:scale(1)}28%{transform:scale(1.4)}55%{transform:scale(1.32) translateX(-5px)}100%{transform:scale(1)}}
/* big comic HIT burst on the boss when the showstopper damage lands */
.bossImpact{position:absolute; z-index:58; width:104px; height:104px; margin:-52px 0 0 -52px; pointer-events:none; display:flex; align-items:center; justify-content:center; animation:biPop .8s ease-out forwards;}   /* smaller — a smack, not an eclipse */
.bossImpact svg{position:absolute; inset:0; width:100%; height:100%; filter:drop-shadow(0 0 12px #f4c54299);}
.bossImpact .bi-txt{position:relative; font-family:var(--fontD); font-size:12px; line-height:1.15; color:#fff; text-align:center; text-shadow:2px 2px 0 #c93a22, 0 0 6px #000; transform:rotate(-7deg);}
.bossImpact .bi-txt b{display:block; font-size:17px; color:#ffe08a;}
@keyframes biPop{0%{transform:scale(.15) rotate(-40deg); opacity:0}22%{transform:scale(1.3) rotate(8deg); opacity:1}55%{transform:scale(1) rotate(-3deg); opacity:1}100%{transform:scale(1.12) rotate(0); opacity:0}}
#stage.ssShake{animation:ssShakeA .42s steps(2) 2;}
@keyframes ssShakeA{0%{transform:translate(0,0)}25%{transform:translate(-6px,3px)}50%{transform:translate(5px,-3px)}75%{transform:translate(-4px,2px)}100%{transform:translate(0,0)}}
/* ---------------- showstopper QTE ---------------- */
#qte{position:absolute; left:0; right:0; top:84px; bottom:230px; z-index:40; background:#141019e6;   /* NO BOX — the whole stage between the reel and the pad IS the game */
  border:none; box-shadow:none; backdrop-filter:blur(1px);
  display:none; flex-direction:column; align-items:center; justify-content:center;
  /* ⚠⚠⚠ v0.735 — SAFE centring. A centred flex column that overflows is clipped at BOTH ends and
     cannot be scrolled back to (the overflow spills past the scroll origin) — which is exactly how
     the Ice-Ink showstopper came out "cut and shown in the back". `safe center` falls back to
     flex-start the moment the content is too tall, so nothing is ever unreachable. The boards are
     measured to fit as well (ssBoardRoom); this is the belt to that brace. */
  justify-content:safe center;
  gap:7px; padding:14px 8px 10px;
  overflow-y:auto; scrollbar-width:none;}
#qte::-webkit-scrollbar{width:0; height:0;}
#qte.on{display:flex; animation:popIn .25s var(--pop);}
/* ⭐ v0.809 — THE 56px IS GONE. It was room for a ribbon that sat INSIDE this box; the rival's sign
   is a floating film reel now (the connection sign, see css/crown.css), so the padding was holding
   the watcher's board 56px shorter than the player's board of the same game. `overflow:hidden`
   stays: nothing may scroll during their turn. */
#qte.foeplay{overflow:hidden;}
/* ⭐⭐⭐ v0.827 — THE RIVAL'S BOARD HAS TO LOOK LIKE THE RIVAL'S. (Odaroo, 2026-08-14: "I'm still
   not able to see the opponent play their showstoppers. I should be able to see it in real time,
   then the battlefield stops.")
   MEASURED FIRST, and the run itself was already there: tools/probe-octa-live.mjs plays a real
   bout and the rival takes the stage 3x in 67s, `#qte` really carries `.on .foeplay`, the board is
   really dealt and really played by foeAIStart, and a screenshot at 416x657 shows Ice-Ink running
   under a "RIVAL'S TURN" line. So this is NOT a missing feature — it is a board that is pixel-for-
   pixel identical to your own, announced by a 9px line, which is a rival's turn you can look
   straight at without registering as one.
   ⚠ IT IS A TINT AND A RING, NOT AN OVERLAY. `inset` box-shadow and a background change only —
   nothing positioned, nothing over the playfield. A positioned sign at z-index 0 paints over every
   non-positioned sibling (CLAUDE.md), and on a showstopper that sibling is the board being watched.
   ⚠ The colour is #ff8ae0, the same rival pink `.unit.foe` and `.ss-foeline b` already wear. */
#qte.foeplay{background:#1a0f1ae8; box-shadow:inset 0 0 0 3px #ff8ae0; animation:foeStageGlow 1.6s ease-in-out infinite;}
@keyframes foeStageGlow{50%{box-shadow:inset 0 0 0 3px #ff8ae066;}}
@media (prefers-reduced-motion:reduce){#qte.foeplay{animation:none;}}
/* ⭐⭐⭐ v0.828 — THE BATTLEFIELD VISIBLY STOPS UNDER A SHOWSTOPPER. See bbStageFreeze (js/brawl.js).
   ⚠⚠ `#qte` AND THE HUD ARE EXEMPT. The board is the thing being accentuated, and the health bars
   and the clock are how you read what the showstopper is doing to the match — greying those would
   hide the only numbers worth watching. So the filter is on the FIELD's own boxes, never on
   `#scr-battle` itself: a filter on the ancestor establishes a containing block and would drag the
   board, the pad and every fixed pop-up down into it with the field.
   ⚠ No `transition` on the filter — it is applied and removed on a frame boundary either side of a
   full main-thread stall (the mini's first deal), and a half-finished fade there reads as a hitch. */
#scr-battle.ssfreeze #stage,
#scr-battle.ssfreeze #units,
#scr-battle.ssfreeze #dock{filter:grayscale(.85) brightness(.42) blur(1px); pointer-events:none;}
#qte.min{max-height:28px; overflow:hidden; opacity:.7; padding:3px 8px; gap:0;}
#qteMin{position:absolute; top:47px; right:7%; z-index:41; display:none; width:28px; height:22px;
  background:var(--gold); color:var(--ink); border:2px solid var(--ink); font-family:var(--fontD);
  font-size:12px; cursor:pointer; align-items:center; justify-content:center; padding:0;}
#qteMin.on{display:flex;}
.qkeys{display:flex; gap:8px;}
.qkey{width:42px;height:42px;border:3px solid var(--paper); background:var(--ink); color:var(--cream);
  font-family:var(--fontD); font-size:18px; display:flex;align-items:center;justify-content:center;}
.qkey.done{background:var(--teal); border-color:var(--teal);}
.qkey.cur{background:var(--gold); color:var(--ink); animation:pulse .5s infinite;}
.qpad{display:grid; grid-template-columns:repeat(4,56px); gap:8px;}
.qbtn{height:46px; border:3px solid var(--paper); background:var(--red); color:var(--cream);
  font-family:var(--fontD); font-size:17px; cursor:pointer;}
.qbtn:active{transform:scale(.88); background:var(--gold); color:var(--ink);}
#mashBtn{width:128px;height:128px;border-radius:50%;background:var(--red);border:5px solid var(--paper);
  color:var(--cream);font-family:var(--fontD);font-size:36px;cursor:pointer; box-shadow:0 0 24px #c93a2299;}
#mashBtn:active{transform:scale(.9); background:var(--gold); color:var(--ink);}
#qteTimer{position:absolute; top:0; left:0; right:0; width:100%!important; height:6px; border:none; background:#00000066;}   /* the clock is ONE magical line across the very top */
#qteTimerF{height:100%; background:linear-gradient(90deg,#ff5a5a,#ffd24a,#6ae06a,#4ad7ff,#b06af0,#ff5a5a); background-size:200% 100%; animation:rgbLine 2.6s linear infinite; transition:width .1s linear;}
@keyframes rgbLine{to{background-position:200% 0}}
.die3d{animation:dieTumble .5s linear infinite;}   /* the bones TUMBLE in 3D until they land */
@keyframes dieTumble{0%{transform:rotate(0) scale(1)}25%{transform:rotate(90deg) scale(1.25) translateY(-8px)}50%{transform:rotate(180deg) scale(.9)}75%{transform:rotate(270deg) scale(1.2) translateY(-5px)}100%{transform:rotate(360deg) scale(1)}}
.bmShot{position:absolute; width:4px; height:10px; background:#ffdd55; margin-left:-2px; image-rendering:pixelated;}
.bmFoe{position:absolute; width:14px; height:14px; background:#c93a22; border:2px solid #1b1510; margin-left:-7px; image-rendering:pixelated;}
.qte-title{font-size:21px!important; letter-spacing:1px; font-family:var(--fontD); color:var(--gold); font-size:23px; letter-spacing:2px; text-align:center;}
.qte-sub{font-size:13px!important; line-height:1.6; color:var(--paper2); font-size:14px; letter-spacing:1px; text-align:center;}
/* ---------------- compact collection ---------------- */
.grid.compact{grid-template-columns:repeat(4,1fr); gap:8px;}
.grid.compact .card .stats,.grid.compact .card .rar{display:none;}
.grid.compact .card .nm{font-size:12px; min-height:16px; padding:2px 1px;}
.grid.compact .cost{width:auto; height:auto; font-size:14px;}   /* never a fixed box — 2E and 3E always fit whole */
.grid.compact .lvl{font-size:12px; padding:1px 4px;}
.grid.compact .cnt{bottom:18px; font-size:12px;}
.grid.compact .dbar{height:11px; margin:2px 4px 0;}
.grid.compact .dbar>b{font-size:9px;}
/* ---------------- loud upgrade indicator ---------------- */
.card.canup{box-shadow:0 0 0 3px var(--gold), 4px 4px 0 #1b151088;}
/* ---------------- fliers ---------------- */

/* ---------------- live combo panel + arcade ---------------- */
#qte .qkey{width:34px;height:34px;font-size:19px;}
#qte .qpad{grid-template-columns:repeat(4,46px); gap:6px;}
#qte .qbtn{height:38px;font-size:18px;}
#qte .qte-title{font-size:16px;} #qte .qte-sub{font-size:12px;}
#qte #qteTimer{width:85%;}
.lr-row{display:flex; gap:10px; width:100%; justify-content:center;}
.lrbtn{flex:1; max-width:150px; height:60px; border-radius:12px; background:var(--red); border:4px solid var(--paper);
  color:var(--cream); font-family:var(--fontD); font-size:28px; cursor:pointer;}
.lrbtn:active{transform:scale(.92); background:var(--gold); color:var(--ink);}
.simpad{border-radius:10px;}
/* ---------------- ghost placement preview ---------------- */
/* v0.421 — .bad = you are holding the card over the enemy's half, where it will not be accepted */
#ghost.bad{filter:drop-shadow(0 0 8px #c93a22) grayscale(.7) !important; opacity:.34;}
#ghost{position:absolute; z-index:30; pointer-events:none; opacity:.5; width:44px;
  display:none; transform:translateX(-50%) scale(.92); filter:drop-shadow(0 0 8px var(--gold));}   /* dragging = the SAME art, shrunken + half-ghosted; the real character pops on deploy */
#ghost.on{display:block;}
/* ---------------- gallery view ---------------- */
.grid.gallery{display:grid; grid-template-columns:repeat(auto-fill,minmax(146px,1fr)); gap:10px; padding:8px 12px; align-items:start;}   /* the GALLERY packs the wall — many cards at once */
.grid.gallery .card{width:100%;}   /* explicit width — grid-stretch + aspect-ratio deadlocked to 6px without it */
.gcard{min-height:auto; display:flex; flex-direction:column;
  align-items:center; justify-content:flex-start; gap:4px; background:var(--cream);
  border:3px solid var(--ink); box-shadow:4px 4px 0 var(--ink); margin:0;
  max-width:none; padding:10px 8px 12px; text-align:center; cursor:pointer;}
.gcard .gart{width:96px; height:96px;}
.statgrid{display:flex; flex-direction:column; gap:5px; max-width:236px; margin:8px auto;}
.statrow{display:flex; align-items:center; gap:6px; font-size:13px;}
.statrow .statlb{width:46px; text-align:left; font-family:var(--fontD);}
.statrow .statval{flex:1; text-align:left;}
.gcard .gart svg{width:100%; height:100%;}
.gcard.r4{border-color:var(--gold2); box-shadow:5px 5px 0 var(--gold2), 0 0 18px #d9a23255;}
.gcard.locked .gart svg{filter:grayscale(1) opacity(.35);}
/* ---------------- deck bar ---------------- */
#deckBar{position:relative; z-index:6;}
.dslot.big{width:60px; height:78px;}   /* the OCTAVE cards, featured big up top */
@media (max-width:430px){ .dslot.big{width:min(38px,calc((100vw - 78px)/8)); height:52px;} .dslot.big .dcost{font-size:8px;} }   /* v3.93 — all EIGHT deck slots in ONE row on the folded phone */
.dslot.big .dcost{font-size:11px;}
.lvlupCard{animation:lvlPop .5s var(--pop) backwards;}
@keyframes lvlPop{0%{transform:scale(.2) rotate(-8deg); opacity:0}70%{transform:scale(1.15)}100%{transform:scale(1); opacity:1}}
.dslot{width:34px; height:46px; border:2px dashed var(--ink); background:var(--paper2);
  position:relative; cursor:pointer; flex:none;}
.dslot.filled{border-style:solid; background:var(--cream); box-shadow:2px 2px 0 #1b151066;}
.gcard,.card[data-cardid],.dslot.filled{cursor:grab;}
.dragghost{position:fixed; z-index:300; width:60px; height:60px; margin:-30px 0 0 -30px; pointer-events:none;
  filter:drop-shadow(2px 4px 3px rgba(0,0,0,.55)); transform:scale(1.12); opacity:.92;}
.dragghost svg{width:100%; height:100%;}
#deckBar.droptarget{outline:3px dashed var(--teal); outline-offset:-2px; background:#a8cdc433;}
.dslot.slotover{outline:3px solid var(--gold2); outline-offset:1px; background:#ffe9b055; transform:scale(1.12); z-index:3;}
.dslot svg{width:100%; height:74%;}
.dcost{position:absolute; bottom:0; right:1px; font-size:12px; font-weight:bold;}
/* a benched/replaced card flung off the bill (#10b) */
.tosscard{position:fixed; z-index:120; pointer-events:none; animation:tossFly .64s ease-in forwards; filter:drop-shadow(2px 4px 3px #0007);}
.tosscard svg{width:100%; height:100%;}
@keyframes tossFly{0%{transform:none; opacity:1}100%{transform:translate(46px,-150px) rotate(74deg) scale(.45); opacity:0}}
/* ---------------- DDR showstopper ---------------- */
.ddr-wrap{position:relative; width:100%; max-width:300px; height:168px; margin:0 auto;}
.ddr-targets{position:absolute; top:0; left:0; right:0; display:flex; z-index:2; pointer-events:none;}
.ddr-t{flex:1; text-align:center; font-family:var(--fontD); font-size:19px; color:var(--gold);
  border:2px dashed var(--gold); border-radius:8px; margin:0 3px; padding:1px 0; opacity:.85;}
.ddr-field{position:absolute; inset:0; overflow:hidden; background:#171310;
  background-image:repeating-linear-gradient(90deg, transparent 0 24.4%, #fdf6e318 24.4% 25.6%);}
.ddr-note{position:absolute; transform:translateX(-50%); font-family:var(--fontD); font-size:22px; color:var(--cream);
  text-shadow:2px 2px 0 #000;}
.ddr-note.nhit{color:var(--gold); transform:translateX(-50%) scale(1.7); opacity:0; transition:all .25s;}
.ddr-note.nmiss{color:var(--red); opacity:0; transition:all .25s;}
.ddr-judge{position:absolute; top:15%; transform:translateX(-50%); font-family:var(--fontD); font-size:15px;
  animation:dmgUp .45s ease-out forwards; text-shadow:1px 1px 0 #000;}
.jp{color:var(--gold);} .jg{color:#7fd4c4;} .jm{color:var(--red);}
.ddr-btn{flex:1; max-width:70px; height:56px; border-radius:12px; background:#2a2320; border:3px solid var(--paper);
  color:var(--cream); font-family:var(--fontD); font-size:24px; cursor:pointer; display:flex; align-items:center; justify-content:center;
  box-shadow:inset 0 0 10px #0007, 0 3px 0 #0006;}
.ddr-btn.lane0{border-color:#c93a22;} .ddr-btn.lane1{border-color:#37a58e;} .ddr-btn.lane2{border-color:#d9a232;} .ddr-btn.lane3{border-color:#4a6fa5;}
.ddr-btn:active{transform:translateY(2px) scale(.94); background:var(--gold); box-shadow:inset 0 0 14px #fff6;}
.ddr-btn.lit{background:var(--gold); box-shadow:0 0 14px var(--gold);}
/* 8-bit two-frame life */
svg g.f1{animation:pxf1 .55s steps(1) infinite;}
svg g.f2{opacity:0; animation:pxf2 .55s steps(1) infinite;}
@keyframes pxf1{0%,49%{opacity:1}50%,100%{opacity:0}}
@keyframes pxf2{0%,49%{opacity:0}50%,100%{opacity:1}}
/* ---------------- stage layers ---------------- */
.slayer{position:absolute; left:-6%; right:-6%; pointer-events:none;}
#clouds{top:6%; height:32%; z-index:2; animation:driftA 46s ease-in-out infinite alternate;}
#skyline{bottom:172px; height:40%; z-index:3; animation:driftB 64s ease-in-out infinite alternate;}
#foots{bottom:168px; height:56px; z-index:4;}
#curtain{display:none;}   /* the red valance is retired — the stage breathes */
#arenaZone{display:none;}   /* the floor is PURE ART now — the field only shows itself while you hold a card */
@keyframes driftA{from{transform:translateX(-2.5%)}to{transform:translateX(2.5%)}}
@keyframes driftB{from{transform:translateX(-1.2%)}to{transform:translateX(1.2%)}}
#stage.night .sunburst{filter:brightness(.6) saturate(.65);}
#stage.night #ground{filter:brightness(.8);}
.balloon{position:absolute; z-index:45; width:64px; height:84px; cursor:pointer;
  animation:balloonFloat linear forwards; filter:drop-shadow(2px 3px 0 #1b151033);}   /* BIGGER, and ABOVE the field — never hides behind the cards */
.balloon .bwink{position:absolute; left:52%; top:16%; width:12%; height:9%; background:#c93a22; border:1px solid #1b1510; opacity:0; animation:bwinkA 1.7s steps(1) infinite;}   /* the right eye WINKS */
@keyframes bwinkA{0%,72%{opacity:0}76%,90%{opacity:1}94%,100%{opacity:0}}
.balloon:hover{transform:scale(1.1);}
@keyframes balloonFloat{from{left:-10%; bottom:44%} 50%{bottom:62%} to{left:106%; bottom:40%}}
#hand{display:flex; gap:7px; justify-content:center; padding:0 12px;}   /* v0.378 — a gutter each side so the last card can lift/scale without running off the fold */
#hand .hcard{
  width:17.5%; max-width:74px; position:relative;
  transition:transform .18s var(--pop); cursor:grab;
}
#hand .hcard .card:hover{transform:translateY(-12px) scale(1.1) rotate(-2deg);}
#hand .hcard.nope{opacity:.55;}
#hand .hcard.nope .card:hover{transform:none;}
#hand .hcard.sel .card{transform:translateY(-16px) scale(1.12) rotate(-3deg); filter:drop-shadow(0 0 5px var(--gold)) drop-shadow(0 0 3px var(--gold)) drop-shadow(4px 5px 0 #000a);}
#battleTop{
  display:flex; justify-content:space-between; align-items:center; z-index:10;
  background:var(--ink); color:var(--cream); padding:7px 12px; font-size:15px;
  position:relative;
}
/* battle HP HUD — mini avatar + 3-segment (tower-style) bar per side, right under the top bar */
#battleHP{display:flex; justify-content:space-between; align-items:center; gap:5px; padding:4px 8px 5px; background:#211a14; position:relative; z-index:9; border-bottom:2px solid #000;}   /* v0.378 — a FLAT dark bar, not a gradient: crReadable can only resolve a flat colour, so the gradient made the cups/LV line untestable */
.bhp-side{display:flex; align-items:center; gap:5px; flex:1; min-width:0;}
.bhp-side.foe{justify-content:flex-end;}
.bhp-col{flex:1; min-width:0; display:flex; flex-direction:column; gap:2px;}
.bhp-cups{display:flex; align-items:center; gap:3px; font-family:var(--fontD); font-size:8px; color:var(--cream); opacity:.85;}
.bhp-cups svg{width:11px; height:11px; flex:none;}
.bhp-side.foe .bhp-cups{justify-content:flex-end;}
/* ===== FEEDBACK LEDGER (pause menu) — the making-of, in the cartridge itself ===== */
/* ===== THE LEDGER SCREEN — movie credits: white on black, a film-reel tab bar, a moonlit 8-bit beach ===== */
#scr-ledger{background:#0a0a0d; color:#f2ead6;}
#ledgReel{position:absolute; top:0; left:0; right:0; height:72px; background:#16161c; z-index:6;
  border-bottom:3px solid #f2ead6; display:flex; align-items:center; justify-content:center; gap:8px; padding:0 10px;
  box-sizing:border-box;}
/* ⭐⭐⭐ v0.734 — THE CREDITS REEL IS THE SAME FILM AS EVERY OTHER REEL. (Odaroo, 2026-08-06: one
   uniform film reel size, the same hole sizes, the same white pixels.) It was the odd one out by
   the widest margin in the game: a 9px DARK hole on a 28px period in a 10px band — twice everyone
   else's period, punched the wrong way round (ink holes through a grey bar instead of white holes
   through black film). It now takes the shared big gauge like the HUD reel, the connecting banner
   and the notification strip. ⚠ The holes are drawn against #16161c, so they must be --film-hole
   (white); the old inverted look was what made this screen read as a different production. */
#ledgReel::before,#ledgReel::after{content:''; position:absolute; left:0; right:0; height:var(--film2-h);
  background-image:linear-gradient(90deg,
    transparent 0 calc((var(--film2-per) - var(--film2-w))/2),
    var(--film-hole) calc((var(--film2-per) - var(--film2-w))/2) calc((var(--film2-per) + var(--film2-w))/2),
    transparent calc((var(--film2-per) + var(--film2-w))/2) var(--film2-per));
  background-size:var(--film2-per) var(--film2-h); background-position:center; background-repeat:repeat-x;
  animation:hudFilmRoll 1.6s linear infinite; will-change:background-position;}
#ledgReel::before{top:3px;} #ledgReel::after{bottom:3px;}
.ledgTab{font-family:var(--fontD); font-size:9.5px; letter-spacing:.5px; padding:9px 10px; cursor:pointer; white-space:nowrap;
  background:none; border:2px solid transparent; color:#8a8a96;}
.ledgTab.on{color:#fff; border-color:#f2ead6; background:#ffffff12; box-shadow:0 0 10px #ffffff33;}
/* v3.86 — the build count rides just under the reel, always visible (it was buried behind the pad) */
#ledgCount{position:absolute; top:72px; left:0; right:0; min-height:22px; z-index:6; text-align:center;
  font-family:var(--fontD); font-size:8px; letter-spacing:1px; color:#f2ead6aa; line-height:1.7; padding:4px 10px;
  box-sizing:border-box; background:#0a0a0d; border-bottom:1px solid #f2ead622;}   /* v3.105 — auto-height: the longer credit wraps to two lines on the fold */
#ledgScroll{position:absolute; top:96px; bottom:212px; left:0; right:0; overflow-y:auto; z-index:5;
  padding:12px max(6%,14px) 24px; -webkit-overflow-scrolling:touch; scrollbar-width:none;}
#ledgScroll::-webkit-scrollbar{display:none;}
@media (max-width:560px){ #ledgScroll{top:118px;} }   /* v3.105 — clears the two-line count bar on the fold */
#ledgScroll .ledg-era{color:#c9b06a; background:#ffffff0d; border-left-color:#c9b06a;}
/* v3.92 — the hero tally atop the BUILDS list */
#ledgScroll .ledg-hero{max-width:620px; margin:0 auto 10px; text-align:center; padding:10px 8px 12px; border-bottom:2px solid #f4c54233;}
#ledgScroll .ledg-hero b{display:block; font-family:var(--fontD); font-size:13px; letter-spacing:1px; color:#f4c542; text-shadow:0 0 12px #f4c54255;}
#ledgScroll .ledg-hero i{display:block; font-style:italic; font-size:11.5px; color:#c9c2b4; margin-top:6px; line-height:1.6;}
#ledgScroll .ledg-hero .lh-by{display:block; text-decoration:none; font-family:var(--fontD); font-size:9px; letter-spacing:1px; color:#e0a34a; margin-top:5px;}   /* v3.105 — the byline under the tally */
/* v3.86 — BUILDS back at its original small, readable size; each entry carries its ship date */
#ledgScroll .ledg-row{border-bottom-color:#f2ead622; display:block; font-size:11.5px; line-height:1.55;
  max-width:620px; margin:0 auto; padding:7px 4px 8px;}
#ledgScroll .ledg-row b{color:#e0a34a; display:block; font-size:9px; letter-spacing:1px; margin-bottom:2px; padding-top:0;}
#ledgScroll .ledg-row b .ledg-date{float:right; color:#8a8a96; font-size:8px; letter-spacing:.5px; font-weight:normal;}
#ledgScroll .ledg-row:first-child{background:#f4c5420f;}   /* the newest build gets a quiet spotlight */
#ledgScroll .ledg-row:first-child b{color:#f4c542;}
#ledgScroll .ledg-story{max-width:520px; margin:0 auto; font-size:10px; line-height:1.7;}   /* v0.839 — the credits read smaller: straight documentation, not a poster */
#ledgScroll .ledg-story p{margin:0 0 10px;}
#ledgScroll .ledg-story b{color:#e0a34a;}
/* — the night beach: dark sky (the screen), moonlit sea rolling in, pale sand — no music, just the surf */
#ledgFoot{position:absolute; left:0; right:0; bottom:0; height:212px; z-index:5; overflow:hidden;}
.lb-sea{position:absolute; left:0; right:0; height:34px; image-rendering:pixelated;}
.lb-sea.s1{bottom:104px; background:repeating-linear-gradient(90deg,#1d3a52 0 18px,#27506e 18px 36px); animation:lbRoll 7s linear infinite;}
.lb-sea.s2{bottom:76px; height:28px; background:repeating-linear-gradient(90deg,#27506e 0 14px,#356a8e 14px 28px); animation:lbRoll 4.6s linear infinite reverse;}
@keyframes lbRoll{from{background-position:0 0}to{background-position:36px 0}}
.lb-foam{position:absolute; left:0; right:0; bottom:70px; height:6px;
  background:repeating-linear-gradient(90deg,#e8f4ff 0 10px,transparent 10px 26px);
  animation:lbFoam 3.4s ease-in-out infinite;}
@keyframes lbFoam{0%,100%{transform:translateY(0); opacity:.85}50%{transform:translateY(5px); opacity:.4}}
.lb-sand{position:absolute; left:0; right:0; bottom:0; height:70px; background:#8a7a58;
  border-top:3px solid #b7a67c;}
/* ⭐ v0.839 — A MINIATURE. Odaroo asked for the inklet on the sand to be the smallest version of
   itself: one little figure on a night beach, not a mascot posing. ⚠ It is still the ten-tap door to
   WEDNESDAY JAMS, so it may not shrink below a usable tap target — 44px is the floor. */
#ledgInk{position:absolute; left:50%; bottom:70px; transform:translateX(-50%); width:44px; height:50px; z-index:7;
  display:flex; align-items:center; justify-content:center;
  filter:drop-shadow(0 0 7px #ffffff33);}
/* ⭐ v0.878 — HALF THE DRAWING, THE SAME DOOR. (Odaroo, 2026-08-30: the mark at the foot of the
   credits is twice as small.) ⚠⚠ THE BOX ABOVE IS DELIBERATELY NOT SHRUNK. It is the ten-tap way
   into WEDNESDAY JAMS and the v0.839 note directly above it records 44px as the tap-target FLOOR,
   so shrinking the box would have quietly broken an easter egg to satisfy a size request. The
   PICTURE halves inside a box that still takes a thumb. */
#ledgInk svg{width:50%; height:50%; display:block; margin:auto;}
#ledgBack{position:absolute; right:10px; bottom:14px; z-index:9;}
#ledgInk{cursor:pointer;}   /* tap her ten times… */
.ledgTab.magic{color:#f4c542; border-color:#f4c54255;}
.ledgTab.magic.on{color:#fff; border-color:#f4c542; box-shadow:0 0 12px #f4c54277; background:#f4c5421a;}
#ledgScroll .mk-h{font-family:var(--fontD); font-size:9px; letter-spacing:1px; color:#e0a34a; margin:14px 0 5px; text-shadow:0 0 8px #e0a34a44;}
#ledgScroll .mk-h:first-child{margin-top:2px;}
#ledgScroll .ledg-story .mk-sign{opacity:.7; font-style:italic; margin-top:16px;}
/* ============================================================
   ⭐ v0.437 — THE TWO ICONS ON A WIN, AND A DIALOG THAT FITS THE FOLDED PHONE.
   ------------------------------------------------------------
   The icons sit BESIDE the dialog's own continue button, never instead of it: the way out of a
   victory screen is navigation and must always be the obvious thing on it. They are square, small
   and quiet, so they read as tools rather than as another decision.
   ============================================================ */
.inkico{flex:0 0 auto; width:34px; height:34px; padding:0; display:inline-flex; align-items:center; justify-content:center;
  background:var(--paper2,#f2e5c9); color:#1b1510; border:2px solid #1b1510; border-radius:3px;
  box-shadow:0 3px 0 #1b1510; cursor:pointer; transition:transform .08s ease, box-shadow .08s ease;}
.inkico svg{width:19px; height:19px; display:block;}
.inkico:active{transform:translateY(3px); box-shadow:0 0 0 #1b1510;}
/* ⭐ v0.508 — SHARE CARRIES ITS WORD. The download stays a bare arrow (the secondary action); the
   share button widens and says so, because it is the one people are looking for. ⚠ min-height 34px
   keeps it the same height as the icon beside it, and the row it sits in already wraps, so a folded
   screen drops it onto its own line rather than pushing anything off the edge. */
.inkico.wide{width:auto; min-width:96px; gap:7px; padding:0 12px; height:34px;}
.inkico.wide span{font-family:'Press Start 2P',monospace; font-size:10px; letter-spacing:1px; line-height:1;}
.inkico:focus-visible{outline:3px solid var(--gold,#d9a232); outline-offset:2px;}

/* ⚠ THE WHOLE DIALOG MUST FIT THE WINDOW ON THE FOLDED PHONE. Odaroo photographed the stage-clear
   box with BACK TO THE MENU hanging off the bottom edge: the unfolded screen has room for the full
   celebration and the folded one does not, and the box was drawn at the same size on both. Nothing
   is removed — the art, the receipt and every figure are all still there — it is simply tightened
   on a short screen so the whole thing, top slam to bottom button, lands inside the frame.
   Keyed off HEIGHT, never width: it is vertical room that runs out. */
@media (max-height:860px){
  .vicJp{font-size:clamp(22px,8vw,34px);}
  .vicEn{font-size:clamp(17px,6vw,26px); margin-top:2px;}
  .defJp{font-size:clamp(20px,7.5vw,32px);}
  .receipt{padding:9px 12px 11px; margin:9px auto 2px;}
  .receipt .rc-line{padding:1px 0;}
  .vicWrap .confetti{opacity:.75;}
}
@media (max-height:760px){
  .vicJp{font-size:clamp(19px,7vw,28px);}
  .vicEn{font-size:clamp(15px,5.4vw,22px);}
  .receipt{padding:7px 11px 9px; margin:7px auto 2px; font-size:11px;}
}

/* ⭐ v0.491 — THE CURTAIN THE ONLINE SPLASH PLAYS OVER. Plain black, sitting directly under the
   splash card (9990) and over everything else, so the game is genuinely not visible behind the
   announcement — and any last settling of the playfield happens where nobody can see it.
   ⚠ It fades OUT only. Fading it in would show the very frame it exists to hide. */
.inkcurtain{position:fixed; inset:0; z-index:9985; background:#000; pointer-events:none;}
.inkcurtain.lift{opacity:0; transition:opacity .3s ease-out;}

/* ⭐⭐ v0.522 — THE FORFEIT RECEIPT. Walking out of an online match is a defeat, and it now says so
   on the same 敗北 / DEFEAT card every other loss ends on, with the cups it cost printed on it.
   ⚠ IT IS A FIXED OVERLAY, ON PURPOSE. The match screen behind it has already been torn down and the
   player is on their way to the boulevard, so the card must not depend on any screen being there —
   it belongs to the journey, not to a room. It sits above the pad (which stays visible and dimmed,
   per the connecting-lock rule) and scrolls rather than clips on the folded phone. */
.netFF{position:fixed; inset:0; z-index:9200; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:10px; padding:18px 14px calc(18px + env(safe-area-inset-bottom));
  background:rgba(8,6,14,.92); overflow-y:auto; animation:netFFin .22s ease-out both;
  /* ⚠⚠ text-align IS LOAD-BEARING HERE, and the pre-flight is what found it. Every other place this
     receipt appears is inside a `.center` box, so .defJp / .defEn / the receipt lines inherit their
     centring from a parent this overlay does not have — measured left-aligned against the edge of
     the screen, 敗北 sitting in the corner. A component that only looks right inside one particular
     parent has to bring that parent's rule with it. */
  text-align:center;}
.netFF .vicWrap{max-width:min(94vw,352px); width:100%;}
.netFF .netFFgo{max-width:min(88vw,300px); width:100%; flex:0 0 auto;}
@keyframes netFFin{from{opacity:0} to{opacity:1}}

/* ============================================================
   ⭐⭐⭐ v0.541 — THE ALBUM PAGE. Every ending in the game — victory, defeat and the solo curtain
   call — is now one object: `.runCard.passport`, built by `inkBillCard()` in js/share.js.

   ⚠⚠ RULE 3 IS THE ONE THAT KEEPS BREAKING ITSELF: THE VALUE IS THE SAME FONT AND SIZE AS ITS
   LABEL. Two inherited rules had to be undone for that — tetrink.css sets `.ovv` to 13px against an
   8px `.ovk`, and `.ovv.hot` to 15px on top of it. Both are restated here at ONE size. If a figure
   ever starts shouting at its own label again, look for a third rule further down a later file.
   ⚠ THE ROW IS CENTRED, so it can no longer be the 1fr/auto grid tetrink.css gives it — a grid puts
   the label hard left however you align the text inside it. It is a centred flex line.
   ============================================================ */
/* ⚠⚠⚠ v0.568 — THE STUB IS MEASURED AGAINST ITS OWN BOX, NEVER AGAINST THE VIEWPORT.
   Odaroo photographed the bill hanging over the edge of the dialog frame, and this line was why:
   `84vw` is 84% of the WHOLE SCREEN, and the card does not live on the whole screen — it lives
   inside #modalBox, which is inset by the backdrop's 20px of side padding, the modal's own 4px
   border and its 18px of padding. On the folded 375px Fold that leaves 291px of usable column and
   the rule asked for min(292, 315) = 292. One pixel over on paper, more once the drop shadow is
   counted — and visibly over the frame, which is what the photograph shows.
   `max-width:none` made it worse by throwing away `.runCard`'s own 92% guard, so there was nothing
   left to catch it.
   ⚠ 100% here means the CONTAINER, which is the whole point — it cannot be wrong on any screen,
   any fold, or inside the wider ≥640px modal. Do not put a vw or a vh back into this rule. */
.runCard.passport{width:min(292px,100%); max-width:100%; padding:10px 12px 12px;}
#modalBox .runCard.passport .ovlist{width:100%; gap:1px;}
#modalBox .runCard.passport .ovr{display:flex; justify-content:center; align-items:baseline;
  gap:7px; text-align:center; background:#2a20180c; padding:2px 6px;}
#modalBox .runCard.passport .ovk,
#modalBox .runCard.passport .ovv,
#modalBox .runCard.passport .ovv.hot{font-family:var(--fontD); font-size:8px; letter-spacing:1px;
  line-height:1.5; text-align:center; white-space:nowrap; min-width:0; word-break:normal;}
#modalBox .runCard.passport .ovk{opacity:.62; color:#2a2018;}
#modalBox .runCard.passport .ovv{opacity:1; color:#2a2018;}
#modalBox .runCard.passport .ovv.hot{color:#7a4f0c;}
#modalBox .runCard.passport .ovbest .ovr{background:#2a201814;}

/* ============================================================
   ⭐⭐⭐ v0.549 — THE ENDING IS A RIPPABLE TICKET STUB.
   Odaroo asked for the big moment to look like a tall boarding-pass / passport stub you could tear
   off: perforated top and bottom, a dashed tear line at the foot, and the whole thing read
   top-to-bottom. The paper, the speckle and the dashed rules are the bill's own from v0.541; the
   notches and the stub foot are new.
   ⚠⚠ THE NOTCHES ARE A MASK, AND A MASK CLIPS THE DROP SHADOW. That is why `.ticket` restates
   box-shadow as `none` and hangs its depth on a drawn edge instead — leaving the v0.541 shadow on
   gave a hard rectangular ghost sitting behind a scalloped card, which read as a rendering fault.
   ⚠ ONE MASK, LISTED TOP THEN BOTTOM THEN BODY. Every one of the three layers has to be named in
   both the `-webkit-` and the plain property or Safari drops the whole card to invisible.
   ============================================================ */
.runCard.passport.ticket{box-shadow:none;
  -webkit-mask:radial-gradient(circle 5px at 5px 0,#0000 98%,#000) repeat-x top/13px 6px,
    radial-gradient(circle 5px at 5px 100%,#0000 98%,#000) repeat-x bottom/13px 6px,
    linear-gradient(#000,#000) no-repeat center/100% calc(100% - 11px);
  mask:radial-gradient(circle 5px at 5px 0,#0000 98%,#000) repeat-x top/13px 6px,
    radial-gradient(circle 5px at 5px 100%,#0000 98%,#000) repeat-x bottom/13px 6px,
    linear-gradient(#000,#000) no-repeat center/100% calc(100% - 11px);
  padding:15px 12px 14px;}
/* the tear-off line along the foot of the stub — a perforation, so it is DRAWN, not typed.
   ⚠ IT WAS A ROW OF "·" CHARACTERS AND THAT WAS A PRE-FLIGHT FAULT. A fixed run of dots is either
   short of the card or wider than it, and at 270px it measured 261 of ink in 246 of box — the exact
   `scrollWidth > clientWidth` clip the golden-rule sweep fails a build for. A gradient fits any
   width by construction and can never overrun. */
.runCard.passport .rcTear{height:2px; margin-top:6px; opacity:.42;
  background:repeating-linear-gradient(to right,#2a2018 0 4px,transparent 4px 9px);}

/* ---- the two faces -------------------------------------------------------
   ⚠⚠ THE MARKUP IS PLAYER-THEN-RIVAL AND THE CLASS DECIDES THE SIZE. `shFoeName()` (js/share.js)
   names the rival by reading the SECOND `.rcSealNm` on the card, so the winner cannot be moved to
   the front of the DOM to make it big — it is placed absolutely off its `won` / `lost` class. */
.runCard.passport .rcSeals{position:relative; display:flex; justify-content:center;
  align-items:flex-end; margin:0 0 7px;}
/* ⭐⭐⭐ v0.761 — THE SEAL IS JUST THE FRAME AND THE INKLET. NO DANCE (Odaroo, 2026-08-07:
   "there shouldn't be a special animation… simply put the frame"). The `rcDance` bounce that used
   to run here is gone: the drop is the same portrait it is on every other screen in the game, and
   a bill is a printed ticket, not a stage. `rcShook` on the mini seal goes with it below. */
.runCard.passport .rcSeals.duo{display:block; height:126px; margin:0 0 8px;}
.runCard.passport .rcSeals.duo .rcSeal{position:absolute; display:flex; flex-direction:column;
  align-items:center; gap:2px; min-width:0;}
/* the winner: one huge circle, centred, bouncing */
.runCard.passport .rcSeals.duo .rcSeal.won{left:50%; top:0; transform:translateX(-60%); z-index:2;}
.runCard.passport .rcSeals.duo .rcSeal.won .rcSealArt{width:96px; height:96px;}
.runCard.passport .rcSeals.duo .rcSeal.won .rcSealNm{font-size:8px; letter-spacing:1px;
  max-width:150px; color:#1c6b35;}
/* the loser: a mini circle bumped out to the bottom-right of the winner's, wobbling in shock */
.runCard.passport .rcSeals.duo .rcSeal.lost{right:8px; bottom:0; z-index:3; gap:1px;}
.runCard.passport .rcSeals.duo .rcSeal.lost .rcSealArt{width:44px; height:44px;}
/* ⚠ THE MINI NAME IS ALLOWED TO BE WIDER THAN ITS OWN CIRCLE. A 14-character name is 91px of Press
   Start 2P at 6px and the circle is 45px — capped to the circle it ellipsised "ODAROOOOOOOOOO" away,
   which the pre-flight fails as a clip. 92px is the widest that still clears the card's own edge
   with the seal pinned 8px in. */
.runCard.passport .rcSeals.duo .rcSeal.lost .rcSealNm{font-size:6px; max-width:92px; color:#a33a24;}
.runCard.passport .rcSeal{display:flex; flex-direction:column; align-items:center; gap:2px; min-width:0;}
/* ⚠⚠ v0.553 — TWO ELEMENTS, AND THE SPLIT IS LOAD-BEARING. `.rcSealArt` is the frame's stage: it
   must stay `overflow:visible` because every frame in the game sits OUTSIDE the portrait it rings.
   `.rcSealDisc` inside it is the thing that clips the inkling into a circle. Merge them back and
   the ring is cropped away by the clip meant for the sprite. */
.runCard.passport .rcSealArt{width:62px; height:62px; position:relative; overflow:visible;
  border-radius:50%; flex:0 0 auto;}
/* ⭐⭐⭐ v0.761 — NOTHING CLIPS THE INKLET ANY MORE (Odaroo, 2026-08-07: "look at how the Inklid is
   cut… now it feels like it's a circle, and the avatar is cropped inside. There's a layering issue
   going on here — just include the frame and the Inklid as usual").
   WHAT WAS HERE: an opaque paper disc with its own 3px ring and an inset highlight, `overflow:hidden`
   so it clipped the sprite into a circle, and the sprite blown up to 112% inside it. That was three
   rings deep — the disc's border, the disc's inset highlight, and the real frame ring outside it —
   and the 112% guaranteed the mascot's sides were shaved off by the clip. The frame the player owns
   is the ONLY ring on this card now, exactly as it is everywhere else in the game.
   ⚠⚠ IT STAYS A SEPARATE ELEMENT FROM `.rcSealArt`. That split is still load-bearing (see the note
      above): the frames are absolutely-positioned siblings sized OUTSIDE the portrait, so the thing
      the sprite is sized against must not be the thing the frames are positioned against.
   ⚠ THE CONFETTI JUSTIFICATION FOR THE OPAQUE FILL IS DEAD. v0.579 removed the burst from this card
     entirely ("static in the saved image cannot be true of a looping animation"), so the fill was
     paying for a thing that no longer exists. Do not bring back one without the other. */
.runCard.passport .rcSealDisc{position:absolute; inset:0; display:flex; align-items:center;
  justify-content:center; overflow:visible; background:none; border:0;
  border-radius:50%; box-shadow:none;}
.runCard.passport .rcSeal.solo .rcSealArt{width:78px; height:78px;}
/* ⚠⚠ SCOPED TO THE DISC, NEVER TO .rcSealArt. The frame rings are svg TOO and they are children
   of .rcSealArt — sized 112% with object-fit they lose their own geometry (.fr-pixring is 108% and
   transform-centred) and the ring lands off the portrait. Only the inkling lives in the disc. */
/* ⚠ v0.761 — 78%, NOT 112%. The 112% was the OVERFILL that made the old clip read as a portrait
   pressed into a coin; with nothing clipping it, an oversized sprite simply hangs out past its own
   frame. 78% is the same room every other framed inklet in the game is given (the .75 in
   css/profile.css), so the seal is now the identical portrait, not a second version of it. */
.runCard.passport .rcSealDisc svg,
.runCard.passport .rcSealDisc img{width:78%; height:78%; object-fit:contain; image-rendering:pixelated;}
/* ⚠ the seal is a PRINT, not a costume change — the ink keeps most of its colour and the paper
   takes the rest, the same trade `SH_TINT` makes on the PNG. */
.runCard.passport .rcSealArt{filter:saturate(.68) contrast(1.08);}
.runCard.passport .rcSeal.lost .rcSealArt{opacity:.72; filter:saturate(.3) contrast(1.05);}
.runCard.passport .rcSealTag{font-family:var(--fontD); font-size:6px; letter-spacing:1px;
  padding:1px 4px; border:1px solid #2a2018; opacity:.72;}
.runCard.passport .rcSealNm{font-family:var(--fontD); font-size:6px; letter-spacing:.5px;
  max-width:106px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}

/* ---- THE WINNER'S NAME, AS BIG AS THE HEADER ABOVE IT (v0.550) ----
   Odaroo asked for the winner's username to be the hero of the screen at the size of the VICTORY /
   DEFEAT banner, and for the green WIN / red LOSE stamps to go entirely. The stamps are deleted;
   `.rcVerdict` no longer exists in the markup.
   ⚠ THE SIZE IS SET INLINE BY `inkBillCard()`, NOT HERE. CSS cannot count a name's letters, and a
   flat 19px runs a 14-character name off both edges of the folded stub. Do not add a font-size to
   this rule — it would win on specificity and undo the fit.
   ⚠ AND IT IS DARK INK ON PAPER, NOT THE BANNER'S WHITE. The header is white outlined in green over
   the game's artwork; the stub is cream paper, where white is invisible. */
.runCard.passport .rcHero{font-family:var(--fontD); color:#2a2018; letter-spacing:2px;
  line-height:1.25; margin:0 0 6px; word-break:normal; white-space:nowrap; overflow:hidden;
  text-shadow:1px 1px 0 #2a201826;}
/* the winner's own seal keeps its name element for `shFoeName()` to read, but the hero line above
   is what the player sees — printing it twice on one card is the row we just deleted, in pictures. */
/* ⚠⚠ v0.587 — SCOPED WITH `>` SO IT CANNOT REACH THE RIVAL'S CREDIT. This hides the name that rides
   under the player's own centred figure (the hero line above already says it). The rival's name now
   has to be VISIBLE, and it lives one level deeper, inside `.rcFoeNm` — an unscoped descendant
   selector here swallowed it and no amount of specificity further down the file could be trusted to
   win back a `display:none` this general. The child combinator makes it structurally impossible. */
.runCard.passport .rcSeals.duo > .rcSeal.won .rcSealNm,
.runCard.passport .rcSeals.one > .rcSeal .rcSealNm{display:none;}

/* ---- the burst around the winner ----------------------------------------
   ⚠ IT IS `pointer-events:none` AND SITS UNDER THE FACES. A scrap that lands on top of the inkling
   at 18 particles reads as damage on the sprite, not as confetti. */
.runCard.passport .rcConf{position:absolute; left:50%; top:52px; width:0; height:0; z-index:1;
  pointer-events:none;}
.runCard.passport .rcConf i{position:absolute; left:-3px; top:-5px; width:6px; height:9px;
  animation:rcBurst linear infinite both;}
@keyframes rcBurst{
  0%{transform:translate(0,0) rotate(0deg) scale(.4); opacity:0}
  18%{opacity:1}
  100%{transform:translate(var(--cx),var(--cy)) rotate(var(--cr)) scale(1); opacity:0}}
@keyframes rcDance{
  from{transform:translateY(3px) rotate(-4deg)}
  to{transform:translateY(-5px) rotate(4deg)}}
@keyframes rcShook{
  0%,100%{transform:translate(0,0) rotate(8deg)}
  25%{transform:translate(-2px,1px) rotate(-6deg)}
  50%{transform:translate(2px,2px) rotate(10deg)}
  75%{transform:translate(-1px,0) rotate(-3deg)}}

/* the way back in — printed on every ending, so a screenshot advertises itself.
   ⚠ THE THREE LINES ARE ONE SIZE ON PURPOSE (Odaroo's rule): only the colour separates them. */
.runCard.passport .inkPromo{display:flex; flex-direction:column; align-items:center; gap:4px;
  margin-top:9px; padding-top:8px; border-top:2px dashed #2a201855; text-align:center;}
.runCard.passport .ipBrand{display:flex; align-items:center; justify-content:center; gap:5px;}
.runCard.passport .ipIco{width:16px; height:16px; image-rendering:pixelated; flex:0 0 auto;}
.runCard.passport .ipBrand b,
.runCard.passport .ipLine{font-family:var(--fontD); font-size:7px; font-weight:normal;
  letter-spacing:.5px; line-height:1.6; white-space:normal; word-break:break-word;}
.runCard.passport .ipBrand b{color:#7a4f0c; letter-spacing:1.5px; white-space:nowrap;}
.runCard.passport .ipLine{color:#2a2018; opacity:.72; max-width:100%;}
.runCard.passport .ipUrl{color:#1f5a53; opacity:1; white-space:nowrap;}
.runCard.passport .ipQR{width:62px; height:62px; flex:0 0 auto; image-rendering:pixelated;
  border:2px solid #2a2018; background:#fff;}
.runCard.passport .rcStamp.red{color:#a33a24; border-color:#a33a24;}

/* folded, and on any short screen: the page keeps every part, it just packs tighter */
@media (max-width:430px), (max-height:790px){
  .runCard.passport{width:min(270px,86vw); padding:8px 10px 10px;}
  .runCard.passport.ticket{padding:13px 10px 12px;}
  .runCard.passport .rcSeals{margin-bottom:5px;}
  .runCard.passport .rcSealArt{width:52px; height:52px;}
  .runCard.passport .rcSeal.solo .rcSealArt{width:70px; height:70px;}
  /* the folded stub: the winner stays big — it is the whole point of the screen — and the row it
     lives in comes down with it so the figures below still fit above the D-pad. */
  .runCard.passport .rcSeals.duo{height:112px;}
  .runCard.passport .rcSeals.duo .rcSeal.won .rcSealArt{width:86px; height:86px;}
  .runCard.passport .rcSeals.duo .rcSeal.lost .rcSealArt{width:40px; height:40px;}
  .runCard.passport .rcHero{margin-bottom:5px;}
  .runCard.passport .rcConf{top:48px;}
  .runCard.passport .inkPromo{margin-top:7px; padding-top:6px; gap:7px;}
  .runCard.passport .ipQR{width:54px; height:54px;}
}

/* ============================================================
   ⭐⭐⭐ v0.579 — THE BILL IS ONE INKLING, DEAD CENTRE, HOLDING STILL. (Odaroo, 2026-08-04:
   "WIN — centred inkling with WIN small underneath. LOSE — the same centred frame with a surprised
   face. Keep it simple. And the SAVED picture must be centred dead centre and static.")

   Everything this overrides is v0.549/v0.550's arrangement, and all three of the things it undoes
   were movement or offset:
     · `.rcSeals.duo .rcSeal.won` was `left:50%; transform:translateX(-60%)` — deliberately NOT
       centred, so the mini loser's circle had somewhere to sit. js/share.js now always emits `one`,
       and the `duo` rules are additionally neutralised below so a cached build cannot resurrect them.
     · `rcDance` bounced the winner's circle forever, and `rcShook` wobbled the loser's.
     · the confetti burst looped behind the winner.
   A looping animation and "static in the saved image" cannot both be true, so they go.

   ⚠⚠ THE RIVAL'S NAME IS STILL IN THE MARKUP AND MUST STAY. `shFoeName()` reads the SECOND
   `.rcSealNm` on the card to name the rival for the PNG; hiding that seal with `display:none` is
   correct (it is data for the scraper, not a picture) but DELETING it would make every shared poster
   say "THE HOUSE" for every rival in the game.
   ============================================================ */
.runCard.passport .rcSeals,
.runCard.passport .rcSeals.one,
.runCard.passport .rcSeals.duo{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  position:relative; height:auto; margin:0 0 7px;
}
/* ⚠ NOTHING ON THIS CARD ANIMATES. Restated for every selector that used to set one, because
   `animation` is a shorthand and a later rule REPLACES it rather than adding to it — so a single
   blanket `animation:none` further up would simply have been overwritten by the specific rules. */
.runCard.passport.won .rcSeals.one .rcSealArt,
.runCard.passport .rcSeals.duo .rcSeal.won .rcSealArt,
.runCard.passport .rcSeals.duo .rcSeal.lost .rcSealArt,
.runCard.passport .rcSeal .rcSealArt{animation:none !important;}
.runCard.passport .rcConf{display:none !important;}
/* the one figure: centred in the flow, never positioned off it */
.runCard.passport .rcSeals .rcSeal.solo{position:static; transform:none; margin:0 auto;}
.runCard.passport .rcSeals .rcSeal.solo .rcSealArt{width:86px; height:86px;}
/* ⚠ the LOSE figure is NOT dimmed. The old `.rcSeal.lost` rule dropped it to opacity .72 and
   saturation .3 because it was a tiny corner circle being stepped on; as the one thing on the card
   it has to read as clearly as the winner does. Odaroo asked for a surprised face, not a faded one. */
.runCard.passport .rcSeals .rcSeal.solo.lost .rcSealArt{opacity:1; filter:saturate(.68) contrast(1.08);}
/* ⭐⭐ v0.587 — THE RIVAL IS DRAWN AGAIN, SMALL, UNDER THE RESULT. (Odaroo: "vs [enemy name] with a
   small version of their inkling, about 25% of the main one — small, clean, not dominant.")
   v0.579 had it `display:none` — markup for `shFoeName()` and nothing else. It is a picture again,
   but a quiet one: one row, `VS` then the icon then the name, all on one baseline.
   ⚠ THE ICON IS 22px BECAUSE THE FIGURE ABOVE IT IS 86px. That is the quarter Odaroo asked for taken
   off the real number; if `.rcSeal.solo .rcSealArt` ever changes, this changes with it.
   ⚠ `.rcSeal` IS A COLUMN EVERYWHERE ELSE (art above name) — this is the one place it lies down, so
   the row reads as a credit line rather than as a second, smaller contender. */
.runCard.passport .rcFoeNm{display:flex; align-items:center; justify-content:center; gap:6px;
  margin:1px 0 3px; max-width:100%; min-width:0;}
.runCard.passport .rcFoeNm .rcSeal{flex-direction:row; align-items:center; gap:5px; min-width:0;}
.runCard.passport .rcFoeNm .rcSealArt{width:22px; height:22px;}
.runCard.passport .rcFoeNm .rcSealDisc{border-width:2px; box-shadow:inset 0 0 0 1px #fbf6e6;}
/* ⚠ the cap is the STUB's width, not the name's: the card's printable column is ~246px folded and
   this row also carries the VS word, the icon and two gaps (~39px), so 150px can never overrun it. */
.runCard.passport .rcSeals .rcFoeNm .rcSealNm{display:block; font-family:var(--fontD); font-size:7px;
  letter-spacing:.5px; color:#2a2018; opacity:.9; max-width:150px; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;}
.runCard.passport .rcVs{font-family:var(--fontD); font-size:7px; letter-spacing:1px;
  color:#2a2018; opacity:.6; flex:0 0 auto;}
/* ⭐ WIN / LOSE, small, under the figure. Odaroo asked for roughly a quarter of the inkling — the
   circle above is 86px, so 21px is that quarter and it is a hard number rather than a percentage
   because the seal's size is a px too. */
.runCard.passport .rcOut{font-family:var(--fontD); font-size:21px; letter-spacing:2px;
  line-height:1; margin:7px 0 2px; text-align:center; white-space:nowrap;
  text-shadow:2px 2px 0 #2a201826;}
.runCard.passport .rcOut.won{color:#1c6b35;}
.runCard.passport .rcOut.lost{color:#a33a24;}
/* ⭐⭐ ONE FONT, CLEAN LINES. (Odaroo: "all stats/data aligned in the same consistent font.")
   The figures were already Press Start 2P via `.passport .ovv`, but the label and the value sat on
   their own baselines at different tracking, so a column of five read as ragged. They are now the
   same face, the same size, the same tracking, on one line each, centred as the passport rules ask. */
.runCard.passport .ovlist .ovr{display:flex; justify-content:center; align-items:baseline;
  gap:6px; line-height:1.5;}
.runCard.passport .ovlist .ovk,
.runCard.passport .ovlist .ovv{font-family:var(--fontD); font-size:8px; letter-spacing:1px;}
.runCard.passport .ovlist .ovk{color:#6b5b47;}
.runCard.passport .ovlist .ovv{color:#2a2018;}

/* ============================================================
   ⭐ v0.663 — THE SHOWSTOPPER'S ONE SMALL LABEL, and the rival's WAIT CARD.
   ============================================================ */
/* the small box that says which of the eight games the die rolled. Deliberately quiet: it is a
   caption, not a title, so the playfield under it keeps the room it was measured for. */
.ss-chip{flex:0 0 auto; align-self:center; font-family:var(--fontD); font-size:10px; letter-spacing:.8px;
  color:var(--ink); background:var(--gold); border:2px solid var(--ink);
  padding:3px 9px; border-radius:3px; box-shadow:2px 2px 0 #1b151066; text-align:center;
  max-width:96%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}

/* ⭐⭐⭐ v0.816 — ONE LINE AT THE TOP OF A SHOWSTOPPER, AND NOTHING OVER THE FELT.
   (Odaroo, 2026-08-14: "remove that and include just a simple instruction like you did on top.")
   `.ss-line` is the mini's own `.qte-sub` with the game's name folded into it (see ssGameChip);
   `.ss-foeline` is the rival's turn, which used to be a fixed-position film banner laid across
   the stage. Both are IN FLOW, so `ssBoardRoom` counts them and the board can never be dealt room
   that a sign is standing in.
   ⚠ NO `position`, NO `z-index`, NO `background` shorthand on either. A positioned sign at
   z-index 0 paints over every non-positioned sibling (CLAUDE.md), which on a showstopper is the
   playfield — and that is the fault this rule exists to end, not a way to draw it more neatly.
   ⚠ It WRAPS rather than ellipsing: a two-line instruction on a 375px screen is the house rule
   for every other sign in the game (v0.746), and a clipped instruction is unplayable. */
.ss-line{flex:0 0 auto; align-self:center; text-align:center; max-width:98%;
  overflow-wrap:anywhere; line-height:1.25;}
.ss-line b{color:var(--gold); letter-spacing:.6px;}
.ss-foeline{flex:0 0 auto; align-self:center; text-align:center; max-width:98%;
  /* ⭐ v0.827 — 9px was not an announcement. Stepped up to the same band the game's other in-flow
     instructions use; it is still in flow, so ssBoardRoom takes it off the board's room honestly. */
  font-family:var(--fontD); font-size:clamp(11px,3.4vw,15px); letter-spacing:.8px;
  color:#f2ead6; overflow-wrap:anywhere; line-height:1.3; padding:2px 0;
  display:flex; align-items:center; justify-content:center; gap:5px;}
.ss-foeline b{color:#ff8ae0;}   /* the rival's own colour — see .unit.foe in css/holo.css */

/* ⭐ "IT'S THEIR TURN — SIT TIGHT!" — what you see instead of playing out their run.
   ⚠ It sits ABOVE the D-pad's z-index the way the old ribbon did (the pad is locked anyway), and
   it is pointer-events:none so it can never eat a press that belongs to the battlefield behind it. */
.foewait{position:fixed; inset:0; z-index:9991; display:flex; align-items:center; justify-content:center;
  pointer-events:none; padding:16px; animation:popIn .3s var(--pop);}
.foewait .fw-card{background:var(--paper2); border:4px solid var(--ink); border-radius:10px;
  box-shadow:6px 6px 0 #1b1510aa; padding:14px 18px 12px; text-align:center;
  max-width:min(300px,86vw); display:flex; flex-direction:column; align-items:center; gap:5px;}
.foewait .fw-ink{width:62px; height:62px; margin-bottom:2px; animation:fwBob 1.15s ease-in-out infinite;}
.foewait .fw-ink svg{width:100%; height:100%; display:block;}
@keyframes fwBob{50%{transform:translateY(-6px) rotate(-3deg)}}
.foewait .fw-hd{font-family:var(--fontD); font-size:13px; letter-spacing:2px; color:var(--red);}
.foewait .fw-game{font-family:var(--fontD); font-size:11px; letter-spacing:1px; color:var(--ink);
  background:var(--gold); border:2px solid var(--ink); padding:2px 8px; border-radius:3px;
  max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.foewait .fw-line{font-family:var(--fontD); font-size:10px; letter-spacing:1px; color:var(--ink); opacity:.82; line-height:1.6;}
.foewait .fw-dots{display:flex; gap:6px; margin-top:3px;}
.foewait .fw-dots i{width:7px; height:7px; background:var(--ink); border-radius:50%; animation:fwDot 1.05s ease-in-out infinite;}
.foewait .fw-dots i:nth-child(2){animation-delay:.16s;}
.foewait .fw-dots i:nth-child(3){animation-delay:.32s;}
@keyframes fwDot{0%,100%{opacity:.22; transform:translateY(0)}50%{opacity:1; transform:translateY(-4px)}}
