/* ============================================================
   INKVILLE — home.css
   the menu tiles, the fold-friendly layout, the brawl lobby and the boot intro

   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.
   ============================================================ */
/* ============================================================
   v2.2 — P5-meets-8bit menu, contained cards, touch & mobile
   ============================================================ */
body{overscroll-behavior:none;}
html,body{-webkit-text-size-adjust:100%; text-size-adjust:100%;}   /* no Android font-boosting — boosted glyphs were overlapping fixed-height lines on the Fold */
#hand .hcard{touch-action:none;}
#stage{touch-action:none;}
.ddr-btn,.qbtn,.lrbtn,.backbtn,.btn,.simpad{touch-action:manipulation;}
/* cards stay in their frames */
/* (card shadows ride the drop-shadow filter now — the notch clips box-shadows) */
.card .art svg{image-rendering:pixelated;}
.cost{clip-path:none;}   /* (the old 20x15 cap CLIPPED the 2E — retired; the tag sizes itself like the dossier's) */
/* persona-flavored home */
#scr-home{--accent:#c93a22;}
#homeTint{position:absolute; inset:0; background:var(--accent); opacity:.12; z-index:1;
  pointer-events:none; transition:background .25s;}
#homeBig{position:absolute; bottom:198px; right:10px; z-index:4; pointer-events:none; max-width:56%; overflow:visible;
  font-family:var(--fontD); font-size:24px; line-height:1.25; text-align:right; color:var(--accent);
  text-shadow:3px 3px 0 var(--ink); transform:rotate(-4deg); opacity:.94; letter-spacing:1px;
  transition:color .2s;}   /* sits ABOVE B/A and BELOW the gigs tile — always readable, wraps to two lines */
#homeBig .hb-jp{display:block; font-family:var(--fontJ); font-size:13px; opacity:.85; letter-spacing:1px; margin-top:3px;}
.menu-card{transition:transform .13s var(--pop), background .13s, border-color .13s, box-shadow .13s;
  border-left:6px solid transparent;}
.menu-card.selm{
  transform:translateX(16px) skewX(-5deg) scale(1.03);
  background:var(--ink); color:var(--cream);
  border-color:var(--accent); border-left:10px solid var(--accent);
  box-shadow:6px 6px 0 var(--accent);
}
.menu-card.selm.selL{transform:translateX(-14px) skewX(5deg) scale(1.03);
  box-shadow:-6px 6px 0 var(--accent); border-left-width:6px; border-right:10px solid var(--accent);}
.menu-card.selm .menu-sub{color:var(--paper2)!important;}
.menu-card.selm .menu-arrow{color:var(--accent); animation:pulse .6s infinite;}
.menu-arrow{font-size:22px;}
@media (min-width:560px){   /* wide handhelds / fold / desktop: the marquee sits BETWEEN the D-pad and B&A, under the gigs tile */
  #homeBig{left:210px; right:170px; bottom:96px; max-width:none; text-align:center; transform:rotate(-3deg);}
}
@media (max-width:430px){
  .leader{width:62px; height:90px;}
  .balloon{width:38px; height:50px;}
  #homeBig{font-size:18px; bottom:198px;}   /* phones: ABOVE the A/B cluster, never on the buttons */
  .title-xl{font-size:19px!important;}
}
/* ============================================================
   v3.3 — FOLD-FRIENDLY: bigger type, gamepad, shaped tiles
   ============================================================ */
/* hub art & tiles grow */
.mtile.hero .menu-art{width:54px!important; height:54px!important;}
.mtile.hero2 .menu-art{width:44px; height:44px;}
.mtile.side .menu-art{width:38px; height:38px;}
.mtile.sq{grid-column:span 3!important; min-height:96px!important;}
.mtile.sq .menu-art{width:40px!important; height:40px!important;}
.mtile.hero{min-height:100px!important;}
.mtile.hero2,.mtile.side{min-height:82px!important;}
.mdk{width:42px!important; height:54px!important;}
.mdeck.small .mdk{width:32px!important; height:42px!important;}
/* shaped tiles */
.mtile.side{border-radius:50%!important; aspect-ratio:1/1; flex-direction:column; gap:2px; justify-content:center;}
.mtile.hex{border-radius:50% 50% 14px 14px!important;}                 /* road-show arch */
.mtile.cart{border-radius:10px 10px 3px 3px!important;}
.mtile.cart::before{content:''; position:absolute; top:0; left:22%; right:22%; height:8px;
  background:var(--ink); border-radius:0 0 8px 8px;}
.mtile.marq{border-radius:20px!important;}                              /* arcade screen */
.mtile.tix{border-radius:999px!important;}                              /* gig ticket pill */
.mtile.sq.selm .menu-art{transform:scale(1.3)!important;}
/* gamepad overlay — ABSOLUTE layout; big D-pad bottom-left, B/A far bottom-right (no SELECT/START) */
/* ⭐⭐ v0.370 — EVERY CONTROL IS SIZED THROUGH var(--gs) ("game scale"), published by
   vpNormalize() in game.js as layoutWidth / deviceWidth. On a browser that lays the page out at
   the device width — Chrome, and Samsung once the meta rewrite lands — --gs is EXACTLY 1 and all
   of these compute to the identical pixel values they always had. On a browser that insists on a
   wider layout viewport, --gs grows to match, so the D-pad, A/B, SELECT/START and the inkling stay
   the same FRACTION of the screen, which on the same phone means the same physical size under the
   thumb. This is the general rule for the whole game: control chrome is never a bare pixel number. */
/* ⭐ v0.558 — THE WHOLE CONTROL DECK IS NUDGED UP AND RIGHT (Odaroo, 2026-08-04: thumb accuracy at speed).
   --padny lifts every control and GROWS #gpad by the same amount, because the pad's own top
   edge is what inkPadVar() measures into --padclear — grow the box and every playfield steps back on its
   own; move the controls without growing it and they hang over the field the games measure their floor
   against.
   ⭐ v0.569 — HALVED, AND BOTH EDGES GET BREATHING ROOM (Odaroo, 2026-08-04: 19px read as confusing).
   The lift is now ~2.5mm (10px). Sideways it is the FINISHED POSITION that was halved, not the nudge:
   the D-pad's left edge was 6px originally and 25px after the shift, so it lands on 16px = --padedge 12
   + --padnx 4. Splitting it that way is deliberate — --padedge is the gap from the glass on BOTH sides
   and 6px was simply too close on either, so the right-hand controls now take --padedge STRAIGHT and no
   longer subtract --padnx: there is no room to the right of a screen edge, and the point of this pass is
   to give that side MORE space, not less (0px → 12px). */
/* ⭐ v0.773 — EXPERIMENT (Odaroo, 2026-08-09): the D-PAD ALONE moves up and slightly left, so he can
   feel whether a higher cross reads better under the thumb. Style, size and colours are untouched —
   this is a position change and nothing else. --dpadup lifts the cross AND grows #gpad by the same
   amount (the v0.558 rule above: the pad's own top edge is what inkPadVar() measures into --padclear,
   so lifting a control without growing the box hangs it over the playfield every game measures against).
   A/B and SELECT/START are deliberately left exactly where they were. Revert by zeroing the two vars.
   ⭐⭐ v0.775 — IT IS GLOBAL NOW (Odaroo, 2026-08-09): "dial it for every game … the same movement,
   the same shifting up everywhere". He wants to feel the pad in ONE place across the whole game, so
   the screen-scoped rule is gone and the two vars carry their values on :root. He was told the cost
   and accepted it: INK-TAR's INFLATE label and INK-PUT's playfield anchor off their own numbers
   rather than --padclear, so the cross can sit over them. That is the experiment, not a regression —
   do not "fix" it by re-scoping the lift. Revert the whole thing by zeroing the two vars. */
:root{--padnx:4px; --padny:10px; --padedge:12px; --dpadup:44px; --dpadlx:8px;}
#gpad{position:absolute; left:0; right:0; bottom:0; height:calc(240px * var(--gs,1) + var(--sab,0px) + var(--padny,0px) + var(--dpadup,0px) * var(--gs,1)); z-index:460; pointer-events:none; opacity:.5;}
/* ⭐⭐ v0.353 — THE PAD OUTRANKS EVERY DIALOG. The backdrop is z400; the pad used to sit at 62, so the
   grey wash covered it — that is why the controls looked greyed out AND stopped taking taps whenever a
   pop-up opened. Nothing may ever be layered over the pad again. (Toasts 9995 / bubbles 9996 still win.) */
.gpsel{outline:3px solid var(--gold)!important; outline-offset:2px; box-shadow:0 0 0 3px var(--gold), 4px 4px 0 #000a!important; position:relative; z-index:1;}
/* ⚠ v0.568 — NO CARET. The cursor used to hang a blinking ◄ off the right of the selected button.
   Odaroo: the gold ring IS the cursor, and the arrow beside it was noise. The rule is deleted rather
   than hidden so nothing measures a caret that is not drawn (it used to add 15px of scrollWidth and
   read to the pre-flight as CLIPPED TEXT — see the note that used to live in css/hub.css). */
.navsel{outline:3px solid var(--gold)!important; outline-offset:1px; box-shadow:0 0 0 3px var(--gold), 0 0 12px #d9a232cc!important; z-index:6; position:relative;}
.gp-d{position:absolute; left:calc(var(--padedge,6px) * var(--gs,1) + var(--padnx,0px) - var(--dpadlx,0px)); bottom:calc(51px * var(--gs,1) + var(--sab,0px) + var(--padny,0px) + var(--dpadup,0px) * var(--gs,1)); width:calc(188px * var(--gs,1)); height:calc(188px * var(--gs,1)); pointer-events:none;}   /* Game Boy: D-pad far bottom-LEFT, lifted clear of SELECT/START */
.gpcross{position:absolute; left:0; bottom:0; width:calc(188px * var(--gs,1)); height:calc(188px * var(--gs,1)); pointer-events:auto; touch-action:none; cursor:pointer;}
.gpcross-svg{width:100%; height:100%; display:block; filter:drop-shadow(3px 3px 0 #00000055);}
.gpc-ar{transition:fill .04s;} .gpc-nub{transition:transform .05s;}
/* press feedback: the pushed arm inverts to bright + the nub leans that way */
.gpcross.d-up .gpc-up,.gpcross.d-dn .gpc-dn,.gpcross.d-lf .gpc-lf,.gpcross.d-rt .gpc-rt{fill:#c93a22;}
.gpcross.pressing .gpc-face{fill:#2a2320;}
.gpcross.d-up .gpc-nub{transform:translateY(-5px);} .gpcross.d-dn .gpc-nub{transform:translateY(5px);}
.gpcross.d-lf .gpc-nub{transform:translateX(-5px);} .gpcross.d-rt .gpc-nub{transform:translateX(5px);}
.gpb{background:var(--ink); border:3px solid var(--paper); color:var(--cream); font-family:var(--fontD);
  pointer-events:auto; touch-action:manipulation; display:flex; align-items:center; justify-content:center; cursor:pointer;}
.gpb:active,.gpb.kbpress{filter:invert(1);}   /* invert colours while held (touch OR keyboard) — clear "pressed" feedback */
.ddr-btn:active,.dpad-b:active,.lrbtn:active,.qbtn:active{filter:invert(1);}
/* ⭐⭐ v0.415 — DAYLIGHT BETWEEN THE BUTTONS AND SELECT/START.
   Measured before touching anything: the B button's bottom edge was at 764 and the SELECT/START row
   began at 760, so they were not close — they were four pixels INTO each other, with B sitting on
   top of START. Three moves, deliberately small and in two directions:
     · A/B come up 8px. This is the pair that was actually colliding and its box has 70px of
       headroom inside the pad, so it can afford the whole move.
     · SELECT/START go down 6px. --sab is still added on top, so a phone with a gesture bar lifts
       them clear exactly as before; only the base gap changes.
     · the D-pad cross comes up 3px, and no more. Its 188px box already sat just 4px below the pad's
       own top edge, so anything greater would push it up out of the pad and over the playfield —
       which is the boundary every game measures its floor against.
   That turns a 4px overlap into a 10px gap under B and a 7px gap under the cross. */
/* Game Boy: A/B far bottom-RIGHT, on the classic diagonal (B lower-left, A upper-right) */
.gp-ab{position:absolute; right:calc(var(--padedge,6px) * var(--gs,1)); bottom:calc(56px * var(--gs,1) + var(--sab,0px) + var(--padny,0px)); width:calc(150px * var(--gs,1)); height:calc(122px * var(--gs,1)); pointer-events:none;}   /* A/B lifted with the D-pad — SELECT/START get real breathing room */
/* during a showstopper minigame A/B give way to a SECOND D-pad on the right (both thumbs!) */
.gp-d2{position:absolute; right:calc(var(--padedge,6px) * var(--gs,1)); bottom:calc(51px * var(--gs,1) + var(--padny,0px)); width:calc(188px * var(--gs,1)); height:calc(188px * var(--gs,1)); pointer-events:none; display:none;}   /* exact mirror of the left D-pad */
.gp-d2 .gpcross{position:absolute; right:0; bottom:0; width:calc(188px * var(--gs,1)); height:calc(188px * var(--gs,1)); pointer-events:auto; touch-action:none; cursor:pointer;}
#gpad.qte2 .gp-ab{display:none;}
#gpad.qte2 .gp-d2{display:block;}
/* ⭐⭐⭐ v0.778 — TWO D-PADS ARE ONE HORIZONTAL LINE, MIRRORED, ON BOTH EDGES.
   (Odaroo, 2026-08-10: "all the mini games that have two d-pads … the same horizontal line, the
   same shift as a d-pad we've done … as symmetrical as possible … toss them so that it touches the
   edge perfectly.")
   The right-hand pad had DRIFTED out of the left one's arithmetic and nobody had noticed because
   the two are never measured against each other: it was missing --sab (a phone with a gesture bar
   put it LOWER than the left one) and missing --dpadup entirely (so the v0.773/v0.775 global lift
   moved the left cross up 44px and left the right one behind — the two pads were 44px apart on a
   line that is supposed to be one line). Both are restated here, together, so the pair can only
   ever move as a pair.
   ⚠ THE SIZE IS CAPPED AT 49vw IN THIS MODE AND THAT IS NOT DECORATION. Flush to both edges,
   two 188px crosses need 376px and the folded phone is 375 — the two touch BOXES would overlap by
   a pixel down the middle of the screen, and .gpcross takes pointer events across its whole box,
   so the overlap is a real mis-press, not a cosmetic one. The cap makes "flush to both edges"
   safe at every width instead of true at most of them. */
#gpad.qte2 .gp-d,#gpad.qte2 .gp-d2{
  bottom:calc(51px * var(--gs,1) + var(--sab,0px) + var(--padny,0px) + var(--dpadup,0px) * var(--gs,1));
  width:min(calc(188px * var(--gs,1)),49vw); height:min(calc(188px * var(--gs,1)),49vw);}
#gpad.qte2 .gp-d{left:0;}
#gpad.qte2 .gp-d2{right:0;}
#gpad.qte2 .gp-d .gpcross,#gpad.qte2 .gp-d2 .gpcross{width:100%; height:100%;}
.gpbtn2{position:absolute; left:calc(2px * var(--gs,1)); bottom:0;}                          /* B — lower-left of the pair */
.gpa{position:absolute; right:calc(2px * var(--gs,1)); bottom:calc(46px * var(--gs,1));}                        /* A — upper-right of the pair */
/* ⭐⭐⭐ v0.415 — THE GLYPH IS CENTRED BY ITS INK, NOT BY ITS BOX.
   Flex centring centres the text's LAYOUT box, and in Press Start 2P that box is not where the
   letter actually is: the face carries its ink high and left inside the em square. Measured off the
   font's own metrics at the size these buttons use, the drawn A, B and X all sit exactly 2px up and
   2px left of the button's true centre — which is small enough to look like nothing in particular
   and big enough to look wrong, which is what Odaroo was seeing.
   2px at font-size 29 is 0.069em, so the correction is written in EM and scales itself: it stays
   true at every --gs, on the folded screen and the unfolded one, and it would still be true if the
   button size were changed tomorrow. (An earlier build did this with a 4px top padding — correct
   arithmetic, but padding on a content-box element also made the circle 4px wider, and it was
   dropped at some point without the centring being put back another way.) */
/* ⚠⚠ v0.780 — A AND B ARE 72×72 AND THEY STAY THAT WAY. v0.779 grew them to 96 tall on every
   screen in the game; Odaroo asked for it back the same day — what he wanted taller was INK-TAR
   HERO'S OWN buttons, in that game's narrow strip, and nothing else. A control that is global is
   not the place to solve one screen's problem. See .ith-fret in css/rhythm.css for where it went. */
.gpa,.gpbtn2,.gpx{width:calc(72px * var(--gs,1)); height:calc(72px * var(--gs,1)); border-radius:50%!important; font-size:calc(29px * var(--gs,1)); pointer-events:auto; line-height:1; padding:0; display:flex; align-items:center; justify-content:center; touch-action:none; -webkit-user-select:none; user-select:none;
  transform:translate(.069em,.069em);}   /* FLEX-centred glyphs, then nudged onto their own ink */
/* SELECT / START — small legible pills, centred along the very bottom like a real Game Boy */
/* ⭐ v0.385 — the body.sbrowser gutter is gone with the rest of the browser detection. SELECT/START
   now get the SAME 18px lift in every browser, on top of the device's own safe-area inset. */
.gp-mid{position:absolute; left:50%; right:auto; transform:translateX(calc(-50% + var(--padnx,0px))); bottom:calc(12px * var(--gs,1) + var(--sab,0px) + var(--padny,0px)); display:flex; flex-direction:row; gap:calc(16px * var(--gs,1)); z-index:3; pointer-events:none;}
.gpss{pointer-events:auto; background:var(--ink); border:2px solid var(--paper); color:var(--cream);
  width:calc(72px * var(--gs,1)); box-sizing:border-box; justify-content:center;   /* SELECT + START — the exact same length */
  font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); letter-spacing:.5px; padding:calc(4px * var(--gs,1)) calc(7px * var(--gs,1)) calc(5px * var(--gs,1)); border-radius:8px!important;
  transform:skewX(-10deg); cursor:pointer; touch-action:manipulation; text-align:center; line-height:1; white-space:nowrap;
  display:flex; flex-direction:column; align-items:center; gap:2px;}   /* pixel icon on top: SELECT=save file, START=pause */
.gpss-ic{width:calc(11px * var(--gs,1)); height:calc(11px * var(--gs,1)); display:block; image-rendering:pixelated;}
/* uniform OPTION buttons for yes/no-style dialogs — identical size & colour */
#modalBox .btn.opt{width:62%; min-height:46px; box-sizing:border-box; display:inline-flex; align-items:center; justify-content:center;}
/* THE SECRET OF INKVILLE — gelatinous ink forms breathe like water */
.inkform{animation:inkWobble 3.2s ease-in-out infinite; transform-origin:50% 88%;}
@keyframes inkWobble{0%,100%{transform:scale(1,1)}30%{transform:scale(1.025,0.972)}55%{transform:scale(0.978,1.022) skewX(.7deg)}80%{transform:scale(1.012,0.99)}}
/* ✨ SHINY rival inklet — prismatic shimmer */
.shinyInk{display:block; width:100%; height:100%; animation:shinyInkA 1.4s linear infinite;}
.shinyInk svg{width:100%; height:100%; display:block;}
@keyframes shinyInkA{0%{filter:hue-rotate(0deg) brightness(1.15) drop-shadow(0 0 4px #fff)}50%{filter:hue-rotate(180deg) brightness(1.35) drop-shadow(0 0 8px #ffe58a)}100%{filter:hue-rotate(360deg) brightness(1.15) drop-shadow(0 0 4px #fff)}}
.gpss:active{filter:invert(1);}
/* ===== BIT-BRAWL PVP LOBBY ===== */
.lob-hub{position:relative; height:88px; margin:4px 0 2px; border:2px solid var(--ink); border-radius:8px; overflow:hidden; flex:none;
  display:flex; align-items:flex-end; justify-content:space-around; padding:6px 8px 10px;
  background:linear-gradient(#e6edf5 0%,#cdd9e6 58%,#b9a888 58%,#a89873 100%);}
.lob-hub::after{content:''; position:absolute; left:0; right:0; bottom:0; height:42%; pointer-events:none;
  background:repeating-linear-gradient(90deg,#0000 0 20px,#00000012 20px 40px);}
.lob-fighter{display:flex; flex-direction:column; align-items:center; z-index:2;}
.lob-art{width:52px; height:52px; image-rendering:pixelated; filter:drop-shadow(2px 3px 0 #0004); display:flex; align-items:flex-end; justify-content:center; animation:aceFloat 3s ease-in-out infinite;}
.lob-art svg{width:100%; height:100%; display:block;}
.lob-myst{font-family:var(--fontD); font-size:44px; color:var(--red); align-items:center; text-shadow:3px 3px 0 var(--ink); animation:mystPulse 1s steps(2) infinite;}
@keyframes mystPulse{0%,100%{opacity:.55}50%{opacity:1}}
.lob-conn{align-items:center;}   /* the enemy inklet floats peacefully, just like you */
.connE{animation:connBlink 1.2s steps(1) infinite; opacity:0;}
.connE:nth-child(2){animation-delay:.4s;} .connE:nth-child(3){animation-delay:.8s;}
@keyframes connBlink{0%,33%{opacity:1}34%,100%{opacity:0}}
.lob-vs{position:absolute; left:50%; bottom:20px; transform:translateX(-50%); font-family:var(--fontD); font-size:26px; color:var(--red); text-shadow:3px 3px 0 var(--ink); z-index:5; text-align:center;}   /* BIG VS, dead centre */
/* the stage: fighters hug the EDGES, VS rides the MIDDLE between the arts (clear of the name line), names sit under their own fighter and never reach the centre */
.lob-stage.lob-hub{justify-content:space-between; padding-left:8px; padding-right:8px;}
.lob-stage .lob-fighter{position:relative; flex:0 0 40%; justify-content:center; padding-bottom:24px;}
.lob-stage .lob-vs{top:50%; bottom:auto; transform:translate(-50%,-50%); font-size:19px; z-index:6;
  text-shadow:3px 3px 0 var(--ink),-2px -2px 0 var(--ink),2px -2px 0 var(--ink),-2px 2px 0 var(--ink),0 0 14px #0009;}   /* dead-centre, floating above the name line, ink-outlined so it never merges with a name */
.lob-stage .lob-tag{position:absolute; bottom:26px; left:50%; transform:translateX(-50%); margin:0; padding:0; width:max-content; max-width:96%; font-size:10px;}
.lob-stage #cDots{position:absolute; top:100%; left:0; right:0; height:12px;}   /* the dots breathe on their OWN line, just under CONNECTING */
/* ⚠ v0.375 — A NAME IS ONE LINE. It used to wrap, and a wrapped name pushed the whole VS stage
   taller and shoved the buttons under it about. Names are capped at 12 characters everywhere
   (capNm), so one line always fits; if a screen is ever too narrow for it, lobbyFit() scales the
   whole box rather than letting a name break in half. */
.lob-tag{font-family:var(--fontD); font-size:11px; line-height:1.45; text-align:center; background:none; color:#fff; padding:0 12px 6px; margin-top:2px; white-space:nowrap; max-width:132px;
  text-shadow:1px 1px 0 #1b1510, -1px -1px 0 #1b1510, 1px -1px 0 #1b1510, -1px 1px 0 #1b1510;}   /* NO plates on ANY name — plain white w/ ink outline, both corners, padded off the frame */
#cDots{display:block; height:12px; text-align:center; letter-spacing:2px;}   /* CONNECTING holds the NAME line; the dots breathe on their OWN line beneath, clear of the frame */
/* BIT-BRAWL — the ONLINE dressing on the home banner */
.bb-globe{position:absolute; left:4px; top:3px; transform:none; width:22px; height:22px; z-index:3; pointer-events:none; image-rendering:pixelated; opacity:.96;
  filter:drop-shadow(0 0 4px #49c6c088); animation:bbGl 5.5s steps(2) infinite;}   /* a tiny world — the ?'s size, in the OPPOSITE corner — still glowing, still glitching */
.bb-globe svg{width:100%; height:100%;}
@keyframes bbGl{0%,88%{filter:drop-shadow(0 0 6px #49c6c088)}91%{filter:drop-shadow(2px 0 4px #ff004077) hue-rotate(80deg)}94%{filter:drop-shadow(-2px 0 4px #00e6ff88) invert(.12)}97%,100%{filter:drop-shadow(0 0 6px #49c6c088)}}
.bb-buddies{position:absolute; left:9px; top:11px; width:26px; z-index:4; pointer-events:none; image-rendering:pixelated; filter:drop-shadow(1px 1px 0 #0008);}   /* the two messengers stand IN FRONT of the earth — one corner badge: world behind, users online in front */
.bb-buddies svg{width:100%; height:auto;}
.bb-online{position:relative; z-index:3; height:18px; box-sizing:border-box; font-family:var(--fontD); font-size:8px; letter-spacing:1px; color:#fff;
  background:#1f7a3f; border:2px solid #1b1510; border-radius:99px; padding:0 8px; display:flex; align-items:center; gap:4px; pointer-events:none; margin:3px auto;}   /* stacked: gloves → BIT-BRAWL → ONLINE → ビット・ブロール */
.bb-dot{width:6px; height:6px; border-radius:50%; background:#b6ffb0; animation:pulse 1.2s ease-in-out infinite;}
.mtile .menu-art,.mtile .menu-label,.mtile .jplab{position:relative; z-index:3;}   /* the title always reads OVER the world — the globe is the backdrop */
.lob-deck{display:grid; grid-template-columns:repeat(8,1fr); gap:3px; margin:2px 0 6px;}
.lob-card{aspect-ratio:3/4; background:#fdf6e3; border:1.5px solid var(--ink); border-radius:2px; overflow:hidden; display:flex; align-items:center; justify-content:center;}
.lob-card.empty{background:#00000014; border-style:dashed;}
.lob-card svg{width:118%; height:118%;}
.lob-actions{display:flex; flex-direction:column; gap:6px; margin-top:2px;}
/* ===== BOOT INTRO — "ODAROO PRESENTS" (SNES cold open) ===== */
#introSplash{position:absolute; inset:0; z-index:150; background:#05040a; display:none;
  align-items:center; justify-content:center; overflow:hidden; font-family:var(--fontD);}
/* ⚠⚠ `preboot` is the FIRST-PAINT state (v0.749) — see the comment on the element in index.html.
   It must be here and not only on `.on`, because `.on` is not added until js/arcade.js runs. */
#introSplash.on,#introSplash.preboot{display:flex;}
/* ⭐⭐⭐⭐ v0.773 — the v0.765 `html.inkwarm` rule that hid this cover on a "warm resume" is GONE
   with the warm-resume skip itself (the intro always plays — see inkWarmBoot in js/arcade.js).
   ⚠ Nothing sets that class any more; do not write a rule for it. */
/* the ON/OFF power switch — the deliberate first press that unlocks audio */
/* ⭐⭐ v0.725 — THE BOOT COLUMN RIDES HIGH, WHERE THE STUDIO MARK STANDS (Odaroo, 2026-08-06).
   Centring the whole column (cart + arrow + handheld + the two words) put the HANDHELD itself
   below the middle of the screen, because everything above it is shorter than the words below it.
   `top:40%` lifts the column so the Game Boy sits about where #introCard's target mark sits on
   the next screen, and the boot and the studio card read as the same place.
   ⚠ IT MUST BE `top`, NOT A TRANSFORM: `bootCorrupt` hard-codes `translate(-50%,-50%)` in every
   keyframe, so any translate written here is thrown away the moment the screen corrupts. */
#introBoot{position:absolute; left:50%; top:40%; transform:translate(-50%,-50%); z-index:8;
  display:none; flex-direction:column; align-items:center; gap:2px; cursor:pointer; perspective:420px;}
/* ⭐ v0.370 — the TAP TO LOAD boot screen scales through --gs like every other control, so the
   very first thing a player sees is already the right size in whatever browser they opened. */
.ib-cart{width:calc(76px * var(--ibs)); height:calc(76px * var(--ibs)); image-rendering:pixelated; filter:drop-shadow(0 0 8px #ffffff99); position:relative; z-index:2;
  animation:cartFloat 2.4s ease-in-out infinite;}   /* a tad bigger — still smaller than the handheld */
/* ⭐⭐⭐ v0.700 — THE INSIDE OF THIS BOX IS NOT DESCRIBED HERE. It is the shared glitch-cart rig
   (inkCartRig() in js/ui.js, styled in css/base.css) — the exact same object the install invite
   shows. This sheet owns the SIZE, the float and the insert; base.css owns the spin, the glitch
   and the still ink drop. Do not re-describe the rig here or the two will drift apart again. */
.ib-arrow{width:calc(44px * var(--ibs)); height:calc(40px * var(--ibs)); margin:calc(3px * var(--ibs)) 0; image-rendering:pixelated; filter:drop-shadow(0 0 6px #ffffffcc); animation:arrDown 1.1s ease-in infinite;}   /* BIG and clearly pointing the way down */
.ib-arrow svg{width:100%; height:100%; display:block;}
@keyframes arrDown{0%{transform:translateY(-6px); opacity:0}25%{opacity:1}75%{opacity:1}100%{transform:translateY(12px); opacity:0}}
.ib-gb{width:calc(104px * var(--ibs)); height:calc(168px * var(--ibs)); position:relative; image-rendering:pixelated; filter:drop-shadow(0 6px 16px #000a); transform-style:preserve-3d;}
.ib-gb .gbface{position:absolute; inset:0; backface-visibility:hidden; -webkit-backface-visibility:hidden;}
.ib-gb .gbface svg{width:100%; height:100%; display:block;}
.ib-gb .gbback{transform:rotateY(180deg);}   /* the spin shows a blank shell + battery cover, never a mirrored front */
/* ⭐⭐ v0.719 — BOTH LINES ARE BIG, AND THEY ARE THE SAME LENGTH LEFT-TO-RIGHT (Odaroo, 2026-08-06).
   "TAP TO LOAD" is 11 characters, タップしてロード is 8, so the Japanese carries the MUCH LARGER font
   ON PURPOSE — that is what makes the two lines span the same width. The arithmetic, measured:
     Press Start 2P is a 1em square, so EN width = 11 x 1.11em = 12.21 x its font size
     DotGothic16 is also 1em square,  so JP width =  8 x 1.025em = 8.2 x its font size
     equal  =>  JP font = 12.21 / 8.2 = 1.489 x EN font.  That is where --ibJP comes from.
   ⚠ THE LETTER-SPACING IS IN `em`, NOT PIXELS. A pixel gap does not scale with the font, so the
     two lines would drift apart the moment --ibs moved (they match folded and miss unfolded).
   ⚠ .ib-hint CARRIES A FIXED WIDTH and nowrap. #introBoot is `translate(-50%,-50%)` with an auto
     width, so whatever is widest inside it decides where the whole boot column sits; at these
     sizes the words are wider than the handheld, and without a fixed width the line wrapped into
     three and the cart hopped sideways. Same family as the coin-toss drift. */
/* ⭐ v0.725 — both lines are 20% smaller (27px/7.5vw → 21.6px/6vw) and sit closer under the
   handheld. ⚠ THE 1.489 AND THE 12.4 ARE RATIOS, so shrinking --ibEN alone keeps the two lines
   the same length and keeps the fixed width honest — never re-tune them to match a new size. */
.ib-hint{--ibEN:min(calc(21.6px * var(--ibs)), 6vw); --ibJP:calc(var(--ibEN) * 1.489);
  margin-top:calc(9px * var(--ibs)); font-family:var(--fontD); font-size:var(--ibEN);
  color:#eef2f6; letter-spacing:.11em; text-align:center; white-space:nowrap;
  width:calc(var(--ibEN) * 12.4); animation:hintPulse 1.3s ease-in-out infinite;}
.ib-hint span{display:block; font-family:var(--fontJ); font-size:var(--ibJP); color:#9aa7bb;
  margin-top:calc(6px * var(--ibs)); letter-spacing:.025em;}
@keyframes cartFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}   /* upright — no tilt */
@keyframes cartGlitch{0%,86%,100%{filter:drop-shadow(0 0 8px #ffffff99)}90%{filter:drop-shadow(3px 0 0 #ffffff) drop-shadow(-3px 0 0 #666666) drop-shadow(0 0 12px #ffffff);}}
@keyframes cartShine{0%,100%{transform:translateX(0); opacity:.15}50%{transform:translateX(20px); opacity:.7}}
@keyframes arrBob{0%,100%{transform:translateY(0)}50%{transform:translateY(5px)}}
@keyframes hintPulse{0%,100%{opacity:.55}50%{opacity:1}}
/* TAP → the cart drops into the slot, the Game Boy spins 360, the screen corrupts */
.ib-cart.loading{animation:cartLoad .52s ease-in forwards!important;}
@keyframes cartLoad{0%{transform:translateY(0)}55%{transform:translateY(48px) scale(.96)}100%{transform:translateY(66px) scale(.88); opacity:.1}}
/* ⭐⭐ v0.700 — IT MUST GO IN FLAT, AND IT MUST BE SEEN TO TURN FLAT FIRST. The tap does not snap
   the cartridge square-on: introPowerOn stops the spin where it is and eases it round to face
   forward (`.ickrig.seat`), and ONLY THEN does the slide into the slot start. A cart still turning
   as it enters the slot reads as a fault, and one that jumps to square reads as a dropped frame. */
.ib-gb.flip360{animation:gbFlip .55s linear 1;}   /* one SMOOTH full 360 — the blank back shows completely mid-spin */
@keyframes gbFlip{from{transform:rotateY(0)}to{transform:rotateY(360deg)}}
#introBoot.corrupt{animation:bootCorrupt .09s steps(2) infinite;}
/* the zoom-in: straight into the handheld's screen while everything fades to black */
.ib-gb{transform-origin:50% 30%;}
#introBoot.zoomin .ib-gb{animation:gbZoom .82s cubic-bezier(.55,0,.85,.36) 1 forwards;}
@keyframes gbZoom{0%{transform:scale(1)}100%{transform:scale(12)}}
#introBoot.zoomin{animation:bootDark .82s ease-in 1 forwards;}
@keyframes bootDark{0%{opacity:1; filter:brightness(1)}55%{opacity:1; filter:brightness(.55)}100%{opacity:0; filter:brightness(0)}}
@keyframes bootCorrupt{0%{filter:none; transform:translate(-50%,-50%)}33%{filter:hue-rotate(90deg) saturate(2.4) contrast(1.5); transform:translate(calc(-50% + 4px),-50%) skewX(3deg)}66%{filter:invert(.15) hue-rotate(-60deg); transform:translate(calc(-50% - 4px),-50%) skewX(-3deg)}100%{filter:none; transform:translate(-50%,-50%)}}
#titleStartBtn{opacity:0; transform:translateY(10px) scale(.92);}   /* appears only after the intro settles */
#titleStartBtn.settled{opacity:1; transform:none; animation:startPop .55s var(--pop);}
@keyframes startPop{0%{opacity:0; transform:translateY(14px) scale(.8)}60%{transform:translateY(0) scale(1.06)}100%{transform:none}}
#introSplash::before{content:''; position:absolute; inset:0; pointer-events:none; opacity:.6;
  background:repeating-linear-gradient(0deg,#0000 0 2px,#ffffff0d 2px 4px);}
#introSplash.fadeout{animation:introFade 1s ease-in forwards;}
@keyframes introFade{0%{opacity:1}100%{opacity:0; visibility:hidden}}
#introText{opacity:0; text-align:center; z-index:3; position:relative;}
#introText.show{animation:introTextIn .6s steps(4) forwards;}
/* ⚠ v0.721 — it must SETTLE at letter-spacing 0. `forwards` makes the last frame the resting state,
   so the old 3px ending quietly added ~37px to a 12-character line at the new size and could push
   it off a 375px screen — the rule in .intro-pres is not what wins here, this is. */
@keyframes introTextIn{0%{opacity:0; filter:brightness(6); letter-spacing:10px}100%{opacity:1; filter:none; letter-spacing:-.5px}}
/* ⚠ HISTORY, so it is not re-litigated: this line has been long side bars (v0.699), short dashes,
   gold engraving, and a small plain "— Odaroo & Co. presents —" (v0.710). v0.721 drops the word
   "presents" and makes what is left BIG, because it is now standing in for the wordmark itself.
   ⚠ DO NOT DECORATE IT. Rules, diamonds, gradients and split sizes have each been tried and each
   been removed by name. If it looks plain, that is the brief. */
/* ⭐⭐⭐ v0.721 — "presents" IS GONE and the line is BIG: it is standing in for the wordmark, so it
   is sized like the wordmark (h1.glogo is clamp(26px,9vw,58px)) and merely held back by its own
   length. ⚠ THE 12.5 IS THE CHARACTER COUNT — the display face is ~1em wide a glyph, so "Odaroo &
   Co." is about 12.5x the font-size across. That divisor is the only thing keeping it on one line
   at 375px; re-measure it if the wording ever changes again. */
/* ⭐⭐⭐ v0.727 — SMALL, AND TIGHT (Odaroo, 2026-08-06). Two changes and a consequence:
     · the size is 30% of what v0.721 set — it is a studio label again, not a stand-in wordmark;
     · "Odaroo" and "& Co." close up: the tracking goes NEGATIVE and the word gaps come in, so the
       four words read as one mark rather than a spaced-out line;
     · because it no longer FILLS the wordmark's row, it can no longer be registered on INKVILLE by
       being the same size — so the box is given INKVILLE's own line height (clamp × 1.15, copied
       from h1.glogo in css/wjam.css) and the words are CENTRED inside it. That is what keeps the
       two screens registered. ⚠ The old 12.5-character divisor is kept as the floor of the min()
       so a wider wording still cannot overrun 375px.
   ⚠⚠ letter-spacing here does NOT win on its own — #introText.show settles the animation with
      `forwards`, so the end frame of introTextIn had to be changed to the same -.5px. */
/* ⭐⭐⭐ v0.734 — IT IS PART OF THE LOGO NOW. The nine tiles spell the name in pixels; this line
   spells it in the game's own 8-bit face directly underneath, and the two together are the mark.
   So it is bigger again than v0.727's studio label — but it is still the SECOND half of the logo,
   never the first, so it is capped well under the tiles' own type.
   ⚠ THE 12.6 IS THE CHARACTER COUNT of "ODAROO & CO." plus a little slack — the display face is
   about 1em wide a glyph, so that divisor is the only thing keeping it on one line at 375px.
   Re-measure it if the wording ever changes. */
.intro-pres{font-family:var(--fontD); font-weight:normal; white-space:nowrap; text-align:center;
  font-size:min(20px, calc((100vw - 48px) / 25.2));   /* ⚠ v0.745 — HALF of v0.734, both halves of it: the cap AND the character divisor. Halving only the cap would have left it full size at 375px, where the divisor is what wins. */
  height:auto;   /* ⚠ v0.734 — it no longer stands in INKVILLE's row, so it no longer borrows
                    INKVILLE's line height; the card's own `gap` sets the space above it. */
  display:flex; align-items:center; justify-content:center;
  line-height:1; letter-spacing:-.5px; word-spacing:-.14em;
  color:#eef2f8; text-shadow:2px 2px 0 #05040a, 0 0 12px #9fd0ff55;}
/* the invisible half of the title's stack — it takes up room and says nothing, which is the whole job */
.ic-ghost{visibility:hidden; pointer-events:none;}
.ic-ghost .subtitle{margin-top:4px;}
.intro-sub2{font-size:58%; font-weight:normal; letter-spacing:4px;}
#introText.zap{animation:introTxZap .09s steps(2) 5;}
@keyframes introTxZap{0%{transform:translate(-3px,1px)}50%{transform:translate(3px,-1px); filter:hue-rotate(70deg) brightness(1.5)}100%{transform:none}}
#gpad.introHide{opacity:0!important;}
/* ⭐ v0.897 (Odaroo, 2026-09-27: "a blank screen while it loads, and only then TAP TO LOAD") — while the
   black `preboot` cover is up the pad is not drawn at all, whatever any heartbeat has done to its
   classes. The pad is z-index 460 and the cover 150, so without this the D-pad, A and B showed
   through the black before a single module had loaded. */
#app:has(#introSplash.preboot) #gpad{visibility:hidden!important;}
#gpad.introRaise{z-index:151!important; opacity:1!important;}
#gpad.introZapIn{animation:gpZapIn .25s steps(3) 1;}
@keyframes gpZapIn{0%{opacity:0; transform:translateY(10px); filter:brightness(7)}45%{opacity:1}100%{opacity:1; transform:none; filter:none}}
/* the ONE slow flash lights the button edges — and they STAY lit through the transition */
#gpad.introLit .gpcross-svg,#gpad.introLit .gpb{filter:drop-shadow(0 0 5px #7be9ff) drop-shadow(0 0 11px #7be9ff) brightness(1.4);}
#gpad.introLit .gpcross .gpc-face,#gpad.introLit .gpb{border-color:#9ff!important;}
#gpad.introLit .gpc-face{stroke:#9ff;}
.elecbolt{position:absolute; z-index:153; pointer-events:none; width:26px; height:30px; opacity:0;}
.elecbolt svg{width:100%; height:100%; filter:drop-shadow(0 0 5px #aef);}
.elecbolt.zap1{animation:boltOnce .8s ease-out 1;}
@keyframes boltOnce{0%{opacity:0; transform:scale(.5)}25%{opacity:1; transform:scale(1.15)}60%{opacity:.6}100%{opacity:0; transform:scale(1.25)}}
.intro-flash{position:absolute; inset:0; z-index:5; background:#eaf6ff; opacity:0; pointer-events:none;}
.intro-flash.go{animation:iFlashSlow .45s ease-out 1;}   /* single, quick */
@keyframes iFlashSlow{0%{opacity:0}18%{opacity:.9}100%{opacity:0}}
.intro-glx{position:absolute; inset:0; z-index:6; pointer-events:none; opacity:0; mix-blend-mode:screen;
  background:linear-gradient(90deg,#ff004055,transparent 42%,transparent 58%,#00e6ff55);}
.intro-glx.go{animation:introGlxA .1s steps(2) 9;}
@keyframes introGlxA{0%{opacity:.75; transform:translateX(-7px)}50%{opacity:.4; transform:translateX(7px)}100%{opacity:0}}
/* --- intro tutorial: inklet + rubberhose glove --- */
.intro-ov{position:fixed; inset:0; z-index:200; background:rgba(18,14,28,.76); cursor:pointer; font-family:var(--fontD);}
.intro-inklet{position:absolute; left:50%; top:24%; width:128px; height:128px; margin-left:-64px; margin-top:-64px;}
.intro-inklet svg{width:100%; image-rendering:pixelated; filter:drop-shadow(0 0 14px #7ab6ffaa);}
.intro-glove{position:absolute; width:96px; height:144px; display:none; margin-left:-48px;}
.intro-glove.pt-top{left:50%; top:38%;}
.intro-glove.pt-dpad{left:22%; bottom:4%; transform:rotate(184deg);}
.glove-inner{width:100%; height:100%; animation:gloveTap .9s ease-in-out infinite;}
.glove-inner svg{width:100%; height:100%; image-rendering:pixelated;}
@keyframes gloveTap{50%{transform:translateY(-13px);}}
.intro-bubble{position:absolute; left:50%; bottom:18%; transform:translateX(-50%); width:80vw; max-width:420px; box-sizing:border-box;
  background:var(--cream); color:var(--ink); border:3px solid var(--ink); padding:14px 16px; box-shadow:5px 5px 0 #1b1510;
  font-size:13px; line-height:1.5; text-align:center; min-height:56px;}
.intro-cur{display:inline-block; width:8px; height:14px; background:var(--ink); vertical-align:middle; animation:blinkCur .5s steps(1) infinite;}
@keyframes blinkCur{50%{opacity:0;}}
.intro-hint{font-size:9px; opacity:.55; margin-top:9px; letter-spacing:1px;}
/* --- card LEVEL-UP crystal/lightning explosion --- */
.lvlfx{position:fixed; inset:0; z-index:250; display:flex; align-items:center; justify-content:center; pointer-events:none;}
.lvlfx-burst{position:absolute; width:22px; height:22px; border-radius:50%; background:radial-gradient(circle,#fff 0,#ffe08a 40%,transparent 70%); animation:lvlBurst .55s ease-out forwards;}
@keyframes lvlBurst{0%{transform:scale(.2);opacity:1}100%{transform:scale(46);opacity:0}}
.lvlfx-rays{position:absolute; width:340px; height:340px; opacity:.9;
  background:conic-gradient(from 0deg,transparent 0 7deg,#ffe08a99 7deg 11deg,transparent 11deg 30deg,#7ab6ff99 30deg 34deg,transparent 34deg 60deg,#e26bd099 60deg 64deg,transparent 64deg 90deg);
  animation:lvlRays 1.3s ease-out forwards;}
@keyframes lvlRays{0%{transform:rotate(0) scale(.3);opacity:0}18%{opacity:1}100%{transform:rotate(80deg) scale(1.7);opacity:0}}
.lvlfx-shards{position:absolute; width:0; height:0;}
.lvlfx-shards i{position:absolute; width:9px; height:16px; margin:-8px 0 0 -4px; background:linear-gradient(#fff,#7ab6ff);
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%); transform:rotate(var(--a)) translateY(-10px) scale(0); animation:lvlShard .95s ease-out forwards;}
@keyframes lvlShard{0%{transform:rotate(var(--a)) translateY(-10px) scale(0);opacity:1}100%{transform:rotate(var(--a)) translateY(-150px) scale(1.1);opacity:0}}
.lvlfx-bolt{position:absolute; width:60px; height:170px; filter:drop-shadow(0 0 10px #ffe08a); animation:lvlBolt .5s steps(2) forwards; opacity:0;}
@keyframes lvlBolt{0%{opacity:0;transform:scaleY(.4)}20%{opacity:1}60%{opacity:1}100%{opacity:0;transform:scaleY(1)}}
.lvlfx-txt{position:relative; text-align:center; animation:lvlTxt 1.35s ease-out forwards;}
.lvlfx-lv{font-family:var(--fontD); font-size:27px; color:#fff; text-shadow:0 0 14px #ffe08a,3px 3px 0 #1b1510;}
.lvlfx-nm{font-family:var(--fontD); font-size:12px; color:#ffe08a; margin-top:6px; text-shadow:2px 2px 0 #1b1510;}
@keyframes lvlTxt{0%{transform:scale(0) rotate(-8deg);opacity:0}28%{transform:scale(1.22) rotate(3deg);opacity:1}48%{transform:scale(1) rotate(0)}82%{opacity:1}100%{transform:scale(1.12);opacity:0}}
/* keep the D-pad full-size on phones (only a mild shrink on the very narrowest handsets) */
/* ⚠⚠ v0.836 — THIS MAY NEVER TOUCH THE DUAL-PAD MODE, AND THAT IS WHY IT IS SCOPED.
   It shrank .gp-d and left .gp-d2 alone, so on a screen this narrow the two pads would have been
   DIFFERENT SIZES — the exact fault the universal rule exists to prevent. It is also the wrong tool
   there: #gpad.qte2 already caps both pads at 49vw, which handles a narrow screen for the pair
   together, and a scale on top of that cap would shrink the left one twice. Scoped to the single-pad
   layout, where .gp-d2 is display:none and there is nothing to stay level with.
   (344px is narrower than any screen this game supports — 375 folded — so this is a safety rail
   that does not fire in practice. Kept as one.) */
@media (max-width:344px){
  #gpad:not(.qte2) .gp-d{transform:scale(.86); transform-origin:bottom left;}
  #gpad:not(.qte2) .gp-ab{transform:scale(.86); transform-origin:bottom right;}
}
/* ⭐⭐⭐ UNIVERSAL D-PAD (Odaroo, 2026-08-18) — THE CONTROL DECK IS ONE SIZE IN EVERY GAME.
   The cross is 188px * --gs everywhere and the deck that holds it is 240px * --gs everywhere. The
   battle deck used to be 222px, i.e. 18px shorter than every other screen in the game, so walking
   into OCTATHON moved the pad under the thumb that was already resting on it. There is exactly ONE
   pad in this game and exactly one size for it: no screen, no game and no mode may restate either
   number. The only thing a mode may change is WHETHER the right-hand pad is shown (#gpad.qte2),
   and that pad is an exact mirror of the left one. Do not add a game-specific height here. */
/* Bit-Brawl keeps the SAME full-size HUD D-pad — just raised so it clears the card dock (no shrink) */
/* BIT-BRAWL restage: the D-pad stays in its normal fixed bottom-left position as a SOLID BLACK control deck (no longer floated up over the arena). The battle content stacks cleanly above it via #scr-battle padding-bottom. */
#gpad.inb{bottom:0; height:calc(240px * var(--gs,1) + var(--sab,0px) + var(--padny,0px) + var(--dpadup,0px) * var(--gs,1)); z-index:20; opacity:1!important; background:var(--ink); box-shadow:0 -3px 0 #000, 0 -5px 16px #0008;}   /* !important beats the inline gpad-opacity setting so the battle deck is solid */
#gpad.inb .gp-mid{display:flex;}   /* SELECT/START are part of the HUD — they stay in battle too (the deck has room) */
/* room so the pad never hides content */
#menuStack,#gigsList,#achList,#arcadeStage,#packStage,.grid{padding-bottom:calc(248px * var(--gs,1) + var(--sab,0px) + var(--padny,0px))!important;}   /* v0.381 — the clearance now tracks the pad, which is itself sized off --gs */
#rinkWrap{margin-bottom:104px;}   /* the rink floats well clear of the D-pad */
#rinkPad{display:none!important;}
/* bitbuddy town */
#townStage{flex:1; min-height:0; position:relative; z-index:5; display:flex; align-items:center; justify-content:center; overflow:hidden;}
#townMap{position:relative;}
.ttile{position:absolute;}
.tspr{position:absolute; z-index:3; transition:left .17s linear, top .17s linear; cursor:pointer;}
.tspr svg{width:100%; height:100%; display:block;}
.tname{position:absolute; left:50%; transform:translateX(-50%); top:-14px; font-size:9px; background:#1b1510cc; color:var(--cream); padding:1px 3px; white-space:nowrap; z-index:4;}
.tflip svg{transform:scaleX(-1);}
/* bullet ballet */
@keyframes hellDream{from{filter:hue-rotate(0deg)}to{filter:hue-rotate(70deg)}}
.hb{position:absolute; border-radius:50%; background:#141018; box-shadow:0 0 5px #000c, 0 0 0 1.5px #ffffff55; pointer-events:none; display:none;}   /* enemy bullets are BLACK now */
.hb.r{background:var(--red); box-shadow:0 0 6px #c93a22bb;}
.hb.b{background:#7ab6ff; box-shadow:0 0 6px #7ab6ffbb;}   /* the costumed star at 50% */
.hpilot{width:100%; height:100%; transition:transform .1s ease-out; transform-origin:50% 70%;}
.hpilot.lean-l{transform:rotate(-8deg);} .hpilot.lean-r{transform:rotate(8deg);} .hpilot.front{transform:scale(1.12);}   /* v0.574 — a gentle bank only; the TURN is a real side sprite now, not a squeezed front one */
.hpilot.face-up{transform:scale(1.02);}
.hpfloat{position:relative; width:100%; height:100%; transform-origin:50% 100%; animation:hpFloat 1.4s ease-in-out infinite;}
.hpilot.bouncing .hpfloat{animation:hpBounce .62s cubic-bezier(.3,0,.4,1) infinite;}   /* Poring hop — squash & stretch when moving */
.hp-front,.hp-back,.hp-side{position:absolute; inset:0;}
.hp-back,.hp-side{display:none;} .hpilot.face-up .hp-front{display:none;} .hpilot.face-up .hp-back{display:block;}
/* v0.574 — the four facings. Only ever ONE is displayed, so the pilot cannot double-expose. */
.hpilot.lean-l .hp-front{display:none;} .hpilot.lean-l .hp-left{display:block;}
.hpilot.lean-r .hp-front{display:none;} .hpilot.lean-r .hp-right{display:block;}
.hpfloat svg{width:100%; height:auto; display:block; overflow:visible; filter:drop-shadow(0 2px 4px #0006);}
@keyframes hpFloat{50%{transform:translateY(-5px)}}
@keyframes hpBounce{
  0%{transform:translateY(0) scaleX(1.1) scaleY(.9);}      /* land — squash */
  30%{transform:translateY(-9px) scaleX(.92) scaleY(1.1);} /* leap — stretch */
  60%{transform:translateY(-3px) scaleX(1) scaleY(1);}
  100%{transform:translateY(0) scaleX(1.1) scaleY(.9);}
}
.hwaveov{position:absolute; left:0; right:0; top:42%; text-align:center; z-index:9; font-family:var(--fontD);
  font-size:19px; color:#1b1510; text-shadow:2px 2px 0 #fdf6e3; animation:hwFade 1s ease-in-out forwards; pointer-events:none;}
@keyframes hwFade{from{opacity:0; transform:scale(.7)}30%{opacity:1; transform:scale(1.05)}80%{opacity:1}to{opacity:0}}
.hb.p{background:#ff7ab6; box-shadow:0 0 9px #ff7ab6cc;}
.hb.ink{background:#1b1510; box-shadow:0 0 10px #000a, 0 0 0 2px #fff9; border-radius:60% 60% 60% 20%;}
@keyframes hgSlide{from{transform:translateX(-34px)}to{transform:translateX(34px)}}
.he.spin{animation:foeSpin 2.4s linear infinite;}
@keyframes foeSpin{to{transform:rotate(360deg)}}
/* LANDSCAPE gate: flag — RING — flag, threaded by a faint SHINY pass-line. Cross it THROUGH the loop. */
.hoop{position:absolute; z-index:3; pointer-events:none; display:flex; align-items:center; justify-content:space-between; gap:0; filter:drop-shadow(0 0 5px #fff9);}
/* the glitchy oval PORTAL checkpoint — fly INTO it */
.hoop.hportal{display:block; border-radius:50%; border:3px solid #8ff0ff; background:radial-gradient(ellipse at 50% 45%,#c47bff44,#7ef0ff1c 55%,transparent 72%); box-shadow:0 0 16px #7ef0ffaa, inset 0 0 20px #b06bff77; animation:hportGlitch .42s steps(2) infinite; filter:none;}
.hoop.hportal .hportal-in{position:absolute; inset:5px; border-radius:50%; border:1px dashed #ffffff88; opacity:.7; animation:hportSpin 2.4s linear infinite;}
@keyframes hportGlitch{0%{filter:drop-shadow(2px 0 0 #ff3d6e) drop-shadow(-2px 0 0 #3dd6ff)}50%{filter:drop-shadow(-2px 0 0 #ff3d6e) drop-shadow(2px 0 0 #3dd6ff) hue-rotate(40deg)}100%{filter:drop-shadow(2px 0 0 #ff3d6e) drop-shadow(-2px 0 0 #3dd6ff)}}
@keyframes hportSpin{to{transform:rotate(360deg)}}
.hoop.hportal.hoop-got{animation:none; box-shadow:0 0 22px #7ef0a0; border-color:#7ef0a0;}
.hoop .hp-line{position:absolute; left:3%; right:3%; top:50%; height:2px; margin-top:-1px; opacity:.7; pointer-events:none;
  background:linear-gradient(90deg,transparent,#ffffffaa 18%,#ffe58a 50%,#ffffffaa 82%,transparent);
  box-shadow:0 0 7px #ffe58acc; animation:passShim 1.1s ease-in-out infinite;}
@keyframes passShim{0%,100%{filter:brightness(.7); opacity:.5}50%{filter:brightness(1.6); opacity:.95}}
.hoop .hp-ring{flex:none; position:relative; z-index:1; animation:ringGlow 1.2s ease-in-out infinite;}
/* the CHECKPOINT stripe — yellow, but it dreams in two other colours and glitches */
.hoop .hg-stripe{flex:1; height:12px; align-self:center; background:#f4c542; border:2px solid #1b1510; image-rendering:pixelated;
  animation:hgGl 2.4s steps(1) infinite;}
@keyframes hgGl{0%,64%{background:#f4c542; transform:none}68%{background:#7fe6ff}72%{background:#f4c542}84%{background:#ff6ba8; transform:skewX(9deg)}88%{background:#f4c542; transform:none}}
/* THE CLIMB — scene backdrops: sky → space → quantum → tunnel → glitch hell */
.hoop .hp-flag{flex:none; width:28px; height:32px; position:relative; z-index:1; animation:flagWave 1.1s ease-in-out infinite;}
.hoop .hp-flag.hf-r{animation-delay:.55s;}
.hoop .hp-flag svg,.hoop .hp-ring svg{width:100%; height:100%; display:block;}
@keyframes flagWave{0%,100%{transform:rotate(-5deg)}50%{transform:rotate(5deg)}}
@keyframes ringGlow{0%,100%{filter:drop-shadow(0 0 3px #f4c542aa)}50%{filter:drop-shadow(0 0 9px #f4c542)}}
.hoop.hoop-got{filter:drop-shadow(0 0 16px #7ef0a0);}
.hoop.hoop-got .hp-ring{animation:ringPop .45s ease-out forwards;}
.hoop.hoop-got .hp-flag{animation:flagParty .45s ease-in-out 2;}
@keyframes ringPop{40%{transform:scale(1.5); opacity:1}100%{transform:scale(2.1); opacity:0}}
@keyframes flagParty{0%,100%{transform:rotate(-16deg) translateY(0)}50%{transform:rotate(16deg) translateY(-7px) scale(1.25)}}
/* swallow BEAM — a bright column that erupts up from the ship after swallowing the pink */
.hbeam{position:absolute; z-index:5; pointer-events:none; width:26px; margin-left:-13px; top:0; transform-origin:top center;
  background:linear-gradient(0deg,#fff,#7fe6ff 30%,#4aa8ff 70%,#4aa8ff00); box-shadow:0 0 22px #7fd4ffcc, 0 0 44px #4aa8ff88; border-radius:0 0 13px 13px; animation:beamPulse .12s steps(2) infinite;}
/* ▲ BEAM — the third pad button, a triangle perched between-and-above B and A (ballet only) */
/* X — the third button; A upper-right + B lower-left + X top-centre = a perfect three-circle triangle */
.gpx{display:none; position:absolute; left:2px; bottom:88px; z-index:2;}   /* v3.97 — B and A NEVER move (classic Game Boy diagonal, same in every game); the X simply parks above B and beside A: B(38,36) A(112,82) X(38,124) = an even triangle */   /* taller box ONLY so the X has room — A and B are anchored to the bottom, so they do not budge */
.gpx.chg{background:#123047!important; border-color:#7fe6ff!important; color:#7fe6ff;}   /* the charged X GLOWS in place — it must never scale (the old pulse grew it 18% and made it jump) */
.gpbeam{display:none; position:absolute; left:auto; right:92px; bottom:58px; width:60px; height:60px;   /* the X sits BETWEEN B and A — an even triangle off their two anchors */
  background:transparent; border:none; padding:0; pointer-events:auto; touch-action:manipulation; cursor:pointer;}
.gpbeam svg{width:66px; height:52px; filter:drop-shadow(0 3px 0 #00000055); display:block; margin:0 auto;}
.gpbeam .gpbeam-lbl{display:block; font-family:var(--fontD); font-size:8px; color:#f2e5c9; opacity:.75; text-align:center; margin-top:1px; letter-spacing:1px;}
.gpbeam .gb-bolt{opacity:.25;}
.gpbeam.chg .gb-bolt{opacity:1;}
.gpbeam.chg svg path.gb-tri{fill:#123047; stroke:#7fe6ff;}
.gpbeam.chg{animation:pulse .45s infinite; filter:drop-shadow(0 0 10px #7fd4ffcc);}
.gpbeam:active svg{transform:translateY(2px);}
.hb.lava{background:radial-gradient(circle at 35% 30%, #ffd27a, #f4732c 55%, #c9330e); border-radius:44%; box-shadow:0 0 12px #f4732cbb; animation:lavaBub .7s ease-in-out infinite alternate;}
@keyframes lavaBub{from{transform:scale(1)}to{transform:scale(1.12)}}
@keyframes beamPulse{0%{filter:brightness(1)}100%{filter:brightness(1.4)}}
/* SCENE power-up chooser */
.puprow{display:flex; gap:8px; justify-content:center; margin:12px 0 4px; flex-wrap:wrap;}
.pupbtn{flex:1; min-width:88px; max-width:120px; background:var(--paper2); border:3px solid var(--ink); border-radius:10px; padding:10px 6px; cursor:pointer; box-shadow:3px 3px 0 #1b151088; transition:transform .12s;}
.pupbtn:hover,.pupbtn.gpsel{transform:translateY(-4px); background:var(--gold); box-shadow:0 0 0 3px var(--gold2), 4px 4px 0 #000a;}
.pup-ic{width:38px; height:38px; margin:0 auto 4px;}
.pup-ic svg{width:100%; height:100%;}
.pup-nm{font-family:var(--fontD); font-size:11px; color:var(--ink);}
.hboss{position:absolute; z-index:4; pointer-events:none; margin-left:-60px; margin-top:-60px; filter:drop-shadow(0 0 14px #ff7ab6); animation:bossPulse 1.5s ease-in-out infinite;}
.hboss svg{width:100%; height:100%;}
.hboss.bhit{filter:drop-shadow(0 0 14px #fff) brightness(2.4);}
@keyframes bossPulse{50%{transform:scale(1.05)}}
@keyframes ringSpin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
/* (scrH/scrV grid scroll retired with the square grill) */
@keyframes hellScan{from{transform:translateY(-60px)}to{transform:translateY(860px)}}
.hstar{position:absolute; z-index:1; pointer-events:none; opacity:.34;
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%); background:#4a6fa5; animation:starDrift linear infinite;}
@keyframes starDrift{from{transform:translate(0,0)}to{transform:translate(-680px,40px)}}
.hs{position:absolute; width:19px; height:28px; background:none; border-radius:0; box-shadow:none;
  pointer-events:none; z-index:4; display:none; filter:drop-shadow(0 0 3px #ffffffcc);}   /* the inklet fires ≫ chevrons pointing UP */
.hs svg{width:100%; height:100%; display:block;}
.hs.hom{width:15px; height:15px; border-radius:50%; background:#ff6ba8; box-shadow:0 0 12px #ff6ba8, 0 0 20px #ffd66b; animation:homSpin .5s linear infinite;}
.hs.hom svg{display:none;}
/* --- jolly TRIPPY clouds (no eyes) — puffy, hue-drifting, gently breathing --- */
.hcloud{position:absolute; z-index:1; pointer-events:none; image-rendering:pixelated; opacity:.85; will-change:transform;}
.hcloud svg{width:100%; height:100%; display:block; animation:cloudTrip var(--trip,7s) linear infinite, cloudPuff 3.1s ease-in-out infinite;}
@keyframes cloudTrip{from{filter:hue-rotate(0deg) saturate(1.3)}to{filter:hue-rotate(360deg) saturate(1.3)}}
@keyframes cloudPuff{0%,100%{transform:scale(1)}50%{transform:scale(1.07) rotate(1.5deg)}}
@keyframes cloudFall{from{transform:translateY(0)}to{transform:translateY(var(--fall,640px))}}
.hcloud.spooky{filter:brightness(.6);}
/* --- ballet-mannequin & bullet STATUES posing in the background --- */
.hstatue{position:absolute; z-index:1; pointer-events:none; opacity:.22; image-rendering:pixelated; animation:statueSway var(--sway,5s) ease-in-out infinite;}
.hstatue svg{width:100%; height:100%; display:block;}
@keyframes statueSway{0%,100%{transform:rotate(-2.5deg) translateY(0)}50%{transform:rotate(2.5deg) translateY(-5px)}}
/* --- TUNNEL scene walls — the sides squeeze in, ring by ring --- */
.htun{position:absolute; top:0; bottom:0; z-index:2; pointer-events:none; transition:width .6s ease;
  background:repeating-linear-gradient(45deg,#1b1510 0 12px,#3a2a3f 12px 24px); box-shadow:inset 0 0 0 3px #1b1510, 0 0 14px #0009;}
#htunL{left:0; border-right:4px solid #f4c542;}
#htunR{right:0; border-left:4px solid #f4c542;}
/* --- maze ASTEROIDS — stone bullets in careful formation --- */
.hb.ast{background:#6b6b76; border-radius:28%; box-shadow:inset -2px -3px 0 #4a4a55, inset 2px 2px 0 #9a9aa8, 0 0 4px #0008;}
/* --- foe 2-frame animation + tiny HP bar --- */
.he{overflow:visible;}
.he .ffr{position:absolute; inset:0;}
.he .ffr svg{width:100%; height:100%; display:block;}
.he .fa{animation:foeFr2a .52s steps(1) infinite;}
.he .fb{animation:foeFr2b .52s steps(1) infinite;}
@keyframes foeFr2a{0%,49%{opacity:1}50%,100%{opacity:0}}
@keyframes foeFr2b{0%,49%{opacity:0}50%,100%{opacity:1}}
.he .fhp{position:absolute; left:6%; right:6%; top:-8px; height:4px; background:#00000088; border:1px solid #1b1510; border-radius:2px; overflow:hidden;}
.he .fhp i{display:block; height:100%; background:linear-gradient(90deg,#8ee87a,#3aa84a); transition:width .15s;}
.he.hurtful .fhp i{background:linear-gradient(90deg,#f4c542,#c93a22);}
/* --- magic slow-time clock --- */
.hclock{position:absolute; width:32px; height:32px; z-index:5; pointer-events:none; image-rendering:pixelated;
  filter:drop-shadow(0 0 6px #f4c542) drop-shadow(0 0 12px #fff7);}
.hclock svg{width:100%; height:100%; display:block; animation:clkPulse .5s steps(2) infinite;}
@keyframes clkPulse{0%{transform:scale(1)}100%{transform:scale(1.14)}}
.scrV.slowmo{filter:saturate(1.3) hue-rotate(200deg) brightness(1.05); transition:filter .3s;}
.scrV.slowmo::after{content:''; position:absolute; inset:0; z-index:2; pointer-events:none; background:radial-gradient(circle at 50% 45%, #7fd4ff00 55%, #2a6cff33 100%); mix-blend-mode:screen;}
/* --- boss wave: the stage turns chaotic + hellish --- */
.scrV.chaos{animation:chaosShake .4s steps(2) infinite;}
.scrV.chaos::before{content:''; position:absolute; inset:0; z-index:2; pointer-events:none; background:radial-gradient(circle at 50% 40%, #ff2b0033 0%, #6b000055 60%, #1a000088 100%); mix-blend-mode:multiply; animation:chaosPulse 1.1s ease-in-out infinite;}
@keyframes chaosShake{0%{transform:translate(0,0)}25%{transform:translate(-2px,1px)}50%{transform:translate(2px,-1px)}75%{transform:translate(-1px,2px)}100%{transform:translate(1px,-1px)}}
@keyframes chaosPulse{0%,100%{opacity:.55}50%{opacity:.95}}
@keyframes homSpin{to{filter:hue-rotate(360deg)}}
.hb.vac{filter:brightness(1.5); box-shadow:0 0 12px #ff7ab6;}
@keyframes vacPulse{50%{transform:scale(1.12); border-color:#ffd66b}}
.he{position:absolute; z-index:4; pointer-events:none; display:none;}
.he svg{width:100%; height:100%; display:block;}
.mtile.wide{grid-column:span 6; min-height:66px; flex-direction:column!important; gap:1px!important;}   /* stack icon/label/JP on narrow so nothing clips */
.mtile.wide .menu-label{font-size:12px!important;}
.mtile.wide .jplab{margin-left:0!important; font-size:8px;}
/* pack overlay */
/* ⭐ v0.390 — THE HAUL CLEARS THE D-PAD. The overlay filled the whole screen and centred its cards in
   it, but the gamepad (z460) sits ON TOP of it (z59) — so on the folded Fold the bottom row of carts
   and the "tap to continue" line were painted straight underneath the controls. The overlay now
   reserves the pad's exact height at the bottom, so everything it centres is centred in the space the
   player can actually SEE. Same var()s the pad itself is built from, so the two can never drift. */
#packOverlay{position:absolute; inset:0; z-index:59; background:#1b1510f0; display:none;
  flex-direction:column; align-items:center; justify-content:center; gap:14px;
  padding:16px 16px calc(248px * var(--gs,1) + var(--sab,0px)); overflow-y:auto;}
#packOverlay.on{display:flex;}
.pk-cart{width:130px; height:142px; animation:cartRumble .5s ease-in-out infinite;}
@keyframes cartRumble{0%,100%{transform:rotate(-3deg)}25%{transform:translateY(-5px)}50%{transform:rotate(3deg)}75%{transform:translateY(-3px)}}
.pk-cards{display:flex; gap:10px; flex-wrap:wrap; justify-content:center; max-width:400px;}
.pk-big{width:112px;}   /* flip wrapper handles the reveal now */
/* a big haul has to fit the shorter space the pad leaves behind — three rows of full-size carts do not */
.pk-cards.many{gap:7px; max-width:470px;}
.pk-cards.many .pk-big{width:88px;}
@media (max-width:400px){ .pk-cards.many .pk-big{width:78px;} }
.cflip .card{width:100%;} .cflip-front{width:100%;}
.cf-single{width:158px;}
@media (max-width:400px){ .cf-single{width:140px;} }
/* ---- old-school pack RIP + one-at-a-time reveal (#28) ---- */
.pk-scan{position:absolute; inset:0; z-index:1; pointer-events:none; opacity:.5;
  background:repeating-linear-gradient(0deg,#0000 0 2px,#00000022 2px 4px);
  mix-blend-mode:multiply; animation:pkScan 5s linear infinite;}
@keyframes pkScan{to{background-position:0 200px;}}
.pk-riphold{position:relative; width:150px; height:160px; z-index:2;}
.pk-riphold > div{position:absolute; left:50%; top:50%; width:130px; height:142px; margin:-71px 0 0 -65px;}
.pk-cart.ripping{animation:cartRumbleHard .55s ease-in-out forwards;}
.pk-cart.ripping.lgd{filter:drop-shadow(0 0 10px var(--gold));}
@keyframes cartRumbleHard{0%{transform:rotate(-4deg)}20%{transform:rotate(5deg) scale(1.05)}40%{transform:rotate(-6deg) translateX(3px)}60%{transform:rotate(6deg) scale(1.07) translateX(-3px)}80%{transform:rotate(-4deg)}100%{transform:scale(1.12); opacity:0}}
.pk-cart.glchip{animation:cartRumbleHard .55s ease-in-out forwards, pkGl .12s steps(2) infinite;}
@keyframes pkGl{0%{clip-path:inset(0 0 0 0)}50%{clip-path:inset(22% 0 34% 0); transform:translateX(-4px)}100%{clip-path:inset(0 0 0 0)}}
.pk-tearL,.pk-tearR,.pk-burst2{opacity:0;}
.pk-riphold.torn .pk-cart{display:none;}
.pk-riphold.torn .pk-tearL{opacity:1; clip-path:polygon(0 0,54% 0,44% 26%,56% 52%,44% 78%,54% 100%,0 100%); animation:tearL .55s ease-out forwards;}
.pk-riphold.torn .pk-tearR{opacity:1; clip-path:polygon(54% 0,100% 0,100% 100%,54% 100%,44% 78%,56% 52%,44% 26%); animation:tearR .55s ease-out forwards;}
@keyframes tearL{to{transform:translateX(-125%) rotate(-24deg);}}
@keyframes tearR{to{transform:translateX(125%) rotate(24deg);}}
.pk-riphold.torn .pk-burst2{animation:pkBurst .6s ease-out forwards;}
.pk-burst2{background:radial-gradient(circle,#fff 0,var(--gold) 38%,transparent 68%); border-radius:50%;}
@keyframes pkBurst{0%{opacity:0; transform:scale(.2)}25%{opacity:1}100%{opacity:0; transform:scale(4)}}
.pk-count{position:relative; z-index:2; font-family:var(--fontD); font-size:15px; color:var(--gold); letter-spacing:1px; text-shadow:2px 2px #000;}
.pk-pull{position:relative; z-index:2; display:flex; align-items:center; justify-content:center;}
.pk-flash{position:absolute; inset:-30px; pointer-events:none; opacity:0; border-radius:50%;}
.pk-flash.on{animation:pkFl .55s ease-out;}
.pk-flash.r3{background:radial-gradient(circle,#e6b8ffcc,transparent 62%);}
.pk-flash.r4{background:radial-gradient(circle,#fff,var(--gold) 34%,transparent 66%);}
.pk-flash.r1,.pk-flash.r2{background:radial-gradient(circle,#ffffffcc,transparent 60%);}
@keyframes pkFl{0%{opacity:.95; transform:scale(.4)}100%{opacity:0; transform:scale(1.3)}}
.pk-single{width:158px; animation:pkPull .5s var(--pop) backwards;}
@keyframes pkPull{0%{transform:translateY(70px) scale(.55) rotateX(65deg); opacity:0}55%{opacity:1}100%{transform:none; opacity:1}}
.pk-single.r3{animation:pkPull .5s var(--pop) backwards, pkGlow 1.1s ease-in-out infinite alternate .5s;}
.pk-single.r4{animation:pkPull .55s var(--pop) backwards, pkGlowG 1s ease-in-out infinite alternate .55s;}
@keyframes pkGlow{to{filter:drop-shadow(0 0 12px #d06bffdd)}}
@keyframes pkGlowG{to{filter:drop-shadow(0 0 18px var(--gold))}}
.pk-single.glx::after{content:''; position:absolute; inset:0; z-index:5; pointer-events:none; mix-blend-mode:screen;
  background:linear-gradient(90deg,#ff004055,transparent 45%,transparent 55%,#00e6ff55); animation:glxSplit .22s steps(2,end) infinite;}
@keyframes glxSplit{0%{transform:translate(-3px,1px); opacity:.9}50%{transform:translate(3px,-1px); opacity:.5}100%{transform:translate(-3px,1px); opacity:.9}}
@media (max-width:400px){ .pk-big{width:96px;} .pk-single{width:140px;} }
@media (max-height:640px){
  .title-xl{font-size:42px} #titleArt{width:150px;height:125px}
  .menu-card{padding:9px 14px} .menu-art{width:46px;height:46px}
}

/* ===== ⭐ v3.72 NIM — one corner inkling, a CPU opponent, a real coin flip ===== */
/* THE COIN — a real two-faced disc tumbling end over end, then landing on a face */
.nim-coin3d.land{animation:nimFlipLand .7s cubic-bezier(.2,.8,.3,1) forwards;}   /* it SLOWS and settles */
.nim-coin3d.lands-cpu.land{animation:nimFlipLandC .7s cubic-bezier(.2,.8,.3,1) forwards;}

/* ⚠ v0.815 — LUMEN INK's top-right TRACK n/6 tag is DELETED (Odaroo: it was redundant with the
   pips, and the pips are gone too). Nothing is printed around that playfield. */
/* ===== ⭐ v3.72 BULLET BALLET — the splash card's waving flags ===== */
.hb-splashflag{position:absolute; top:50%; width:44px; height:44px; margin-top:-40px; z-index:3; pointer-events:none;
  animation:nimFlagWave 1.05s ease-in-out infinite;}
.hb-splashflag svg{width:100%; height:100%; display:block; image-rendering:pixelated;}
.hb-splashflag.l{left:8%;}
.hb-splashflag.r{right:8%; animation-delay:.32s;}
@media (min-width:640px){ .hb-splashflag{width:60px; height:60px;} .hb-splashflag.l{left:20%;} .hb-splashflag.r{right:20%;} }

/* ===== ⭐ v3.73 CARTS ALBUM ===== */
/* the one-liner deck hint (AUTO BEST / AUTO FILL retired) */
.deckHint{display:flex; align-items:center; justify-content:center; gap:7px; padding:2px 0 8px;
  font-family:var(--fontD); font-size:9px; letter-spacing:2px; color:var(--ink); opacity:.75;}
.deckHint .dh-arrow{display:inline-block; animation:dhArrow 1.1s ease-in-out infinite; font-size:12px;}
@keyframes dhArrow{0%,100%{transform:translateY(2px)}50%{transform:translateY(-3px)}}
/* the ALBUM backdrop — world stamps + the cast's silhouettes, scattered like a collector's book */
/* ⭐ v0.437 — THE WALLPAPER STEPS BACK. Odaroo: the city stamps were competing with the cards for
   attention. They are decoration and the grid is the content, so they lose contrast rather than the
   grid gaining it — lower opacity, a soft blur and a touch of desaturation put them behind the glass
   instead of on it. Nothing is removed; the passport still reads, it just stops shouting. */
#collDeco{position:absolute; inset:0; z-index:1; overflow:hidden; pointer-events:none;
  opacity:.26; filter:blur(2.4px) saturate(.55);}
.cd-stamp{position:absolute; padding:4px; background:#fdf6e3; border:2px dotted #b8a888; border-radius:2px;
  box-shadow:1px 2px 0 #1b151022; transform:rotate(var(--rot,0deg));}   /* dotted border = the perforated stamp edge */
.cd-stamp .cds-in{border:1px dashed #8a7a5f; padding:3px 5px; font-family:var(--fontD); font-size:6px; letter-spacing:1px; color:#6e5c40; text-align:center;}
.cd-stamp .cds-in svg{width:26px; height:26px; display:block; margin:0 auto 1px; image-rendering:pixelated;}
/* ⭐ v0.393 — the MART wears the same wall, in souvenir-shop colours (warmer paper, a shop-tag tint) */
#martDeco{position:absolute; inset:0; z-index:1; overflow:hidden; pointer-events:none; opacity:.55;}
.md-stamp{background:#fbf0d8; border-color:#c0a071;}
.md-stamp .cds-in{border-color:#9c7f52; color:#7a5f38;}
#collGrid,.grid{position:relative; z-index:5;}
/* ===== ⭐ v3.73 THE 3D CARD INSPECTOR — hold any card to study it like a real TCG pull ===== */
#inspectOv{position:fixed; inset:0; z-index:9992; display:flex; flex-direction:column; align-items:center; justify-content:center;
  background:#0d0a08e8; touch-action:none;}
#inspectCard{width:min(72vw,300px); transform-style:preserve-3d; will-change:transform; position:relative;
  transition:transform .12s ease-out;}
#inspectCard .card{width:100%; font-size:1.15em; box-shadow:0 18px 44px #000c;}
#inspectCard .glare{position:absolute; inset:0; z-index:9; pointer-events:none; border-radius:inherit; mix-blend-mode:screen;
  background:radial-gradient(circle at var(--gx,50%) var(--gy,40%), #ffffff55 0%, #ffffff18 26%, transparent 58%);}
#inspectCard.holo .holo{position:absolute; inset:0; z-index:8; pointer-events:none; mix-blend-mode:color-dodge; opacity:.5;
  background:linear-gradient(115deg, transparent 20%, #ff004466 36%, #ffee0066 44%, #00ff8866 52%, #00ccff66 60%, #cc00ff66 68%, transparent 84%);
  background-size:220% 220%; background-position:var(--hx,50%) var(--hy,50%);}
#inspectStory{max-width:min(84vw,340px); margin:14px 8px 0; text-align:center; font-size:11px; line-height:1.7; color:#e8dfc8;
  font-style:italic;}
#inspectHint{margin-top:8px; font-family:var(--fontD); font-size:8px; letter-spacing:2px; color:#9a8f78;}
#inspectBtns{display:flex; gap:8px; margin-top:12px;}
/* ===== ⭐ v3.73 THE 2x INK BUBBLE — a comic-book shout on every unplayed-today game tile ===== */
.x2badge{position:absolute; top:26px; right:3px; z-index:6; width:44px; height:34px; pointer-events:none;
  animation:x2pulse 1.15s ease-in-out infinite;}
/* ⭐⭐⭐ v0.859 — NO `filter` ON THIS BADGE. It carried `drop-shadow(1px 2px 0 #1b151066)` while
   scaling and rotating on a 1.15s loop, and a filter on an animating element is a COMPOSITING
   BLOCKER — Blink re-rasterises the shadow on the main thread on every frame instead of handing
   the transform to the GPU. Same family as the sunburst (css/wjam.css) and as v0.832's rule about
   a shape inside an <svg>: the cost is not the movement, it is what the movement is forced through.
   The shadow is DRAWN now, not filtered — a second copy of the star offset by (1,2) underneath the
   real one (js/cards.js, x2svg) plus a `text-shadow` on the "2x" below. Identical pixels.
   ⚠ This does NOT reverse v0.834 — the "2x" stays an HTML span, for the reason recorded there. */
@keyframes x2pulse{0%,100%{transform:scale(1) rotate(-6deg)}50%{transform:scale(1.12) rotate(2deg)}}
/* ⚠⚠⚠ v0.834 — THE "2x" IS HTML, BECAUSE AN SVG <text> UNDER A SCALE COSTS A FULL PAGE LAYOUT A
   FRAME. See the note on x2svg in js/cards.js: this one text node was measured at 240 layout/s on
   scr-home while every other screen in the game scored 0, and it is the flicker Odaroo has been
   filming on the unfolded Fold 8 (a layout costs more on a bigger screen, which is why that fold is
   the one he sees it on). The star stays SVG — a <polygon> under a scale is free.
   ⚠ The two children are stacked, not laid out: the span must not be able to move the svg. */
.x2badge>svg{position:absolute; inset:0; width:100%; height:100%;}
.x2badge .x2lab{position:absolute; inset:0; display:grid; place-items:center; z-index:1;
  font-family:var(--fontD); font-size:12px; font-weight:bold; color:#c93a22; line-height:1;
  letter-spacing:0; pointer-events:none; text-shadow:1px 2px 0 #1b151066;}
.mtile.quad .x2badge{top:30px;}

/* ===== ⭐ v3.76 — THE ×10 PERFECT COUNTER CAN'T MOVE THE BOARD =====
   #qte lays out as a column with the play field on flex:1 — so any HUD line that GREW handed the board less
   room and visibly shrank the whole minigame mid-combo. Both HUD lines are fixed-height forever now, and the
   PERFECT counter occupies its two stacked lines (PERFECT / ×N) from the very first note, invisible until
   it earns itself at ×10. Nothing below the board can ever change size again. */
.ddr-subline{flex:0 0 auto; height:20px; overflow:hidden; white-space:nowrap;}
#ddrPerf{flex:0 0 auto; display:block; height:38px; overflow:hidden; opacity:0; color:var(--gold); line-height:1.25;}
#ddrPerf.on{opacity:1; transition:opacity .25s;}   /* a plain fade — the counter never scales, so nothing can ever read as movement */
#ddrPerf i{display:block; font-style:normal; font-size:11px; letter-spacing:2px;}
#ddrPerf b{display:block; font-weight:normal; font-size:15px; letter-spacing:1px;}

/* ===== ⭐ v3.84 THE STUDIO LOGO CARD — the bulb-on-target mark, first thing anyone ever sees ===== */
/* ONE splash: the bulb flickers on above the presents line, then holds STATIC — no halo, no glow,
   no breathing. The PNG carries real alpha transparency, so it sits on the dark with no tricks. */
/* ⭐⭐ v0.710 — THE STUDIO CARD: the bulb over the words, centred on the screen AS ONE THING.
   js/arcade.js nests both inside #introCard on the way in. The gap is a real `gap`, so it is the
   same on a folded phone and an unfolded one and it cannot drift when the wording changes height.
   ⚠ NEITHER CHILD MAY BE ABSOLUTELY POSITIONED ANY MORE — that is what put them on percentages of
   the viewport, which is the thing this replaces. */
/* ⭐⭐⭐ v0.721 — THE CARD IS THE TITLE SCREEN'S SKELETON, so the two screens are registered to
   each other and the transition is a dissolve rather than a cut. The rows are copied from
   `#scr-title` in css/wjam.css (1fr / auto / 1fr, the wordmark owning the true centre):
     · row 2 holds the WORDS, so "Odaroo & Co." stands exactly where INKVILLE is about to stand;
     · row 1 holds a box the SIZE OF #titleAce, bottom-aligned with the same 32px of furniture
       under it (the ace's 4px margin + the t-top gap + the bulb row + its padding), so the target
       mark is centred precisely on the inklet — a size bigger, so the inklet arrives INSIDE it.
   ⚠⚠ NEITHER CHILD MAY BE ABSOLUTELY POSITIONED, and nothing here may go back onto a percentage of
   the viewport: that is exactly the drift this replaces. If the title's grid changes, change it
   there and this follows on its own. */
/* ⭐⭐⭐ v0.734 — THE CARD IS CENTRED AS ONE MARK, not registered on the title screen any more.
   The 1fr/auto/1fr skeleton (v0.721) existed to stand the bulb where the title inklet was about to
   stand and the words where INKVILLE was about to stand, so the two screens melted together. The
   bulb is gone: the mark is now the nine tiles PLUS the wordmark under them, and those two are one
   object. A 298px-tall grid parked on the title's inklet row left the bottom half of the unfolded
   Fold empty. One centred flex column, one real `gap` — the same lesson as v0.710, applied to the
   pair that exists now. ⚠ `.ic-ghost` (the invisible tagline+version that reserved the title's
   lower rows) goes with it; keep it in the markup, but it must not add height here. */
/* ⭐⭐⭐ v0.745 — THE CARD IS THE TITLE SCREEN'S SKELETON AGAIN (Odaroo, 2026-08-07), and this
   time it holds BOTH marks. v0.734 flattened it to one centred column because the bulb had been
   retired; the bulb is back, so the registration is back with it, and the rows are copied from
   `#scr-title` in css/wjam.css (1fr / auto / 1fr):
     · row 1 holds a box THE SIZE OF #titleAce (152x158), bottom-aligned with the same ~32px of
       furniture under it, so the bulb-on-target stands exactly where the title inklet is about to
       stand — the rings are the contour the inklet fades up inside;
     · row 2 holds the nine tiles + the wordmark, at HALF their v0.734 size, standing where
       INKVILLE is about to stand.
   ⚠⚠ NEITHER ROW MAY BE ABSOLUTELY POSITIONED and nothing here may go onto a percentage of the
   viewport — that is the drift v0.710 removed. If the title's grid changes, change it there. */
/* ⭐⭐⭐ v0.749 — THE BULB AND ITS TARGET ARE GONE AGAIN (Odaroo, 2026-08-07), so the card is one
   centred column holding the nine tiles and the wordmark — the same lesson as v0.734: a skeleton
   copied from the title screen only earns its keep while something stands in the inklet's row. */
#introCard{position:absolute; inset:0; z-index:4; pointer-events:none;
  display:flex; flex-direction:column; align-items:center; justify-content:center;}
#introCard .ic-ghost{display:none;}
#introCard .ic-mid{display:flex; flex-direction:column; align-items:center;
  gap:clamp(9px, 1.7vh, 17px);}   /* ⚠ half of v0.734's gap — the mark under it is half the size */
/* ⚠ the 32px is the title's furniture under the ace: its own 4px margin + the .t-top 8px padding
   + the gap and bulb row that follow it. Measured, not guessed — if #titleAce's surroundings
   change in css/wjam.css, re-measure this. */

/* THE BULB-ON-TARGET, Odaroo's original studio mark. It flickers up once and then holds STATIC —
   no halo, no glow, no breathing (that has been tried and removed by name). */

/* ⭐⭐⭐ v0.734 — THE MARK IS THE NINE-TILE GRID, SO ITS BOX IS THE GRID'S OWN SIZE.
   It used to be a hard 152x158 box holding the bulb PNG, sized to register on the title inklet.
   The grid is a different animal — it is the wordmark itself — so the box is `auto` and the tile
   size does the deciding. ⚠ THE TILE IS CAPPED BY BOTH `vmin` AND `vh`: a vmin-only size is 175px
   on the unfolded Fold (min(906,832)=832), which is a 574px-tall grid on a 832px screen with the
   words still to come. The vh half is what keeps row 1 able to hold it on both folds. */
/* ⭐⭐⭐ v0.745 — EXACTLY HALF OF v0.734, every number of it (Odaroo: "50% of its size"). The tile
   size is the only knob — the borders, the type, the shadows and the grid gap are all written as
   fractions of `--odct`, so halving it halves the whole mark and nothing can drift out of ratio.
   ⚠ DO NOT halve this by putting a `transform:scale(.5)` on the grid: a scaled box still claims
   its full height in the row, and the reveal animation already owns `transform`. */
/* ⭐ v0.749 — 25% BIGGER than v0.745, every number of it: the tile size is still the only knob. */
/* ⭐ v0.896 — the size Lunatide's boot uses (lunatide/style.css): the vh cap is 16vh, so on a phone
   the vmin half decides and the grid is ~20% bigger than it was; it is dead centre now. */
#introLogo{--odct:clamp(32px, min(13.1vmin, 16vh), 65px); flex:0 0 auto; pointer-events:none;
  display:grid; grid-template-columns:repeat(3, var(--odct)); grid-auto-rows:var(--odct);
  gap:calc(var(--odct) * .13);}
/* Each tile is a chunky pixel block with one character stamped in it, and they SNAP IN one at a
   time (`--i` is the tile's index, written by js/arcade.js). Nine tiles x 55ms = 495ms, finished
   long before the backlit flash at 1100ms — the reveal must never still be running when the
   screen changes under it. */
.odc-tile{position:relative; opacity:0; background:#0d1018;
  border:calc(var(--odct) * .075) solid #eef2f8; box-sizing:border-box;
  display:flex; align-items:center; justify-content:center;
  box-shadow:inset 0 0 0 calc(var(--odct) * .045) #0d1018, 0 0 calc(var(--odct)*.22) #9fd0ff33;}
/* ⚠ v0.733 — the ninth tile is "O." — the letter at full size with the stop hung off it small, so
   all nine LETTERS still read as one size across the block. Do not grow the stop to match. */
/* ⭐⭐⭐ v0.749 — THE STOP HANGS OUTSIDE THE CENTRING (Odaroo: the last O has to line up with the
   column above it). It used to sit in the flow, so the ninth tile centred "O." as a pair and the
   O itself sat left of the other Os in its column. It is absolute now: the O is centred like every
   other letter and the period comes right after it. */
.odc-tile span{position:relative;}
.odc-tile i{position:absolute; left:100%; bottom:0; font-style:normal; font-size:.62em; margin-left:.06em;}
.odc-tile span{font-family:var(--fontD); font-size:calc(var(--odct) * .46); line-height:1;
  color:#eef2f8; text-shadow:calc(var(--odct)*.035) calc(var(--odct)*.035) 0 #05040a;
  transform:translateY(calc(var(--odct) * .02));}
#introLogo.show .odc-tile{animation:odcTileIn .34s steps(3) forwards; animation-delay:calc(var(--i) * 32ms);}
/* ⭐⭐⭐ v0.749 — THE WINNING DIAGONAL. Tiles 0, 4 and 8 are O, O, O, so each of them turns
   over on the Y axis and comes back GREEN — the WHOLE square, letter still white, a confirmed-OK
   green (#4CAF50) rather than a neon or a dark one. js/arcade.js starts them 240ms apart so they
   land in order; then all three invert for a beat to call the line.
   ⚠⚠ THE `animation` PROPERTY IS A SHORTHAND: these rules REPLACE odcTileIn rather than adding to
   it, which is why every keyframe here restates opacity:1 — without it the tile snaps back to its
   hidden base the instant the flip starts. Specificity has to beat `#introLogo.show .odc-tile`.
   ⚠ The inset ring is re-coloured with the square, or the dark inner border stays printed on the
   green and reads as a hole.
   ⚠ The colour swap happens at 50%, edge-on to the camera, so the square is never seen changing. */
#introLogo{perspective:800px;}
#introLogo.show .odc-tile.spin{animation:odcFlip .38s cubic-bezier(.45,.05,.3,1) forwards;}
@keyframes odcFlip{
  0%{opacity:1; transform:rotateY(0deg); background-color:#0d1018; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #0d1018;}
  24.9%{opacity:1; transform:rotateY(90deg); background-color:#0d1018; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #0d1018;}
  25%{opacity:1; transform:rotateY(90deg); background-color:#4CAF50; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #4CAF50;}
  100%{opacity:1; transform:rotateY(360deg); background-color:#4CAF50; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #4CAF50;}}
/* the glitch that calls the line — black-on-white, held about a second */
#introLogo.show .odc-tile.inv{animation:odcInv .7s steps(1) forwards;}
@keyframes odcInv{
  0%{opacity:1; background-color:#eef2f8; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #eef2f8;}
  8%{opacity:1; background-color:#4CAF50; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #4CAF50;}
  16%{opacity:1; background-color:#eef2f8; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #eef2f8;}
  100%{opacity:1; background-color:#eef2f8; box-shadow:inset 0 0 0 calc(var(--odct) * .045) #eef2f8;}}
#introLogo.show .odc-tile.inv span{color:#0d1018; text-shadow:none;}
@keyframes odcTileIn{0%{opacity:0; transform:scale(.55); filter:brightness(3)}
  55%{opacity:1; transform:scale(1.1); filter:brightness(1.8)}
  100%{opacity:1; transform:scale(1); filter:none}}
#introSplash #introText{width:100%; z-index:3; padding:10px 0;}
/* ⚠ v0.702 — ONE swell, on the card, so the bulb and the words can never grow into each other on
   the way out (they did at 1.14/1.10 when they were animated separately). */
#introSplash.fadeout #introCard{animation:presOut .9s ease-in forwards;}
/* ⚠ v0.721 — the card is full-screen now, so there is no -50% to carry: the swell is plain `scale`
   about the centre. Kept SMALL (1.03) because the mark is registered on the inklet — a big swell
   would slide the target off the thing it is meant to dissolve into. */
@keyframes presOut{0%{transform:scale(1); opacity:1}100%{transform:scale(1.03); opacity:0}}
/* ⚠ v0.745 — v0.734's note here said `.il-img` was retired from this card. It is BACK, on Odaroo's
   word, in the title-inklet slot above the halved mark; its rules live with #introCard above. *//* ⭐ v0.453 — BULLET BALLET STOPS AT THE CONTROLS.
   #hellStage is flex:1 in a column with nothing reserving room for the D-pad, so the bullet field
   ran the full height of the screen and the bottom ~230px of it — where the player's ship spends
   the whole game — was behind the controls. It looked fine because the ship is drawn above the pad's
   z-index; the bullets underneath it were simply invisible until they hit you.
   --stage-bot is published by inkStage(): the distance from the bottom of the viewport to the top of
   the controls, safe areas included. It changes only when the screen does. */

/* ============================================================
   ⭐⭐ v0.687 — THE CHALLENGE FLOW: THE EMPTY CHAIR.

   ⚠⚠ v0.693 — THERE IS NO `.chaltoast` AND THERE MUST NOT BE ONE. v0.692 put a brief CHALLENGE SENT
   card on the glass between the YES and the connecting screen; Odaroo cut it the same day — the
   connecting screen is the confirmation, and a box saying so as well is one event announced twice.
   The rule is written here as well as in js/online.js because a style is the easiest half to add back.

   `.lob-empty` is the rival's side when NOBODY ANSWERED: the outline of a chair nobody sat in. It is
   deliberately drawn rather than left blank — an empty gap reads as a screen that failed to load,
   and the whole point of the ten-second timeout is to say clearly that the player is not there.
   ⚠ It must be the SAME BOX as .lob-art (52px) or the VS stage shifts as it appears.
   ============================================================ */
.lob-art.lob-empty{border:3px dashed #ffffff66; border-radius:50%; box-sizing:border-box;
  filter:none; animation:none; background:#00000033;}
.lob-art.lob-empty svg{display:none;}
