/* ============================================================
   INKVILLE — hub.css
   the home boulevard, the ledger screen, the badge wall and the cheat drop

   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.
   ============================================================ */

/* ============================================================
   ⭐ v0.395 — THE MARQUEE CAROUSEL + THE BADGE WALL
   The boulevard is one act under a red curtain (Mario-3 stage drapes),
   ALBUM + MART side by side beneath it, and the GIGS & ACHIEVEMENTS
   bar running the full width along the bottom.
   ============================================================ */
#menuStack .mtile.feat{
  grid-column:1 / span 12; position:relative;
  /* ⭐⭐⭐ v0.746 — overflow:hidden → VISIBLE, so the NEW-MODE badge can hang off the corner like every
     other one in the game (see the .ndot block in css/menu.css). Nothing inside the curtain needs the
     tile to clip: every piece of it (.ftn-cur, .ftn-val, the drapes, the ropes, the chain lock) is
     anchored to an edge with inset:0 or 0, and .mpat is not emitted on this tile at all.
     ⚠ The gold cursor ring stays an `outline` regardless — see the .selm note below; that reasoning is
     about the border sizing the curtain, not about the clipping. */
  overflow:visible;
  height:calc(206px * var(--gs,1)); min-height:0;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:calc(3px * var(--gs,1));
  padding:calc(32px * var(--gs,1)) calc(38px * var(--gs,1)) calc(14px * var(--gs,1));
  text-align:center; transform:none;
  /* ⚠ v0.616 — the browser must not steal a HORIZONTAL drag for scrolling, or press-and-slide
     never reaches the swipe handler. Vertical panning is left alone so the page still scrolls. */
  touch-action:pan-y;
  container-type:inline-size;   /* v0.616 — so the arrows' tap columns can be a SHARE of the curtain */
  /* ⭐⭐⭐ v0.686 — THE ACT'S TILE IS BLACK (Odaroo, 2026-08-06). "Make the whole tile background
     black. The cartridge silhouette stays neon white in the centre. Below it the game title in
     WHITE, with the Japanese underneath also in white."
     ⚠⚠ THIS IS WHAT THE CREST WAS ALWAYS DRAWN FOR. Every crest carries its own #06080b plate
     (see cartCrest, js/cards.js) — on cream that plate was a black postage stamp sitting on a
     poster; on black it disappears and the neon reads as the cartridge floating on the tile. So
     the field here is #06080b to MATCH the crest's own plate, not #000: a hair of daylight
     between the two would draw a rectangle around every logo. Change one, change the other.
     ⚠ crReadable is the judge of the type over this, not the eye — see the deploy's pre-flight. */
  /* ⭐⭐⭐ v0.712 SUPERSEDES THE FIELD ABOVE: PITCH BLACK, like a movie theatre (Odaroo, 2026-08-06).
     The soft radial glow and the drifting wallpaper are both gone — the tile is #000 and the only
     thing in it is the neon-white cartridge turning on the spot. The crest's own #06080b plate
     stopped mattering here the moment the art became cartCrestRig(), which draws no plate at all;
     the flat crest still wears one everywhere else on the tile set, and that is still correct. */
  background:#000;
  border:calc(3px * var(--gs,1)) solid var(--ink); box-shadow:calc(5px * var(--gs,1)) calc(5px * var(--gs,1)) 0 var(--ink);}
#menuStack .mtile.feat .menu-art{width:calc(88px * var(--gs,1)); height:calc(88px * var(--gs,1)); margin:0 auto;}
/* ⭐⭐⭐ v0.712 — NO WALLPAPER ON THE FEATURED TILE AT ALL. js/cards.js does not emit .mpat for the
   act any more; this rule is the belt to that braces, so a pattern can never creep back onto the
   black. (The v0.686 note that used to live here — turning the wallpaper UP to .34 with a screen
   blend so the dark accents still read — is retired with it.) */
#menuStack .mtile.feat .mpat{display:none!important;}

/* ⭐⭐⭐ v0.712 — THE PREVIEW *IS* THE CARTRIDGE TURNING. Markup from cartCrestRig() in js/cards.js.
   The shell rotates in 3D; the game's symbol sits over the axis, flat, still and neon white. It is
   the boot screen's rig with the glitch taken off — the carousel is a calm dark room, not a fault.
   ⚠⚠ .crig-mark IS A SIBLING OF .crig-spin, never a child, so nothing added to the spinner later
   can drag the logo round with it. That is the whole trick.
   ⚠ The two layers share one 0 0 40 40 viewBox at the same size, so the glyph's centre lands on the
   spinner's axis. Size one and you must size the other.
   ⚠ Perspective is on the BOX, not the faces — a perspective per face gives each its own vanishing
   point and the cart shears as it turns. */
.crig{position:absolute; inset:0; perspective:300px;}
.crig-spin{position:absolute; inset:0; transform-style:preserve-3d;
  animation:crigSpin 4.4s linear infinite;}
@keyframes crigSpin{from{transform:rotateY(0deg)}to{transform:rotateY(360deg)}}
.crig-face{position:absolute; inset:0; backface-visibility:hidden; -webkit-backface-visibility:hidden;}
.crig-back{transform:rotateY(180deg);}   /* a real reverse (the battery hatch), never a mirror */
.crig-face svg,.crig-mark svg{width:100%; height:100%; display:block;}
.crig-mark{position:absolute; inset:0; z-index:3; pointer-events:none;}
/* ⚠ REDUCED MOTION STILL SHOWS A CARTRIDGE — it just stops turning. Hiding the shell would leave
   the logo floating with nothing round it. */
@media (prefers-reduced-motion:reduce){ .crig-spin{animation:none!important;} }
/* ⚠ SIZE ONLY. The colour of both of these is decided much further down this file — see the
   v0.686 white-on-black rule near #menuStack .mtile.feat.showcase. A colour written here loses. */
#menuStack .mtile.feat .menu-label{font-size:calc(16px * var(--gs,1))!important; line-height:1.25; letter-spacing:calc(1px * var(--gs,1));
  word-break:break-word; max-width:100%;}
#menuStack .mtile.feat .jplab{font-size:calc(9px * var(--gs,1)); opacity:.92;}
/* ⚠ v0.616 — the "?" must outrank the arrow's WIDE tap column (z-index 14) or it cannot be pressed
   at all. It is a 21px target, so it costs the arrow almost nothing. */
/* ⭐⭐⭐ v0.732 — ONE EVENLY SPACED ROW ALONG THE TOP RIGHT: [READY TO UNLOCK] [?] [2x].
   (Odaroo, 2026-08-06: the unlock plate used to sit on the top-LEFT corner, where a costume or a
   loud frame on the act's crest could run under it and cut it off. It is furniture, not artwork, so
   it now lines up with the other two pieces of furniture.)
   ⚠⚠ THE THREE GAPS ARE ARITHMETIC, NOT GUESSES, and they only stay even if all three keep the
   widths written here: the 2x starburst is 44 wide at right:6 (so its left edge is 50), the "?" is
   20 wide and starts one 8px gap later at right:58 (left edge 78), and the plate starts one more
   8px gap later at right:86. Change any width and re-do the sum — this is the one place it lives.
   ⚠ They are centred on ONE line (centre y = 43), which is why each `top` is different: the three
   boxes are 34, 20 and ~25 tall. Do not "tidy" them to a common top. */
#menuStack .mtile.feat .mtq{top:calc(33px * var(--gs,1)); right:calc(58px * var(--gs,1)); z-index:16;}
#menuStack .mtile.feat .x2badge{z-index:8; top:calc(26px * var(--gs,1)); right:calc(6px * var(--gs,1));}
#menuStack .mtile.feat .ftn-rdy{left:auto; right:calc(86px * var(--gs,1)); top:calc(31px * var(--gs,1));}
/* ⚠⚠⚠ v0.746 — THE ACT'S OWN NEW-MODE BADGE HANGS OFF THE TOP-**RIGHT**, and this is the one tile in
   the game that may not use the top-left.
   ⚠⚠ THE INKLET'S PORTRAIT IS SITTING ON THAT CORNER. `.hud-cam` is 92px and hangs ~30px BELOW its own
   HUD bar (see the v0.744 note in CLAUDE.md), which puts it exactly over the carousel's top-left —
   measured, at BOTH folds: the hit-test at the badge's own centre came back `hud-cam`. A notification
   you have to look behind your own face for is the fault being fixed here, not a new place to put it.
   ⚠ And the pip sits ON this corner (centre on the black rule) rather than half beyond it: the tile is
   FULL WIDTH and scales up 3% when the cursor is on it, which walks its edges ~13px past the stack's
   own padding. A full half-bite there would be sawn off by the screen. */
#menuStack .mtile.feat .ndot{left:auto; z-index:18;
  right:calc(var(--ndot-sz) / -2 - 2px); top:calc(var(--ndot-sz) / -2 - 2px);}
/* ⭐⭐ v0.705 — "A GAME IS READY TO UNLOCK". A gold plate on the curtain's top-left corner, opposite
   the "?", carrying the count and the reason. Tapping it walks the bill to the game it means.
   ⚠ z-index 17: the LEFT arrow's tap column (.ftn-arw::before) reaches nearly the full height of the
   tile at z-index 14, so anything below that on this corner cannot be pressed at all — the same trap
   the "?" hit in v0.616. It is a small target, so it costs the arrow almost nothing.
   ⚠ It is anchored 6px in, NOT on the corner: .ndot straddles -4px/-4px there, and a mode you have
   just unlocked can be the featured act while another one is already paid for. */
.ftn-rdy{position:absolute; top:calc(6px * var(--gs,1)); left:calc(6px * var(--gs,1)); z-index:17;
  display:flex; align-items:center; gap:calc(4px * var(--gs,1)); cursor:pointer;
  padding:calc(3px * var(--gs,1)) calc(6px * var(--gs,1)) calc(3px * var(--gs,1)) calc(3px * var(--gs,1));
  background:#1b1510f2; border:calc(2px * var(--gs,1)) solid var(--gold); border-radius:calc(6px * var(--gs,1));
  box-shadow:calc(2px * var(--gs,1)) calc(2px * var(--gs,1)) 0 #000a; text-align:left;
  animation:ftnRdy 1.9s ease-in-out infinite;}
.ftn-rdy i{flex:none; display:flex; align-items:center; justify-content:center; font-style:normal;
  width:calc(15px * var(--gs,1)); height:calc(15px * var(--gs,1)); border-radius:50%;
  background:var(--red); color:#fff; border:calc(1.5px * var(--gs,1)) solid var(--ink);
  font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); line-height:1;}
.ftn-rdy span{font-family:var(--fontD); font-size:calc(6.5px * var(--gs,1)); line-height:1.35;
  color:var(--gold); letter-spacing:0; white-space:nowrap;}
@keyframes ftnRdy{0%,72%,100%{transform:scale(1);} 82%{transform:scale(1.08);} 90%{transform:scale(.98);}}
@media (prefers-reduced-motion:reduce){ .ftn-rdy{animation:none;} }
/* the ALBUM's app badge carries a NUMBER now — the pip grows just enough to hold two characters */
.ndot.pop b{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); color:#fff; line-height:1;
  transform:skewX(-3deg);}   /* counter-skew the badge's own 3deg so the figure stands up straight */
/* ⭐⭐⭐ v0.732 — THE CAROUSEL IS NEVER DIMMED (Odaroo, 2026-08-06: "always keep the background
   black, as if it's always there").
   css/menu.css fades every unselected tile to opacity .82 when the cursor is somewhere on the
   screen. On a cream tile that reads as "not focused"; on the act's PITCH-BLACK stage it reads as
   the stage turning grey, because .82 of black over the page is exactly a grey rectangle. So the
   featured tile opts out of the fade entirely and keeps its field.
   ⚠ It is opted out by SPECIFICITY (#id + two classes beats #id + one class + :not), not by
   !important — the .selm rule below still has to be able to lift and shake it. */
#menuStack.hasSel .mtile.feat:not(.selm){opacity:1; transform:none;}
/* ⭐⭐⭐ v0.732 — AND WHEN THE CURSOR *IS* ON IT, THE WHOLE PERIMETER LIGHTS UP. The only cue used
   to be a 1px jitter, which on a black tile is nearly invisible — you could not tell whether left
   and right were about to walk the bill or do nothing. A gold rule is drawn round the entire
   silhouette of the tile now, pulsing gently.
   ⚠⚠ IT IS AN `outline`, NOT A BORDER AND NOT A BOX-SHADOW. The tile is `overflow:hidden` (an
   inset ring would be cut by the drapes) and its border is what sizes the curtain, so a border
   change would move every piece of furniture inside it. An outline is drawn outside the box, costs
   no layout, and is not clipped.
   ⚠ `animation` is a SHORTHAND: featJit is restated here rather than added, or the shake is lost. */
#menuStack .mtile.feat.selm{animation:featJit .5s steps(2) infinite;
  outline:calc(3px * var(--gs,1)) solid var(--gold); outline-offset:calc(2px * var(--gs,1));
  box-shadow:calc(5px * var(--gs,1)) calc(5px * var(--gs,1)) 0 var(--ink),
             0 0 calc(16px * var(--gs,1)) rgba(244,197,66,.55);}
@media (prefers-reduced-motion:reduce){ #menuStack .mtile.feat.selm{animation:none;} }
@keyframes featJit{0%,100%{transform:translate(-1px,-2px) scale(1.012)}50%{transform:translate(1px,-1px) scale(1.012)}}
#menuStack .mtile.feat.swipe .menu-art,#menuStack .mtile.feat.swipe .menu-label{animation:featIn .28s ease both;}
@keyframes featIn{from{transform:translateX(calc(22px * var(--gs,1))); opacity:0;} to{transform:none; opacity:1;}}

/* ---- the curtain itself ---- */
.ftn-cur{position:absolute; inset:0; z-index:2; pointer-events:none;}
.ftn-val{position:absolute; left:0; right:0; top:0; height:calc(24px * var(--gs,1));
  background:repeating-linear-gradient(90deg, #c93a22 0 7px, #a02a17 7px 14px);
  border-bottom:calc(2px * var(--gs,1)) solid var(--ink);
  box-shadow:0 calc(2px * var(--gs,1)) calc(5px * var(--gs,1)) rgba(0,0,0,.35);}
.ftn-val::after{content:''; position:absolute; left:0; right:0; bottom:calc(-9px * var(--gs,1)); height:calc(10px * var(--gs,1));
  background:radial-gradient(circle at calc(11px * var(--gs,1)) 0, #a02a17 calc(10px * var(--gs,1)), transparent calc(10.5px * var(--gs,1)));
  background-size:calc(22px * var(--gs,1)) 100%; background-repeat:repeat-x;}
.ftn-drape{position:absolute; top:0; bottom:0; width:calc(30px * var(--gs,1));
  background:repeating-linear-gradient(90deg, #c93a22 0 6px, #8e2413 6px 9px, #e04a2c 9px 12px, #a02a17 12px 18px);
  box-shadow:inset 0 0 calc(14px * var(--gs,1)) rgba(0,0,0,.5);}
.ftn-drape.l{left:0; border-right:calc(2px * var(--gs,1)) solid var(--ink);}
.ftn-drape.r{right:0; border-left:calc(2px * var(--gs,1)) solid var(--ink);}
.ftn-rope{position:absolute; top:calc(34px * var(--gs,1)); width:calc(30px * var(--gs,1)); height:calc(8px * var(--gs,1));
  background:repeating-linear-gradient(45deg, #f4c542 0 3px, #a87f22 3px 6px); border:calc(2px * var(--gs,1)) solid var(--ink);}
.ftn-rope.l{left:0;} .ftn-rope.r{right:0;}

/* ============================================================
   ⭐⭐⭐ v0.683 — THE PROJECTOR IS GONE (Odaroo, 2026-08-06). Every .pj-* rule (the blur, the scan
   lines, the grain, the lamp flicker, the tear, the vignette and the reel motions) is DELETED along
   with the PJ scenes in js/cards.js. The featured act is advertised by its WALLPAPER now — the
   animated .mpat patterns in css/menu.css, one per game.
   ⚠ THE PAINT-ORDER LIFT BELOW MUST STAY. .mpat is a POSITIONED backdrop, and a positioned
   backdrop paints over every NON-positioned sibling, text included, whatever the markup order.
   That is why the art, the label, the JP line and the price band are pinned above it.
   ============================================================ */
#menuStack .mtile.feat .menu-art,
#menuStack .mtile.feat .menu-label,
#menuStack .mtile.feat .jplab,
#menuStack .mtile.feat .ftn-need{position:relative; z-index:4;}
/* ---- ⭐⭐ v0.405 — THE ARROWS THAT WALK THE BILL, AS BIG CUT GEMS ----
   They used to be small gold tabs with a ◀ character on them, tucked against the curtain's edge —
   so subtle that Odaroo did not read them as buttons at all. They are now large faceted triangles
   in the Zelda-rupee spirit: a solid pixel triangle with a lighter facet down one side, a specular
   glint that sweeps across, and a slow pulse so they read as "press me". They deliberately POP OUT
   over the drapes rather than sitting behind them.
   ⚠ z-index has to beat the drape (.ftn-drape sits above the tile art), and the button itself is
   transparent — the triangle is the `i`, drawn with clip-path so the tap target stays a comfortable
   rectangle rather than a thin diagonal sliver. */
.ftn-arw{position:absolute; top:50%; transform:translateY(-50%); z-index:14;
  width:calc(74px * var(--gs,1)); height:calc(78px * var(--gs,1)); padding:0; cursor:pointer;
  background:none; border:0; box-shadow:none; overflow:visible;
  display:flex; align-items:center; justify-content:center;
  animation:arwBob 1.5s ease-in-out infinite;}
/* ⭐⭐ v0.616 — A DOUBLE CHEVRON, AND A TAP ZONE FAR BIGGER THAN THE GEM.
   Two gems side by side (« ») read as "walk the bill" much louder than one, and both sides are the
   same size so the curtain stays symmetrical.
   ⚠⚠ THE HIT AREA IS THE ::before, NOT THE BUTTON BOX. Odaroo kept opening the game when he meant
   to scroll, so each arrow now claims a wide invisible column that reaches well INWARD over the
   act's artwork — left and right together own more of the tile than the middle does. It stops short
   of the top and bottom on purpose, so the "?" button and the pips are still reachable.
   ⚠⚠ THE INWARD EDGE IS A SHARE OF THE TILE (32cqw), NOT A PIXEL COUNT. A fixed column that owned
   a third of the folded curtain owned barely a tenth of the unfolded one, so the fault Odaroo
   reported would have come straight back on the wide screen. The tile is the container. */
.ftn-arw::before{content:''; position:absolute; top:-52%; bottom:-52%;
  left:calc(-18px * var(--gs,1)); right:calc(66px * var(--gs,1) - 32cqw);}
.ftn-arw.r::before{left:calc(66px * var(--gs,1) - 32cqw); right:calc(-18px * var(--gs,1));}
.ftn-arw .arw-gem{position:absolute; top:calc(4px * var(--gs,1)); bottom:calc(4px * var(--gs,1)); left:0; width:52%;
  background:linear-gradient(135deg,#ffe98a 0 38%, var(--gold) 38% 62%, #b8860f 62%);
  filter:drop-shadow(calc(2px * var(--gs,1)) calc(3px * var(--gs,1)) 0 #1b1510)
         drop-shadow(0 0 calc(6px * var(--gs,1)) #f4c54288);}
/* the glint that sweeps across the face */
.ftn-arw .arw-shine{position:absolute; top:calc(4px * var(--gs,1)); bottom:calc(4px * var(--gs,1)); left:0; width:52%; opacity:.85;
  background:linear-gradient(115deg,transparent 0 38%, #fffbe8cc 46%, transparent 54% 100%);
  background-size:260% 100%; animation:arwShine 2.6s linear infinite;}
.ftn-arw .g2{left:auto; right:0;}                       /* the second chevron of the pair */
.ftn-arw .g2.arw-shine{animation-delay:.18s;}           /* the glint sweeps one, then the other */
.ftn-arw.l .arw-gem, .ftn-arw.l .arw-shine{clip-path:polygon(0 50%, 100% 0, 100% 26%, 42% 50%, 100% 74%, 100% 100%);}
.ftn-arw.r .arw-gem, .ftn-arw.r .arw-shine{clip-path:polygon(100% 50%, 0 0, 0 26%, 58% 50%, 0 74%, 0 100%);}
.ftn-arw:active{transform:translateY(-50%) scale(.9); animation:none;}
.ftn-arw:active .arw-gem{filter:drop-shadow(0 0 calc(3px * var(--gs,1)) #1b1510);}
/* pulled OUT past the curtain's fold, half on the drape and half off it */
.ftn-arw.l{left:calc(-8px * var(--gs,1));} .ftn-arw.r{right:calc(-8px * var(--gs,1));}
@keyframes arwBob{0%,100%{transform:translateY(-50%) translateX(0);} 50%{transform:translateY(-50%) translateX(calc(3px * var(--gs,1)));}}
.ftn-arw.l{animation-name:arwBobL;}
@keyframes arwBobL{0%,100%{transform:translateY(-50%) translateX(0);} 50%{transform:translateY(-50%) translateX(calc(-3px * var(--gs,1)));}}
@keyframes arwShine{0%{background-position:180% 0;} 100%{background-position:-80% 0;}}
@media (prefers-reduced-motion:reduce){ .ftn-arw,.ftn-arw .arw-shine{animation:none!important;} }
/* ⭐⭐ v0.406 — THE PAGE INDICATOR, BIG ENOUGH TO NOTICE. Five-pixel dots told nobody anything: you
   could not count them and you could not tell there were more games to walk to. They are proper
   squares now, on a dark plate so they read against any act's artwork, and the one you are on is
   half again as wide, lit in the house red and lifted — so "there are eleven of these and I am on
   the third" is readable at a glance. */
.ftn-pips{position:absolute; left:0; right:0; bottom:calc(5px * var(--gs,1)); z-index:12;
  display:flex; gap:calc(4px * var(--gs,1)); justify-content:center; align-items:center; pointer-events:none;}
.ftn-pips i{flex:0 0 auto; width:calc(9px * var(--gs,1)); height:calc(9px * var(--gs,1)); display:block;
  background:#fdf6e3; opacity:.5; border:calc(2px * var(--gs,1)) solid var(--ink);
  box-shadow:calc(1px * var(--gs,1)) calc(1px * var(--gs,1)) 0 rgba(13,10,8,.5);
  transition:width .18s var(--pop), opacity .18s ease, background .18s ease;}
.ftn-pips i.on{flex:0 0 auto; width:calc(20px * var(--gs,1)); opacity:1; background:var(--red);}
@media (min-width:640px){
  .ftn-pips i{width:calc(11px * var(--gs,1)); height:calc(11px * var(--gs,1));}
  .ftn-pips i.on{width:calc(26px * var(--gs,1));}
}
.ftn-need{position:relative; z-index:6; margin-top:calc(4px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(9.5px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1));
  color:var(--gold); background:#1b1510ee; border:calc(2px * var(--gs,1)) solid var(--gold);
  padding:calc(3px * var(--gs,1)) calc(7px * var(--gs,1)); display:inline-flex; align-items:center; gap:calc(4px * var(--gs,1));}
.ftn-cupico{width:calc(12px * var(--gs,1)); height:calc(12px * var(--gs,1)); display:inline-block;}
.ftn-cupico svg{width:100%; height:100%; display:block;}
/* a locked act stays visible under the curtain — the chains are the story, not a blackout */
#menuStack .mtile.feat.cuplocked{filter:grayscale(.7) brightness(.82);}
#menuStack .mtile.feat .chainlock .cl-need{font-size:calc(11px * var(--gs,1));}

/* ============================================================
   ⭐⭐ v0.458 — THE VAULT. Bit-Brawl is the last thing unlocked and the player is told nothing about
   it until then: a question mark under the curtain, ??? for a name, and a lock that states the
   condition instead of a price.
   ⚠ The condition is a SENTENCE, not a number, so unlike every other .ftn-need it has to wrap. An
   inline-flex row with nowrap would have run straight off the folded phone.
   ============================================================ */
/* ⭐⭐⭐ v0.807 — THE VAULT'S CONDITION IS PLAIN TEXT, NOT A PLATE. (Odaroo, 2026-08-13: "there's a
   little grey box behind the lock message text that makes it hard to read — remove that box, just
   leave the text as plain clear instructions.")
   Every OTHER `.ftn-need` is a PRICE — a short number on a gold-edged plate, which is exactly right
   for a number. The vault's is a SENTENCE, and the dark plate under it (`#1b1510ee` from the base
   rule) sat as a grey slab across the middle of the white inverted tile and fought the words on it.
   ⚠ It must clear the base rule's `background` AND `border` AND `padding` — leaving the padding
   behind holds the sentence in a box-shaped gap that still reads as a plate.
   ⚠ The colour is the INK now, not the cream: the tile behind it is white in both states since this
   same build. Bare text over artwork needs its outline — see `--tac` and crReadable's two rules. */
.ftn-need.vault{display:block; max-width:min(88%, calc(240px * var(--gs,1))); text-align:center;
  white-space:normal; line-height:1.5; font-size:calc(8.5px * var(--gs,1));
  letter-spacing:calc(.5px * var(--gs,1));
  background:none; border:0; padding:calc(2px * var(--gs,1)) 0; color:#06080b; text-shadow:none;}
.cl-need.vault{max-width:86%; white-space:normal; text-align:center; line-height:1.55;
  font-size:calc(8px * var(--gs,1));}
#menuStack .mtile.mystery{filter:none;}          /* the vault is not greyed — it is simply dark */
#menuStack .mtile.mystery .menu-label{color:var(--cream); letter-spacing:.18em;}
/* ============================================================
   ⭐⭐⭐ v0.484 — THE VAULT BREATHES, AND YOU CAN SEE WHAT IS INSIDE IT.
   Odaroo: show the artwork behind the chains, and give the tile a slow pulse so it feels like the
   final boss calling to you. Both of those live here.

   THE ARTWORK sits BEHIND the chains and the lock, dimmed and pushed back a little so the chains
   stay the thing you read first — it is still locked, and it has to look locked. It is darkened
   rather than hidden, which is what makes it read as "behind bars" instead of "greyed out".
   ⚠ z-index: the chainlock overlay is drawn after the art in the markup and carries its own
   stacking, so the art only needs to stay out of its way — see the paint-order note in CLAUDE.md
   before adding anything else to this tile.

   THE BREATH is on the TILE, not on its contents, so the chains, the lock, the artwork and the
   label all swell together as one object. 1 → 1.02 over 3.6s, which is slow enough to read as
   breathing rather than as a throb.
   ⚠⚠ IT ANIMATES `scale`, NOT `transform`, AND THAT IS LOAD-BEARING. The selected featured tile
   already runs `featJit` (see above), which animates `transform` — and `animation` is a single
   shorthand, so a second rule naming it simply REPLACES the first. Writing the breath as a
   transform animation would have silently killed the selection jitter on this one tile.
   `scale` is an independent property, so the two compose: featJit keeps nudging the tile about and
   the breath swells it, at the same time and without either one knowing about the other.
   ⚠ THE SELECTED VAULT THEREFORE HAS TO NAME BOTH ANIMATIONS. There is no way to "add" to the
   shorthand from a second rule, so the .selm case restates featJit alongside the breath. If featJit
   is ever retuned, this line has to follow it — hence it is written directly underneath.
   ⚠ Honour a player who has asked for less motion. */
/* ⭐⭐⭐ v0.772 — THE NEGATIVE (Odaroo, 2026-08-09) — see the note beside `mystery` in js/cards.js.
   ⚠⚠ THIS REPLACES THE v0.484 DIMMING, WHICH IS WHY THOSE TWO RULES ARE NOT ABOVE ANY MORE. The art
   is at full strength now: a black cartridge on white is already the strongest contrast the screen
   can print, and fading it turned the negative into a grey smear.
   ⚠⚠ THE INVERSION IS ONE `filter:invert(1)` ON THE ART, NOT A SECOND SET OF COLOURS. cartCrestRig
   draws the shell and the glyph with `stroke="#ffffff"` written inline on the <g> AND an inline
   `style` carrying the neon bloom — an inline style can only be beaten with !important, and doing
   that for every layer would fork the rig into a light copy and a dark copy that must be kept in
   step for ever. Inverting the finished picture flips the cart, the stop sign AND the cold bloom
   together, and it keeps working if the rig is ever redrawn.
   ⚠ `.cuplocked` greys the featured tile; the vault opts out, or the white field comes out beige. */
#menuStack .mtile.mystery .vault-art{opacity:1; filter:none;}
#menuStack .mtile.feat.mystery .vault-art{opacity:1;}
#menuStack .mtile.feat.vaultinv{background:#ffffff;}
#menuStack .mtile.feat.vaultinv.cuplocked{filter:none;}
/* ⚠ GREYSCALE FIRST: the rig's bloom is COLD BLUE, and inverting blue gives an orange halo round a
   black cartridge. Draining it first keeps the glow neutral, which is what a negative should be. */
#menuStack .mtile.feat.vaultinv .menu-art{filter:grayscale(1) invert(1);}
/* the type inverts with the room: black on white, and no cold bloom to smear it */
#menuStack .mtile.feat.vaultinv .menu-label,
#menuStack .mtile.feat.vaultinv .jplab{color:#06080b; text-shadow:none; -webkit-text-stroke:0;}
#menuStack .mtile.vaultinv .menu-label{color:#06080b;}
#menuStack .mtile.feat.vaultinv .ftn-need.vault{color:#06080b; border-color:#06080b;}
/* ⚠⚠ THE CHAIN OVERLAY CARRIES ITS OWN DARK SCRIM (`background:#1b1510aa`, css/base.css) — it is
   drawn over the WHOLE tile, so left alone it would have painted the white room brown and none of
   the inversion above would have been visible. The chains and the padlock themselves keep their
   metal: they are the one thing on this tile that is meant to look heavy.
   ⚠⚠ AND IT IS TRANSPARENT, NOT "A WHITE SCRIM INSTEAD OF A BROWN ONE". A 73% white veil was tried
   first and it washed the black cartridge, the black title and the lock's own condition line down to
   a ghost — on a dark tile a scrim makes the neon read, on a white one it erases everything under it.
   The chains alone are enough to say the tile is shut. */
#menuStack .mtile.vaultinv .chainlock{background:transparent;}
#menuStack .mtile.vaultinv .cl-need{color:#06080b; text-shadow:none;}
/* ⭐⭐⭐ ON THIS ONE TILE THE ACT STANDS IN FRONT OF ITS OWN CHAINS. Odaroo asked for the vault to be
   VISIBLE — "make it available to see" — and the chain run is drawn at z-index 6 while the artwork
   and the title sit at 4, so left in the usual order the black cartridge, the stop sign and the word
   OCTATHON were all behind a wall of metal on a white field: legible in theory, unreadable in fact.
   The run also drops to 55% here, which is what keeps it reading as a chain rather than as a scribble
   over the logo. The padlock keeps its full weight and its place — it is the thing that says SHUT.
   ⚠ This is the vault ONLY. Every other chained tile is a game you can still buy, and its art staying
   behind the chains is the whole point of the picture. */
#menuStack .mtile.vaultinv .cl-chain{opacity:.55;}
#menuStack .mtile.feat.vaultinv .menu-art,
#menuStack .mtile.feat.vaultinv .menu-label,
#menuStack .mtile.feat.vaultinv .ftn-need{z-index:8;}
#menuStack .mtile.feat.mystery{animation:vaultBreath 3.6s ease-in-out infinite;}
#menuStack .mtile.feat.mystery.selm{animation:featJit .5s steps(2) infinite, vaultBreath 3.6s ease-in-out infinite;}
@keyframes vaultBreath{0%,100%{scale:1}50%{scale:1.02}}
@media (prefers-reduced-motion:reduce){
  #menuStack .mtile.feat.mystery{animation:none;}
  #menuStack .mtile.feat.mystery.selm{animation:featJit .5s steps(2) infinite;}
}

/* ---- ⭐⭐ v0.410 — ALBUM and MART, the TWO beneath ----
   v0.406 put a Bit-Brawl circle between them and cut both posters back to five of the twelve columns
   to make room, curving their inner edges to cradle it. Odaroo has withdrawn that: Bit-Brawl is an
   act on the bill again, so the circle is gone entirely and the two posters take six columns each —
   an even pair, square inner edges, nothing between them. */
#menuStack .mtile.poster{grid-column:span 6; height:calc(92px * var(--gs,1));}
/* ---- the full-width GIGS & ACHIEVEMENTS bar ---- */
#menuStack .mtile.gigbar{grid-column:1 / span 12; height:calc(58px * var(--gs,1)); min-height:0;
  flex-direction:row; align-items:center; justify-content:center; gap:calc(10px * var(--gs,1));
  padding:calc(6px * var(--gs,1)) calc(10px * var(--gs,1)); transform:skewX(-3deg); position:relative;
  /* ⚠ v0.401 — VISIBLE, not hidden. The bar used to clip its own overflow, which ate the corner of
     the notification badge. Nothing inside it needs the bar to clip: the wallpaper (.mpat) is
     inset:0 and the rolling showcase does its own clipping in .gigshow. */
  overflow:visible;}
#menuStack .mtile.gigbar .menu-art{width:calc(32px * var(--gs,1)); height:calc(32px * var(--gs,1)); margin:0;}
/* ⚠ THE GIGS BAR IS A GRID (v0.397), not a row. Top row = the crest, the name and the badge count;
   bottom row = the rolling showcase, full width. As a flex ROW the strip was crushed to 49px on the
   folded phone, because the name alone is 206 of the 330 available. */
#menuStack .mtile.gigbar{display:grid!important; grid-template-columns:auto minmax(0,1fr) auto;
  grid-template-rows:auto minmax(0,1fr); align-items:center;
  column-gap:calc(7px * var(--gs,1)); row-gap:calc(3px * var(--gs,1)); transform:skewX(-3deg);}
#menuStack .mtile.gigbar .menu-art{grid-column:1; grid-row:1; align-self:center;}
#menuStack .mtile.gigbar .menu-label{font-size:calc(11px * var(--gs,1))!important; line-height:1.25;
  text-align:left; grid-column:2; grid-row:1; min-width:0;}
#menuStack .mtile.gigbar .gigshow-cnt{grid-column:3; grid-row:1;}
#menuStack .mtile.gigbar .gigshow{grid-column:1 / -1; grid-row:2; width:100%; height:100%;}
.achmedal.big .ach-tick,.achmedal.big .ach-q{right:calc(2px * var(--gs,1)); bottom:calc(10px * var(--gs,1));
  width:calc(24px * var(--gs,1)); height:calc(24px * var(--gs,1)); line-height:1;
  font-size:calc(13px * var(--gs,1));}
#menuStack .mtile.gigbar.selm{animation:selJit .45s steps(2) infinite;}

/* ============================================================
   ⭐ v0.398 — THE TWO TABS: GIGS | ACHIEVEMENTS
   Pinned above the scroller, never scrolls away.
   ============================================================ */
/* ⚠ the top padding clears the FILM REEL'S INKLING. The avatar is 69px tall in a 54px bar and hangs
   below it by design (the bar is overflow:visible, exactly as on the boulevard) — at 6px the
   inkling's feet sat 2px over the first tab's corner. */
/* ⚠⚠ v0.404 — AND THIS IS WHY THE TABS LOOKED CRAMPED AND STACKED.
   #achTabs was the flex row, but achTabsHTML() returns its buttons wrapped in a `.ach-tabs` div —
   so #achTabs had exactly ONE flex child (the wrapper), the wrapper itself was a plain block, and
   the buttons inside it shrank to their own text instead of sharing the width. They were never
   laid out as a row at all. The wrapper is the row now, and it fills the bar. */
#achTabs{display:flex; gap:calc(4px * var(--gs,1)); z-index:6; position:relative; flex:none;
  padding:calc(12px * var(--gs,1)) calc(8px * var(--gs,1)) 0;}
#achTabs>.ach-tabs{flex:1 1 auto; min-width:0;}
.ach-tabs{display:flex; flex-direction:row; flex-wrap:nowrap; align-items:stretch;
  gap:calc(4px * var(--gs,1)); width:100%;}
.ach-tab{position:relative; flex:1 1 0; min-width:0; cursor:pointer; text-align:center;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:calc(2px * var(--gs,1)); overflow:hidden;
  padding:calc(7px * var(--gs,1)) calc(2px * var(--gs,1)) calc(6px * var(--gs,1));
  background:#1b15101a; color:var(--ink);
  border:calc(2px * var(--gs,1)) solid var(--ink); border-bottom-width:calc(3px * var(--gs,1));
  font-family:var(--fontD); letter-spacing:calc(.5px * var(--gs,1));}
/* ⚠ v0.404 — THREE ACROSS, NOT TWO. "ACHIEVEMENTS" is twelve characters and at 9px it needed more
   than a third of a 375px screen, so the row either wrapped or clipped. The label shrinks a step,
   the side padding comes in, and the longest word is allowed to size itself down further still —
   all three now sit on one line on the folded screen with room to spare. */
.ach-tabn{font-size:calc(8px * var(--gs,1)); line-height:1.2; white-space:nowrap;}
.ach-tab.ach-long .ach-tabn{font-size:calc(6.6px * var(--gs,1)); letter-spacing:0;}
.ach-tabjp{font-size:calc(6.5px * var(--gs,1)); opacity:.6; line-height:1; white-space:nowrap;}
/* the live tab is the LIT one — cream face, gold underline, and it sits a touch proud */
.ach-tab.on{background:var(--cream); border-bottom-color:var(--gold);
  box-shadow:0 calc(-2px * var(--gs,1)) 0 var(--gold) inset;}
.ach-tab:not(.on){opacity:.72;}
.ach-tab:active{transform:translateY(calc(1px * var(--gs,1)));}
.ach-tabdot{position:absolute; top:calc(-5px * var(--gs,1)); right:calc(-4px * var(--gs,1));
  min-width:calc(16px * var(--gs,1)); height:calc(16px * var(--gs,1)); padding:0 calc(3px * var(--gs,1));
  display:flex; align-items:center; justify-content:center; font-style:normal;
  font-size:calc(7.5px * var(--gs,1)); background:var(--red); color:#fdf6e3;
  border:calc(2px * var(--gs,1)) solid var(--ink); animation:achOwed 1.05s steps(2) infinite;}

/* ============================================================
   ⭐ v0.398 — THE CASINO FLOOR: the 7-day jackpot ladder
   ============================================================ */
.cas{position:relative; z-index:5; overflow:hidden;
  background-color:#241726;
  background-image:linear-gradient(170deg,#2b1a2e,#1b1510 62%); border-color:var(--ink);}
.cas-marquee{position:relative; text-align:center; padding:calc(9px * var(--gs,1)) 0 calc(6px * var(--gs,1));}
/* the chasing bulbs — a row of lamps that runs left to right, the way a marquee does */
.cas-bulbs{position:absolute; left:0; right:0; top:0; height:calc(6px * var(--gs,1));
  background:repeating-linear-gradient(90deg, var(--gold) 0 calc(4px * var(--gs,1)),
             transparent calc(4px * var(--gs,1)) calc(12px * var(--gs,1)));
  animation:casChase .7s steps(3) infinite;}
@keyframes casChase{to{background-position:calc(24px * var(--gs,1)) 0;}}
.cas-title{font-family:var(--fontD); font-size:calc(13px * var(--gs,1)); color:var(--gold);
  letter-spacing:calc(1.5px * var(--gs,1)); line-height:1.3;
  text-shadow:0 calc(2px * var(--gs,1)) 0 #1b1510, 0 0 calc(10px * var(--gs,1)) rgba(244,197,66,.55);}
.cas-sub{display:block; font-size:calc(7.5px * var(--gs,1)); color:#f2e5c9; letter-spacing:calc(1px * var(--gs,1));
  margin-top:calc(4px * var(--gs,1)); text-shadow:0 calc(1px * var(--gs,1)) 0 #1b1510;}
.cas-live{margin:0 calc(6px * var(--gs,1)) calc(7px * var(--gs,1)); text-align:center;
  font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  background:var(--gold); color:#1b1510; border:calc(2px * var(--gs,1)) solid var(--ink);
  padding:calc(4px * var(--gs,1)); animation:achOwed 1.05s steps(2) infinite;}
/* ⚠ SIX ACROSS AND THE JACKPOT UNDERNEATH. Seven equal columns gave each drum 44px on the folded
   phone and "12k" would not fit inside one. */
.cas-reels{display:grid; grid-template-columns:repeat(6, minmax(0,1fr));
  gap:calc(4px * var(--gs,1)); padding:0 calc(6px * var(--gs,1));}
.drum{position:relative; overflow:hidden; text-align:center;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:calc(3px * var(--gs,1)); padding:calc(5px * var(--gs,1)) calc(2px * var(--gs,1));
  background-color:#f3ead2; background-image:linear-gradient(180deg,#fdf6e3,#e9dcc0); color:#1b1510;
  border:calc(2px * var(--gs,1)) solid var(--ink); box-shadow:0 calc(2px * var(--gs,1)) 0 #00000055;}
.drum-hd{font-family:var(--fontD); font-size:calc(7px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  line-height:1; white-space:nowrap;}
.drum-win{display:flex; flex-direction:column; align-items:center; gap:calc(2px * var(--gs,1));}
.cw{display:inline-flex; align-items:center; gap:calc(2px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(7px * var(--gs,1)); line-height:1;}
.cw i{width:calc(10px * var(--gs,1)); height:calc(10px * var(--gs,1)); display:block; flex:none;}
.cw i svg{width:100%; height:100%; display:block;}
.drum.got{background-color:#3d7c5c; background-image:linear-gradient(180deg,#4f8d63,#37755a); color:#fdf6e3;}
.drum.got .drum-hd{font-size:calc(11px * var(--gs,1));}
.drum.today{border-color:var(--gold); border-width:calc(3px * var(--gs,1));
  background-color:#f7dd97; background-image:linear-gradient(180deg,#fff3cf,var(--gold));}
.drum.live{animation:casSpin 1.05s steps(2) infinite; cursor:pointer;}
@keyframes casSpin{0%,100%{box-shadow:0 calc(2px * var(--gs,1)) 0 #00000055;}
  50%{box-shadow:0 calc(2px * var(--gs,1)) 0 #00000055, 0 0 0 calc(3px * var(--gs,1)) rgba(244,197,66,.85);}}
/* ⚠ var(--red) #c93a22 reads at 4.1:1 on cream and worse on gold — this is a DEEP circus red. */
.drum-pull{font-family:var(--fontD); font-size:calc(6.5px * var(--gs,1)); color:#7e2010; line-height:1;}
/* ---- the DAY 7 jackpot bar, full width beneath the six ---- */
.drum.jack{grid-column:1 / -1; flex-direction:row; flex-wrap:wrap; justify-content:center;
  gap:calc(4px * var(--gs,1)) calc(10px * var(--gs,1));
  margin-top:calc(5px * var(--gs,1)); padding:calc(11px * var(--gs,1)) calc(8px * var(--gs,1)) calc(8px * var(--gs,1));
  background-color:#efc954; background-image:linear-gradient(180deg,#fff6dd,var(--gold) 55%,#c99a1c); border-width:calc(3px * var(--gs,1));}
.drum.jack.got{background-color:#3e7a59; background-image:linear-gradient(180deg,#4f8d63,#2f6a4d); color:#fdf6e3;}
.drum-bulbs{position:absolute; left:0; right:0; top:0; height:calc(5px * var(--gs,1));
  background:repeating-linear-gradient(90deg,#fff6dd 0 calc(3px * var(--gs,1)),
             transparent calc(3px * var(--gs,1)) calc(9px * var(--gs,1)));
  animation:casChase .55s steps(3) infinite;}
.drum-jhd{font-family:var(--fontD); font-size:calc(10px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1));
  line-height:1.2;}
.drum-jhd b{color:#7e2010;}
.drum.jack.got .drum-jhd b{color:#ffe9a8;}
.drum.jack .drum-win{flex-direction:row; gap:calc(9px * var(--gs,1));}
.drum.jack .cw{font-size:calc(10px * var(--gs,1));}
.drum.jack .cw i{width:calc(14px * var(--gs,1)); height:calc(14px * var(--gs,1));}
.drum-crown{width:100%; font-family:var(--fontD); font-size:calc(7px * var(--gs,1));
  letter-spacing:calc(.5px * var(--gs,1)); line-height:1;}
.cas-foot{margin-top:calc(9px * var(--gs,1)); text-align:center; color:#f2e5c9;
  font-family:var(--fontD); font-size:calc(7px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  line-height:1.5; padding:0 calc(6px * var(--gs,1));}
.cas-foot b{color:var(--gold);}
.cas-warn{margin-top:calc(4px * var(--gs,1)); text-align:center; color:#cbbba0;
  font-size:calc(8px * var(--gs,1)); line-height:1.4; padding:0 calc(6px * var(--gs,1));}
@media (prefers-reduced-motion:reduce){
  .cas-bulbs,.drum-bulbs,.drum.live,.cas-live,.ach-tabdot{animation:none!important;}
}

/* ============================================================
   THE BADGE WALL — scr-ach
   ============================================================ */
#achList{flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch;
  padding-top:calc(8px * var(--gs,1)); padding-left:calc(11px * var(--gs,1)); padding-right:calc(11px * var(--gs,1));
  z-index:5; position:relative;}
#achFoot{display:flex; gap:calc(8px * var(--gs,1)); justify-content:center; padding:calc(14px * var(--gs,1)) 0 0;}
.ach-head{text-align:center; margin-bottom:calc(9px * var(--gs,1));}
.ach-bar{height:calc(10px * var(--gs,1)); background:#1b151033; border:calc(2px * var(--gs,1)) solid var(--ink);
  margin-top:calc(6px * var(--gs,1)); overflow:hidden;}
.ach-fill{height:100%; background:repeating-linear-gradient(90deg, var(--gold) 0 6px, #a87f22 6px 9px); transition:width .5s ease;}
.ach-sec{font-family:var(--fontD); font-size:calc(10px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1));
  display:flex; align-items:center; gap:calc(6px * var(--gs,1));
  margin:calc(12px * var(--gs,1)) 0 calc(6px * var(--gs,1));
  border-bottom:calc(2px * var(--gs,1)) solid var(--ink); padding-bottom:calc(3px * var(--gs,1));}
.ach-sec-ic{width:calc(18px * var(--gs,1)); height:calc(18px * var(--gs,1)); flex:none;}
.ach-sec-ic svg{width:100%; height:100%; display:block;}
.ach-sec-n{margin-left:auto; opacity:.65; font-size:calc(9px * var(--gs,1));}
/* ⚠ ALWAYS TWO TO A ROW (odaroo, v0.397) — never one badge sitting on its own. auto-fill used to
   drop to a single column on the folded phone; this is a hard 2, and every group in ACHS holds an
   EVEN number of badges so the last row is never a lone card. */
.ach-grid{display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); justify-content:center;
  gap:calc(8px * var(--gs,1)); max-width:calc(700px * var(--gs,1)); margin:0 auto;}
.achcard{position:relative; overflow:hidden; cursor:pointer;
  background:
    repeating-linear-gradient(135deg, rgba(27,21,16,.045) 0 calc(3px * var(--gs,1)), transparent calc(3px * var(--gs,1)) calc(7px * var(--gs,1))),
    var(--cream);
  border:calc(2px * var(--gs,1)) solid var(--ink);
  box-shadow:calc(3px * var(--gs,1)) calc(3px * var(--gs,1)) 0 var(--ink);
  padding:calc(8px * var(--gs,1)) calc(6px * var(--gs,1)) calc(7px * var(--gs,1)); text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:calc(3px * var(--gs,1));
  filter:grayscale(1) brightness(.93); opacity:.78; transition:transform .12s ease;}
.achcard:active{transform:translate(calc(2px * var(--gs,1)), calc(2px * var(--gs,1)));}
.achcard.got{filter:none; opacity:1; border-color:var(--tmc,#f4c542);
  background:
    radial-gradient(circle at 50% 22%, rgba(255,255,255,.85), transparent 58%),
    repeating-conic-gradient(from 0deg at 50% 22%, rgba(27,21,16,.05) 0deg 9deg, transparent 9deg 18deg),
    linear-gradient(160deg,#fff6dd,var(--cream) 58%,#f2e2bd);}
.achcard.owed{border-color:var(--gold); animation:achOwed 1.05s steps(2) infinite;}
@keyframes achOwed{0%,100%{box-shadow:calc(3px * var(--gs,1)) calc(3px * var(--gs,1)) 0 var(--ink);}
  50%{box-shadow:calc(3px * var(--gs,1)) calc(3px * var(--gs,1)) 0 var(--ink), 0 0 0 calc(3px * var(--gs,1)) rgba(244,197,66,.75);}}
.ach-tierpip{position:absolute; top:0; left:0; font-family:var(--fontD);
  font-size:calc(6.5px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  padding:calc(2px * var(--gs,1)) calc(5px * var(--gs,1)) calc(2px * var(--gs,1)) calc(3px * var(--gs,1));
  background:var(--tmc,#c98a4b); color:#1b1510; border-right:calc(2px * var(--gs,1)) solid var(--ink);
  border-bottom:calc(2px * var(--gs,1)) solid var(--ink);}
/* ---- THE MEDAL: the rosette, with the game's own crest stamped in the middle ---- */
/* ⚠ v0.605 — NO margin-top HERE. The tile centres the medal with flexbox, so a 4px top margin
   pushed every badge on the wall 4px BELOW its own frame's centre. Any spacing a list needs goes
   on the list, never on the medal. */
.achmedal{position:relative; display:block; width:calc(52px * var(--gs,1)); height:calc(52px * var(--gs,1));
  flex:none;}
.achmedal.big{width:calc(74px * var(--gs,1)); height:calc(74px * var(--gs,1)); margin:0 auto;}
.achros{position:absolute; inset:0; width:100%; height:100%; display:block;}
.achmedal.got .achros{animation:achShine 3.4s ease-in-out infinite;}
@keyframes achShine{0%,88%,100%{filter:none;} 94%{filter:brightness(1.5);}}
/* ⚠ v0.558 — THE CREST IS CENTRED ON THE ROSETTE'S DISC, NOT PLACED BY EYE. The old
   left:31%/top:26% with a 38% box put the crest's centre at 50%,45% — five percent HIGH of the
   frame, which is what made every badge in the album read as shifted up. The rosette's white disc
   is drawn at cx/cy 32 of a 64 viewBox, i.e. dead centre, so the crest is anchored to 50%,50% and
   pulled back by half its own size. Sizes only ever change the box now; the centre cannot drift. */
.achmedal-c{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:38%; height:38%; display:block; color:var(--tac,#3f7d4e);}
.achmedal-c svg{width:100%; height:100%; display:block; image-rendering:pixelated;}
.achmedal.big .achmedal-c{width:40%; height:40%;}
.ach-tick,.ach-q{position:absolute; right:calc(-4px * var(--gs,1)); bottom:calc(2px * var(--gs,1));
  width:calc(16px * var(--gs,1)); height:calc(16px * var(--gs,1)); line-height:1;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--fontD); font-size:calc(9px * var(--gs,1)); text-align:center;
  border:calc(2px * var(--gs,1)) solid var(--ink);}
.ach-tick{background:#2e9e4f; color:#fdf6e3;}
.ach-q{background:#8a8296; color:#1b1510;}
.ach-nm{font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); line-height:1.4; word-break:break-word;}
.ach-d{font-size:calc(8.5px * var(--gs,1)); line-height:1.45; opacity:.85; word-break:break-word;}
.ach-prog{width:100%; height:calc(6px * var(--gs,1)); background:#1b151033;
  border:calc(1px * var(--gs,1)) solid var(--ink); margin-top:auto;}
.ach-progf{height:100%; background:var(--tac,#3f7d4e);}
.ach-progn{font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1)); opacity:.75;}
.ach-prog.big{height:calc(10px * var(--gs,1)); margin:calc(8px * var(--gs,1)) 0 calc(3px * var(--gs,1));}

/* ---- THE PURSE — what a badge pays ---- */
.ach-rew{display:inline-flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:calc(6px * var(--gs,1));}
.ach-rw{display:inline-flex; align-items:center; gap:calc(2px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1)); color:var(--ink);}
.ach-rw i{width:calc(11px * var(--gs,1)); height:calc(11px * var(--gs,1)); display:block; flex:none;}
.ach-rw i svg{width:100%; height:100%; display:block;}
.ach-rew.big .ach-rw{font-size:calc(12px * var(--gs,1)); gap:calc(4px * var(--gs,1));}
.ach-rew.big .ach-rw i{width:calc(17px * var(--gs,1)); height:calc(17px * var(--gs,1));}
.ach-rew.dim{opacity:.5;}
.ach-claim{margin-top:calc(4px * var(--gs,1)); width:100%;
  display:flex; flex-direction:column; align-items:center; gap:calc(2px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(7px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  background:var(--gold); color:#1b1510; border:calc(2px * var(--gs,1)) solid var(--ink);
  padding:calc(3px * var(--gs,1)) calc(2px * var(--gs,1));}
.ach-paid{margin-top:calc(4px * var(--gs,1)); font-family:var(--fontD);
  font-size:calc(7px * var(--gs,1)); color:#1f6c39; letter-spacing:calc(.5px * var(--gs,1));}
.ach-locked{margin-top:calc(4px * var(--gs,1));}

/* ---- the wall's header ---- */
.ach-hdrow{display:flex; align-items:center; justify-content:center; gap:calc(8px * var(--gs,1));}
.ach-hdmed{width:calc(30px * var(--gs,1)); height:calc(30px * var(--gs,1)); position:relative; flex:none;}
.ach-hdmed .achros{position:static;}
.ach-owed{margin-top:calc(5px * var(--gs,1)); font-family:var(--fontD);
  font-size:calc(7.5px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  background:var(--gold); color:#1b1510; border:calc(2px * var(--gs,1)) solid var(--ink);
  padding:calc(4px * var(--gs,1));}

/* ---- THE DETAIL CARD ---- */
.achdet{position:relative; text-align:center; overflow:hidden;}
.achdet-ray{position:absolute; left:50%; top:calc(-40px * var(--gs,1)); width:calc(300px * var(--gs,1));
  height:calc(300px * var(--gs,1)); transform:translateX(-50%); pointer-events:none; z-index:0;
  background:repeating-conic-gradient(from 0deg at 50% 40%, rgba(244,197,66,.22) 0deg 7deg, transparent 7deg 14deg);
  animation:achRay 22s linear infinite;}
@keyframes achRay{to{transform:translateX(-50%) rotate(360deg);}}
/* ⚠ :not(.achdet-ray) is LOAD-BEARING. This rule sits after .achdet-ray and matches at the same
   specificity, so without it the sunburst loses position:absolute, drops into the flow as a 300px
   block and pushes COLLECT off the bottom of the card on the folded phone. */
.achdet>*:not(.achdet-ray){position:relative; z-index:1;}
.achdet-med{margin:0 auto calc(4px * var(--gs,1));}
.achdet-game{display:flex; align-items:center; justify-content:center; gap:calc(5px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1));
  color:var(--ink);}
.achdet-gic{color:var(--tac,#3f7d4e);}
.achdet-gic{width:calc(14px * var(--gs,1)); height:calc(14px * var(--gs,1)); display:block;}
.achdet-gic svg{width:100%; height:100%; display:block;}
.achdet-nm{font-size:calc(12.5px * var(--gs,1)); line-height:1.35; margin:calc(4px * var(--gs,1)) 0;
  word-break:break-word;}
.achdet-d{font-size:calc(9.5px * var(--gs,1)); line-height:1.45; opacity:.9; margin-bottom:calc(5px * var(--gs,1));}
.achdet-tier{display:inline-block; font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1));
  letter-spacing:calc(1px * var(--gs,1)); background:var(--tmc,#c98a4b); color:#1b1510;
  border:calc(2px * var(--gs,1)) solid var(--ink); padding:calc(3px * var(--gs,1)) calc(8px * var(--gs,1));}
.achdet-when{margin-top:calc(6px * var(--gs,1)); font-family:var(--fontD);
  font-size:calc(7px * var(--gs,1)); opacity:.6;}
.achdet-pay{margin:calc(7px * var(--gs,1)) 0; padding:calc(6px * var(--gs,1)) calc(5px * var(--gs,1));
  background:#1b15100f; border:calc(2px * var(--gs,1)) dashed var(--ink);}
.achdet-payhd{font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1));
  letter-spacing:calc(2px * var(--gs,1)); opacity:.7; margin-bottom:calc(5px * var(--gs,1));}
.achdet-done{font-family:var(--fontD); font-size:calc(11px * var(--gs,1)); color:#186030;
  letter-spacing:calc(1px * var(--gs,1)); padding:calc(5px * var(--gs,1)) 0;}
.achdet-locked{font-family:var(--fontD); font-size:calc(9px * var(--gs,1)); opacity:.6;
  letter-spacing:calc(1px * var(--gs,1)); padding:calc(6px * var(--gs,1)) 0;}

/* ---- THE BOULEVARD SHOWCASE — the gigs bar shows off the badge wall ----
   ⚠⚠ v0.604 — STILL, AND TWICE THE SIZE. The strip used to roll (gigRoll) and flip each medal on
   hover (gigFlip); both are GONE, and the row starts hard against the left edge and reads left to
   right. Only the RIGHT edge fades, to say "there are more on the wall" — a fade on the left just
   made the first badge look broken. The badges are 60px folded / 88px unfolded, double the old
   30/44. Do not re-add an animation here. */
/* ⭐⭐ v0.606 — THE MARCH IS BACK. ⚠ v0.615 — IT TRAVELS RIGHT TO LEFT AGAIN (Odaroo: that is how it
   originally read, and it is the way a marquee sign scrolls). The track starts at 0 and slides to
   -50%; do not flip it back.
   ⚠⚠ THE GAP IS A MARGIN, NOT `gap`. A seamless marquee slides the track by exactly one cycle, and
   the CSS `gap` puts a space between the two copies as well as inside them — so half the track's
   width is one cycle plus HALF a gap, and the strip visibly jumped every time it wrapped. As a
   right margin on every medal each one occupies exactly width+gap, and half the track is exactly
   one cycle. Both edges fade now, because a moving row has to arrive from somewhere. */
.gigshow{position:relative; min-width:0; overflow:hidden; display:flex; align-items:center;
  -webkit-mask-image:linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image:linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);}
.gigshow-track{display:flex; align-items:center; justify-content:flex-start;
  width:max-content; height:100%; will-change:transform;
  animation:gigMarch 60s linear infinite;}
@keyframes gigMarch{from{transform:translateX(0);} to{transform:translateX(-50%);}}
.gsb{position:relative; display:block; flex:none;
  height:100%; width:auto; aspect-ratio:1/1; margin-right:calc(7px * var(--gs,1));
  max-height:calc(60px * var(--gs,1)); min-height:calc(20px * var(--gs,1));
  filter:grayscale(1) brightness(.85); opacity:.6;}
.gsb.got{filter:none; opacity:1;}
.gsb .achros{position:absolute; inset:0; width:100%; height:100%;}
/* ⚠ v0.606 — anchored to the rosette's disc (50%,50%), the same as .achmedal-c. It still carried
   the by-eye 31%/26% that was put right on the album's badges back in v0.558. */
.gsb-c{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  width:38%; height:38%; display:block; color:var(--tac,#3f7d4e);}
.gsb-c svg{width:100%; height:100%; display:block;}
.gigshow-cnt{display:flex; flex-direction:column; align-items:flex-end; justify-content:center;
  gap:calc(2px * var(--gs,1)); font-family:var(--fontD); font-size:calc(7.5px * var(--gs,1));
  letter-spacing:calc(.5px * var(--gs,1)); line-height:1; white-space:nowrap;}
/* ⚠ gold LETTERING on cream reads at 3.3:1. The purse count is ink ON gold instead. */
.gigshow-cnt b{background:var(--gold); color:#1b1510; border:calc(1.5px * var(--gs,1)) solid var(--ink);
  padding:calc(2px * var(--gs,1)) calc(3px * var(--gs,1)); font-size:calc(6.5px * var(--gs,1));
  animation:achOwed 1.05s steps(2) infinite;}
/* the showcase BRIGHTENS when the cursor lands on the bar (it keeps marching either way) */
#menuStack .mtile.gigbar:hover .gsb,
#menuStack .mtile.gigbar.selm .gsb{filter:none; opacity:.85;}
#menuStack .mtile.gigbar:hover .gsb.got,
#menuStack .mtile.gigbar.selm .gsb.got{opacity:1;}
@media (prefers-reduced-motion:reduce){
  .achdet-ray,.achmedal.got .achros,.achcard.owed{animation:none!important;}
  .gigshow-track{animation:none!important; transform:none!important;}   /* the march stands still */
}

/* ============================================================
   THE CHEAT-CODE DROP — a badge falls in from the top, holds, and lifts away.
   ============================================================ */
.achpop{position:fixed; left:50%; top:calc(10px * var(--gs,1)); transform:translateX(-50%); z-index:9998;
  display:flex; align-items:center; gap:calc(9px * var(--gs,1)); cursor:pointer;
  width:max-content; max-width:calc(100vw - 24px);
  padding:calc(8px * var(--gs,1)) calc(13px * var(--gs,1)) calc(8px * var(--gs,1)) calc(8px * var(--gs,1));
  background:#1b1510f2; border:calc(3px * var(--gs,1)) solid var(--gold);
  box-shadow:0 calc(5px * var(--gs,1)) 0 rgba(0,0,0,.55), 0 0 calc(18px * var(--gs,1)) rgba(244,197,66,.35);
  animation:achDrop .5s cubic-bezier(.2,1.6,.45,1) both;}
@keyframes achDrop{
  0%{transform:translateX(-50%) translateY(calc(-130px * var(--gs,1))); opacity:0;}
  60%{transform:translateX(-50%) translateY(calc(5px * var(--gs,1))); opacity:1;}
  100%{transform:translateX(-50%) translateY(0); opacity:1;}}
.achpop.out{animation:achLift .4s ease both;}
@keyframes achLift{to{transform:translateX(-50%) translateY(calc(-130px * var(--gs,1))); opacity:0;}}
.achpop-badge{width:calc(42px * var(--gs,1)); height:calc(42px * var(--gs,1)); flex:none;
  background:var(--tac,#3f7d4e); border:calc(2px * var(--gs,1)) solid var(--gold); padding:calc(3px * var(--gs,1));
  animation:achSpark .45s steps(2) 3;}
@keyframes achSpark{50%{filter:brightness(2.1) saturate(.4);}}
.achpop-badge svg{width:100%; height:100%; display:block; image-rendering:pixelated;}
.achpop-txt{min-width:0;}
/* ⭐ v0.711 — READABLE AT A GLANCE, mid-match, the same pass that grew the toast. The name is the
   line that matters, so it took the biggest step (11 → 15). ⚠ The `top` on .achpop above is
   overwritten inline by inkNoteTop() on every card — this one hangs under the HUD bar like every
   other pop-up in the game, and the 10px here is only the fallback. */
/* ⚠ v0.734 — 9px → 13px. The card is TWO LINES like every other notification in the game, and its
   first line is the one that names what just happened ("ACHIEVEMENT UNLOCKED!"). At 9px against a
   15px name it read as a caption on the badge rather than as the headline — a category line you
   have to lean in for is not a category line. The 2px tracking comes down with it, or the longer
   headline stops fitting the card on the folded phone. */
/* ⭐ v0.743 — the badge card comes down to 80% with the toast (Odaroo: "make sure that it's a
   little bit smaller whenever there's a cheat code"). 13 → 11 headline, 15 → 12 name. */
/* ⭐⭐⭐ v0.745 — THE BADGE CARD IS THE SAME SIGN AS THE TOAST: one font, one size, one colour on
   both of its lines (Odaroo, 2026-08-07). The headline was 11px GOLD against a 12px cream name —
   two sizes and two colours on a two-line card, the same fault as the strip. Both are 12px cream
   now, and the exclamation mark is what makes the first line read as the headline. */
.achpop-hd{font-family:var(--fontD); font-size:calc(12px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1)); color:#fdf6e3;}
/* ⚠⚠ v0.743 — TWO LINES MEANS TWO LINES ON THIS CARD TOO. `word-break:break-word` let a long badge
   name wrap onto a third and fourth row, which is the rule the whole notification system is built
   on being broken by the one pop-up that celebrates hardest. The name is one row and ellipsises;
   the card is already capped at the screen width, so there is nothing to gain by growing taller. */
.achpop-nm{font-family:var(--fontD); font-size:calc(12px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1)); line-height:1.35; color:#fdf6e3;
  margin:calc(4px * var(--gs,1)) 0 calc(2px * var(--gs,1));
  max-width:100%; overflow-wrap:anywhere;}
/* ⭐⭐⭐ v0.767 — NOTHING ON THIS CARD IS EVER CUT OFF WITH THREE DOTS. (Odaroo, 2026-08-08: he
   photographed "ACHIEVEMENT UNLOCKE…" on the folded phone.) This card obeys the same rule as the
   toast: the HEADLINE stays on one row and is SHRUNK to fit by achPopFit(); the NAME under it may
   take a second row instead. Do not put the ellipsis back on either line. */
.achpop-hd{white-space:nowrap; overflow:hidden; max-width:100%;}
.achpop-sub{font-size:calc(11px * var(--gs,1)); color:#cdbfa6;}

/* ============================================================
   ⭐ v0.685 — THE INSTALL INVITE. Same drop-from-the-top card as the badge pop, so it reads as
   part of the game rather than as a browser thing bolted on.
   ⚠ TOP, NEVER BOTTOM: the D-pad owns the bottom of the glass and a card down there would sit
   on the controls. z-index is one under .achpop — a badge that lands while this is up must win.
   ⚠ FIXED WIDTH-CAP, not width:auto: it is anchored by translateX(-50%), and anything anchored
   by a percentage transform slides about when its words change (see the coin-toss note). ============================================================ */
/* ⭐⭐ v0.696 — IT IS AN ACHIEVEMENT CARD NOW, NOT A NOTIFICATION (Odaroo, 2026-08-06).
   The old card was a thin strip with 8px type and a stamp-sized button, which read as a browser
   toast. This is the badge pop BLOWN UP: one centred column — spinning cartridge, header, big
   name, the line underneath, then a full-width button you cannot miss.
   ⚠ THE FIXED WIDTH-CAP STAYS, and it matters more now: the card is still anchored by
   translateX(-50%), and anything anchored by a percentage transform slides about when its words
   change (the coin-toss note). Both cards — invite and celebration — are the same box.
   ⚠ TOP, NEVER BOTTOM: the D-pad owns the bottom of the glass. And it must stay SHORT enough to
   clear the pad on the folded phone, which is what the 92px cart and the 12px type are sized for
   (measured at 375x812: ~250px tall, pad starts at ~572). */
.inkinst{position:fixed; left:50%; top:calc(10px * var(--gs,1)); transform:translateX(-50%); z-index:9997;
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:calc(5px * var(--gs,1));
  width:min(calc(100vw - 24px), calc(300px * var(--gs,1)));
  padding:calc(14px * var(--gs,1)) calc(14px * var(--gs,1)) calc(12px * var(--gs,1));
  background:#1b1510f8; border:calc(4px * var(--gs,1)) solid var(--gold);
  box-shadow:0 calc(6px * var(--gs,1)) 0 rgba(0,0,0,.55), 0 0 calc(26px * var(--gs,1)) rgba(244,197,66,.45);
  animation:achDrop .5s cubic-bezier(.2,1.6,.45,1) both;}
.inkinst.out{animation:achLift .4s ease both;}
/* the celebration wears the badge-green, so "asked" and "done" are never confused */
.inkinst.done{border-color:#7fe3c8; box-shadow:0 calc(6px * var(--gs,1)) 0 rgba(0,0,0,.55), 0 0 calc(26px * var(--gs,1)) rgba(127,227,200,.45);}
.inkinst.done .inkinst-hd{color:#7fe3c8;}
.inkinst.done .inkinst-ico{border-color:#7fe3c8;}
.inkinst.done .inkinst-go{background:#7fe3c8;}
/* ⭐⭐⭐ v0.700 — THIS IS THE GLITCH CART, THE SAME ONE THE BOOT SCREEN SHOWS. The box is all this
   sheet owns; what is inside it is inkCartRig() (js/ui.js), styled in css/base.css — a cartridge
   spinning and glitching in 3D with the ink drop still and clean at its centre.
   ⚠ The rig is `position:absolute; inset:0`, which fills the PADDING BOX and so ignores padding —
   the breathing room has to be written as the rig's own inset instead. */
.inkinst-ico{width:calc(92px * var(--gs,1)); height:calc(92px * var(--gs,1)); flex:none;
  border:calc(3px * var(--gs,1)) solid var(--gold); background:#000; position:relative; padding:0;}
.inkinst-ico .ickrig{inset:calc(6px * var(--gs,1));}
.inkinst-hd{font-family:var(--fontD); font-size:calc(9px * var(--gs,1)); letter-spacing:calc(2.5px * var(--gs,1)); color:var(--gold);}
.inkinst-nm{font-family:var(--fontD); font-size:calc(15px * var(--gs,1)); line-height:1.35; color:#fdf6e3;
  margin:calc(1px * var(--gs,1)) 0; word-break:break-word;}
.inkinst-sub{font-size:calc(11px * var(--gs,1)); line-height:1.4; color:#e3d7bf;}
.inkinst-go{width:100%; font-family:var(--fontD); font-size:calc(13px * var(--gs,1));
  letter-spacing:calc(2px * var(--gs,1)); color:#1b1510; background:var(--gold);
  border:calc(3px * var(--gs,1)) solid #1b1510; cursor:pointer;
  margin-top:calc(4px * var(--gs,1)); padding:calc(11px * var(--gs,1)) calc(10px * var(--gs,1));
  box-shadow:0 calc(3px * var(--gs,1)) 0 rgba(0,0,0,.5);}
.inkinst-go:active{transform:translateY(calc(2px * var(--gs,1))); box-shadow:none;}
/* the dismiss is a CORNER cross now — the OK button owns the bottom of the card */
.inkinst-no{position:absolute; top:calc(4px * var(--gs,1)); right:calc(4px * var(--gs,1));
  width:calc(28px * var(--gs,1)); height:calc(28px * var(--gs,1));
  font-size:calc(13px * var(--gs,1)); line-height:1; color:#cdbfa6; background:transparent;
  border:calc(2px * var(--gs,1)) solid #6b5b44; cursor:pointer; padding:0;}
@media (prefers-reduced-motion:reduce){ .inkinst,.inkinst.out{animation:none!important;} }   /* the rig quiets itself — see css/base.css */

/* the featured act already prints its price in ftn-need — the chain overlay must not say it twice */
#menuStack .mtile.feat .chainlock.locked .cl-need{display:none;}

/* ⚠ FILL THE SCREEN (the standing rule). The three rows of the boulevard are sized by homeFill()
   in game.js, which measures the stack's REAL content box and writes --hfeat/--hpair/--hbar.
   ⚠ Neither fr units NOR --appvh work here. fr collapses (the stack is a flex item whose height grid
   treats as indefinite) and --appvh is the WHOLE app — on the folded screen the stack also carries a
   248px bottom padding to clear the D-pad, so half of --appvh put the gigs bar underneath it. */
#menuStack .mtile.feat{height:var(--hfeat, calc(206px * var(--gs,1)));}
#menuStack .mtile.poster{height:var(--hpair, calc(92px * var(--gs,1)));}
#menuStack .mtile.gigbar{height:var(--hbar, calc(92px * var(--gs,1)));}   /* v0.604 — the badges are twice the size, so the bar is taller to hold them */
#menuStack .mtile.feat .menu-art{height:40%; width:auto; aspect-ratio:1/1; max-width:56%; min-height:calc(56px * var(--gs,1));}
#menuStack .mtile.poster .menu-art{height:36%; width:auto; aspect-ratio:1/1; min-height:calc(24px * var(--gs,1));}
#menuStack .mtile.gigbar .menu-art{height:56%; width:auto; aspect-ratio:1/1; min-height:calc(24px * var(--gs,1));}
/* the JP line under the featured act must WRAP, never run off the side of the curtain */
#menuStack .mtile.feat .jplab{white-space:normal; max-width:78%; line-height:1.3;}

/* ⭐⭐⭐ v0.686 — THE ACT'S NAME IS WHITE, BECAUSE THE STAGE IS BLACK NOW. Odaroo: "the game title
   should be WHITE text, with the Japanese equivalent underneath also in white."
   ⚠⚠ THIS RULE IS THE ONLY PLACE THE FEATURED TYPE'S COLOUR IS DECIDED, and it is written far
   below the block that sizes it — a `color` up there is simply overruled here, which cost a pass.
   It replaces the ink-on-cream pair that crReadable had forced when the stage was lit cream (that
   version was itself a fix for inherited cream-on-cream at 1.2:1 — the lesson being that this one
   line has to be rewritten every time the tile's field changes, and it is now black).
   ⚠ NO INK RING. Ringing black type in cream is what made it legible on the cream stage; a black
   ring on a black stage is a smudge. White type gets the crest's own cold BLOOM instead, so it
   still has an edge. */
#menuStack .mtile.feat .menu-label,
#menuStack .mtile.feat .jplab{color:#ffffff; -webkit-text-stroke:0;
  text-shadow:0 0 calc(2px * var(--gs,1)) #cdeeff, 0 0 calc(8px * var(--gs,1)) rgba(85,185,255,.55);}

/* ⭐ v0.406 — the freshly-unlocked act announces itself: a gold ring and a shake, once, on arrival */
#menuStack .mtile.feat.showcase{animation:ftnShow 2.4s ease-out 1;}
@keyframes ftnShow{
  0%{box-shadow:0 0 0 0 #f4c542, 0 0 0 0 #f4c54200; transform:translateX(0);}
  6%{transform:translateX(-6px);} 12%{transform:translateX(6px);}
  18%{transform:translateX(-4px);} 24%{transform:translateX(4px);} 30%{transform:translateX(0);}
  0%,30%,60%{box-shadow:0 0 0 calc(4px * var(--gs,1)) #f4c542, 0 0 calc(22px * var(--gs,1)) #f4c542cc;}
  100%{box-shadow:0 0 0 0 #f4c54200, 0 0 0 #f4c54200;}
}
@media (prefers-reduced-motion:reduce){ #menuStack .mtile.feat.showcase{animation:none;} }

/* ⭐⭐ v0.406 — the VS name strip: you on the left, them on the right, on every vs screen.
   ⚠ It sits BELOW the top row (the inkling and the wordmark both live up there) and it never takes
   a tap — the "?" button and the pause control are directly above it. */
/* ⭐⭐ v0.409 — A 1930s FILM-REEL LABEL, PARKED UNDER THE PORTRAIT.
   The strip used to sit at a guessed 46px, which on Crown Deluxe cut straight across the middle of
   both circular inklings. Its `top` is now MEASURED from the portraits themselves (vsNamePlace), and
   each name is dressed as a strip of film: a black leader with a row of sprocket holes above and
   below the lettering, hard against its own edge of the screen — yours on the left, theirs on the
   right — exactly like the reel motif on the entrance marquee. */
/* ⭐⭐⭐ v0.536 — ONE CONTINUOUS STRIP OF FILM ACROSS THE TOP. (Odaroo, 2026-08-03: "remove the white
   light bars at the start and end of the usernames, the reel background should be white not yellow,
   and it should read as one continuous black bar.")
   Three things were breaking the run of it: the two cells were pushed apart by a gap, each carried a
   CREAM VERTICAL BORDER on its inner edge (the "white light bar"), and nothing painted the space
   between them. So the row itself now wears the film — same leader, same sprocket rows, at the same
   scale — and the two name cells sit on top of it seamlessly. Nothing about their own skin changed,
   which is why they still line up hole-for-hole with the strip underneath. */
/* ⭐⭐⭐ v0.548 — AND NOW THE MIDDLE OF IT IS GONE AGAIN. (Odaroo, 2026-08-03: "remove the film-reel
   bar in the MIDDLE between the two usernames — the reel behind each name stays, the one joining
   them across the top does not." It is the same strip v0.536 deliberately drew, and it is a
   deliberate reversal, not a regression: the run of film across the top edge sat over the middle of
   every playfield and read as a black band with nothing in it.)
   ⚠⚠ THE ROW IS NOW A TRANSPARENT LAYOUT BOX AND NOTHING ELSE — every scrap of skin belongs to the
   two .vsnm-c cells, which still carry the identical leader and sprocket rows they always did. Do
   NOT put a background back on .vsnm; if a strip has to be joined again it is two cells growing to
   meet, never a third bar painted underneath them. */
/* ⭐⭐⭐ v0.701 — THE IN-GAME REEL PRINTS SMALLER THAN THE MENU BAR. One line, and it re-points
   --nmsz for everything inside the strip: the cells, the name, the turn arrow (which is sized
   `--nmsz * 1.5`) and the wells that reserve `--nmsz * 1.6 + pad` all follow it without a second
   edit. See the note on --nmsz-game in css/base.css for why this is a separate var. */
.vsnm{--nmsz:var(--nmsz-game,13px);}
/* ⭐⭐⭐ v0.705 — THE ONE HEADER ROW, AND THE CLOCK SITS BARE IN THE MIDDLE OF IT.
   (Odaroo, 2026-08-06, on two Panel-Ink screenshots: "the top film reel row is the ONE header for
   all games — LEFT username, CENTER the time as 00:00 at the same font size, RIGHT opponent, all on
   the SAME ROW, same Y. No new film reel for the timer. Delete the second timer.")

   ⚠⚠⚠ THIS REPLACES v0.701's `.vsmid` AS A FILM STRIP ON A ROW OF ITS OWN — which is exactly what
   was asked to go. The reel already IS the header; a second strip under it made two headers, and on
   Panel-Ink a third figure floated over the board as well (#hzClock, now deleted).
   ⚠⚠ NO REEL, NO PLATE, NO BACKGROUND. The two names carry the film; the time is bare cream type
   between them, which is what makes the row read as one header rather than three objects.
   ⚠ IT IS SIZED FROM --nmsz, THE SAME VAR THE USERNAMES USE — "same font size as usernames" is the
   ask, so it must not be a number of its own or the two drift the next time the reel is resized.
   ⚠ A FIXED WIDTH, because it is anchored by a percentage transform and its text changes every
   second: a `width:auto` box under `translateX(-50%)` re-measures and visibly slides — the
   coin-toss fault of v0.679. Wide enough for `00:00` and centred inside that.
   ⚠⚠ ITS `top` IS WRITTEN BY vsClockPaint FROM THE REEL'S MEASURED CENTRE, never from a constant.
   "Same Y as the usernames" has to survive --hudtop moving (it already has once, in v0.641) and the
   reel changing size. And it is a child of the SCREEN, not of the bar, because the SOLO reel is
   `width:max-content` and hugs its own corner — a child of that would centre on the username
   rather than on the screen.
   ⚠ z-index above any full-bleed backdrop, and `position` is what earns it a paint layer at all —
   see the CLAUDE.md note on positioned backdrops painting over non-positioned siblings. */
/* ⭐⭐⭐ v0.712 — AND NOW IT WEARS THE FILM AGAIN, AND THAT IS A DELIBERATE REVERSAL OF v0.705.
   (Odaroo, 2026-08-06: "put it in the SAME frame, the SAME size as a film reel behind the username,
   behind the enemy inkling. Centred in the middle. Give some space in between the names and the
   time.") v0.705 stripped the plate off on the reasoning that three objects on one row read as
   three headers; the answer Odaroo wants is the opposite one — three pieces of the SAME film read
   as one strip broken by daylight, exactly the way the two name cells already do.
   ⚠⚠ THE SKIN IS COPIED FROM .vsnm-c BELOW ON PURPOSE and must stay in step with it: same ink,
   same 2px borders, same drop, same vertical padding, and its sprockets come from the ONE shared
   rule in css/base.css (which .vsmid was added to in the same build). "The same film reel" is a
   promise about every one of those numbers, not just the colour.
   ⚠⚠⚠ background-COLOR, NEVER the `background:` shorthand — this file loads AFTER base.css, so a
   shorthand here would reset background-image and silently wipe the holes. Two builds have already
   been lost to that (v0.473, v0.536).
   ⚠⚠⚠ THE WIDTH STILL COVERS THE WORST FIGURE PLUS ITS OWN PADDING. The 6.3 multiplier is the INK
   ("99:59" measured, letter-spacing included — see below); the plate adds 6px of padding each side,
   so the box must too, or the reel clips the very figure it was widened for. It stays a FIXED width
   for the same reason it always has: this box is anchored by a percentage transform and its text
   changes every second, so a `width:auto` box would re-measure and visibly slide (the coin-toss
   fault of v0.679). box-sizing is border-box, so the calc below is the OUTSIDE edge and that is
   what vsMidReserve books. */
.vsmid{position:absolute; left:50%; transform:translate(-50%,-50%); z-index:12; pointer-events:none;
  box-sizing:border-box;
  color:#fdf6e3; background-color:#0d0a08;
  border-top:calc(2px * var(--gs,1)) solid #0d0a08; border-bottom:calc(2px * var(--gs,1)) solid #0d0a08;
  padding:calc(4.75px * var(--gs,1)) calc(6px * var(--gs,1));
  box-shadow:0 calc(2px * var(--gs,1)) 0 rgba(13,10,8,.55);
  overflow:hidden;
  /* ⚠⚠ 6.3, MEASURED, NOT GUESSED — 5.4 was arithmetic (five glyphs at one em) and the display
     face carries a letter-spacing on top of that. Panel-Ink's own check caught it at 906: "99:59''
     wanted 90px in an 81px box, so the figure overflowed its own element and vsMidReserve, which
     books the ELEMENT, under-reserved the middle. Re-derive this if --nmsz-game or the tracking
     ever changes; hzSelfTest measures it against 99:59 on every sweep. */
  width:calc(var(--nmsz-game,13px) * 6.3 + 12px * var(--gs,1)); text-align:center;
  font-family:var(--fontD); font-size:var(--nmsz-game,13px);
  /* ⚠ 1.35, MATCHED TO .vsnm-c, NOT 1.1. The three plates are one strip of film only if they are
     the same THICKNESS, and the line box is what sets a plate's height. At 1.1 the clock's reel
     came out ~3px shallower than the two names either side of it and read as a smaller, different
     object dropped between them — which is the whole fault this build is fixing. */
  line-height:1.35;
  letter-spacing:1px; white-space:nowrap;
  /* ⚠ NO INK RING. v0.705 needed one because the figure sat bare over the playfield; it has a black
     plate behind it now, exactly as the usernames do, and the usernames wear no ring. Leaving it on
     would make the clock the one bold thing on a row that is supposed to read as three equals. */}
.vsnm{position:absolute; left:0; right:0; z-index:9; pointer-events:none;
  /* ⚠ v0.578 — --satnm, not the raw inset: in true fullscreen there is no status bar to clear and
     the corners are glass either side of the camera hole. See the note in css/base.css. */
  /* ⚠ v0.641 — 46px is 30 + the top gap. Split so this default follows --hudtop with everything
     else; vsNamePlace overwrites it from the portraits the moment there is a layout to measure. */
  top:calc(30px * var(--gs,1) + var(--hudtop,16px) + var(--satnm, env(safe-area-inset-top,0px)));
  display:flex; justify-content:space-between; align-items:stretch;
  padding:0; gap:0;
  background:none;}
/* ⚠⚠ 46% → 47.5%, AND THE SIDE PADDING COMES IN. MEASURED, not nudged: at the new 13px face a
   12-character worst-case name ("WWWWWWWWWWWW", the widest string the 12-char cap can produce)
   wants 162px and 46% of a folded 375px screen minus 8px of padding each side gave it 157. Five
   pixels short is a clipped name, and the worst case is the case that has to fit (v0.454).
   At 48.5% with 6px of padding the cap stops binding at all on that worst case: the cell settles at
   its own content width, measured 174px, and the name asks for 162 and gets 162 — unclipped, with
   27px of empty screen left between the two strips. THAT GAP IS THE POINT: with nothing painting
   underneath them any more, the daylight is the only thing saying they are two strips and not one
   bar, so the cap must stay under 50% however this face is resized. */
.vsnm-c{max-width:48.5%; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  /* ⭐⭐ v0.548 — BIGGER. Odaroo: "the usernames need to be more prominent." 10px/12px was sized to
     keep a 12-character name inside 46% of a folded screen; at 13px the same worst case is ~112px
     of a 375px screen, so it still clears its own max-width with room and reads at arm's length.
     ⚠ THE INSTRUCTION PANEL IS MATCHED TO THIS NUMBER (see #ipInfo in rhythm.css). Change it
     and change that with it — Odaroo’s rule is that the name and the instructions are
     equally prominent. (v0.567: the level tab that was the third of these is deleted.)
     ⭐⭐⭐ v0.567 — AND THE NUMBER ITSELF MOVED OUT, to --nmsz in css/base.css. It is unchanged
     (13px folded, 16px from 640px up) — but it is now the size the HOME BAR and the PROFILE print
     a name at too, because Odaroo's note was that this reel is how big a username should always
     be. Editing it here would only resize the match; edit --nmsz. */
  font-family:var(--fontD); font-size:calc(var(--nmsz) * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  color:#fdf6e3; background-color:#0d0a08; line-height:1.35;
  border-top:calc(2px * var(--gs,1)) solid #0d0a08; border-bottom:calc(2px * var(--gs,1)) solid #0d0a08;
  /* ⚠⚠ v0.554 — 4.75px VERTICAL, NOT 6, AND IT IS MEASURED. The level chip that now rides inside the
     cell is 20px tall against the name's 17.5px line box, so it grew the whole strip 33.5 → 36px —
     and the strip's height is what pushes the portraits down and what the pre-flight measures the
     playfield against. Two and a half pixels put TETR-INK's board under the top HUD (a fault the
     baseline did not have) and made INK PANIK's worse. The 2.5px comes back out of the cell's own
     padding, so the film is exactly the thickness it has always been and the chip sits INSIDE it.
     ⚠ If the chip's padding or face changes, re-measure this. It is a difference, not a constant. */
  padding:calc(4.75px * var(--gs,1)) calc(6px * var(--gs,1));
  box-shadow:0 calc(2px * var(--gs,1)) 0 rgba(13,10,8,.55);
  /* ⚠⚠ v0.567 — THE SPROCKET HOLES MOVED OUT to the shared rule in css/base.css, where they are now
     drawn CENTRED and MIRRORED rather than started hard against the left edge. Nothing about this
     strip's thickness or colour changed. The one thing that MUST stay true here is the line above:
     `background-color`, never the `background:` shorthand — this file loads AFTER base.css, so a
     shorthand would reset background-image and wipe the holes. */}
/* ⭐⭐⭐ v0.473 — ONE FILM-STRIP RECIPE, SHARED. Odaroo: everything that is a strip of film should
   be the SAME strip of film — same thickness, same sprocket holes, same weight — whether it is a
   username in a corner, the turn ticker between the inklings, or the RECONNECTING sign.
   ⚠ The numbers below are copied from .vsnm-c above ON PURPOSE and must stay in step with it: it is
   the strip everything else is being matched TO. If that one changes, change this with it.
   ⚠⚠ ONE NUMBER IS NOW DELIBERATELY OUT OF STEP: v0.536 lifted the USERNAME strip to 10px/12px so a
   full name is readable at arm's length, and left this at 8px/10px. That is not drift. #crTurn is
   PINNED TO 25px because crFit measures that band to place the Crown table (v0.447), so growing the
   shared strip's face would shrink the folded board. The leader, the holes and the colour still
   match, which is what "one strip of film" actually means. */
.inkfilm{
  font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); letter-spacing:calc(.5px * var(--gs,1));
  /* ⚠⚠ background-COLOR, NEVER the `background:` shorthand — see the note on .vsnm-c above. The
     sprockets now come from the shared rule in css/base.css, which loads FIRST, so a shorthand
     here would wipe them. */
  color:#fdf6e3; background-color:#0d0a08; line-height:1.35;
  border-top:calc(2px * var(--gs,1)) solid #0d0a08; border-bottom:calc(2px * var(--gs,1)) solid #0d0a08;
  padding:calc(6px * var(--gs,1)) calc(8px * var(--gs,1));
  box-shadow:0 calc(2px * var(--gs,1)) 0 rgba(13,10,8,.55);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
@media (min-width:640px){ .inkfilm{font-size:calc(10px * var(--gs,1));} }
/* ⚠⚠ NO VERTICAL BORDERS. Both cells used to cap their inner edge with a 3px cream rule, which read
   on a real screen as a WHITE LIGHT BAR chopping the black bar into two pieces — the exact thing
   v0.536 was asked to remove. The strip runs unbroken now; only the rival's warm ink is kept. */
.vsnm-c.r{color:#ffd6cc;}   /* the rival reads warm, the way the brawl's FOE label does */
/* (v0.567 — the ≥640px 16px bump lives in --nmsz now, so the home bar and the profile grow with the
   reel instead of being three separate numbers that drifted apart.) */
/* ⭐⭐ v0.422 — NAMES-ON-TOP (Tetr-Ink, Ink Panik). The strip is pinned to the very top edge and the
   two inklings are pushed underneath it by vsNamePlace, so each corner reads name-then-face. */
/* ⭐⭐⭐ v0.641 — THE 4px IS --hudtop NOW, AND IT IS 16. (Odaroo, 2026-08-05: the home screen's
   position is the standard position — mine top-left, the rival top-right, at the home reel's new
   lowered gap.) The number is declared once in css/base.css; the four #crLineBadge rules in
   css/crown.css ask for the same one, so the connecting reel comes down with the names by
   construction rather than by remembering to edit five places (which is what their comments used to
   have to ask for). ⚠ vsNamePlace (js/online.js) now MEASURES this strip's top rather than assuming
   it is ~0 — it was adding the bar's HEIGHT to zero to find where the portraits go, which put both
   faces straight back over the names the moment this gap grew. */
.vsnm.vsnm-top{top:calc(var(--hudtop,16px) + var(--satnm, env(safe-area-inset-top,0px))); z-index:11;}
/* ⚠⚠ THE PORTRAITS ARE POSITIONED AND SIZED FROM JS (vsNamePlace), NOT FROM HERE, and that is
   deliberate. The obvious stylesheet way to say "a portrait on a screen whose names are on top" is
   `.vsnm-top ~ .gink-game` — but `~` only matches LATER SIBLINGS, and whether the name strip is
   built before or after the two portraits depends on which of vsNameBar / vsFoeInk the match
   happened to call first. It matched the rival and missed mine, so one face came out 68px and the
   other 39px. Inline styles have no such ordering, so vsNamePlace writes both. */

/* (v0.406's Bit-Brawl circle rules lived here; the circle was withdrawn in v0.410 and they went with it) */

/* ============================================================
   ⭐⭐ v0.437 — THE BADGE ALBUM READS LIKE AN ALBUM
   ------------------------------------------------------------
   Odaroo: the achievement cards were so big that a handful filled the screen, so reading the album
   meant scrolling past everything. A badge album should look like one — the medals tiled tight, the
   whole tour visible at a glance. Every word that used to be printed on a card still exists; it
   moved into the detail view the tap already opened. Only the two facts you must read WITHOUT
   tapping stay on the tile: the tier, and whether a reward is waiting.
   ============================================================ */
.ach-grid.tight{grid-template-columns:repeat(auto-fill, minmax(calc(64px * var(--gs,1)), 1fr));
  gap:calc(7px * var(--gs,1)); max-width:calc(760px * var(--gs,1));}

.achtile{position:relative; aspect-ratio:1; padding:calc(4px * var(--gs,1)); cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background:var(--paper2,#f2e5c9); border:calc(2px * var(--gs,1)) solid var(--ink);
  border-radius:calc(4px * var(--gs,1)); box-shadow:0 calc(3px * var(--gs,1)) 0 #1b151055;
  transition:transform .09s ease, box-shadow .09s ease; overflow:visible;}
.achtile>svg,.achtile .achbadge,.achtile img{width:86%; height:86%; display:block;}
.achtile:not(.got){filter:grayscale(1) opacity(.42);}      /* locked reads as unearned at a glance */
.achtile.got{background:linear-gradient(160deg,#fffaf0,var(--paper2,#f2e5c9));}
.achtile:active{transform:translateY(calc(3px * var(--gs,1))); box-shadow:0 0 0 #1b151055;}
.achtile:focus-visible{outline:calc(3px * var(--gs,1)) solid var(--gold,#d9a232); outline-offset:2px;}

/* the tier, small and out of the way */
.achtile-pip{position:absolute; top:calc(-3px * var(--gs,1)); left:calc(-3px * var(--gs,1));
  font-family:var(--fontD); font-size:calc(6px * var(--gs,1)); line-height:1; letter-spacing:.5px;
  padding:calc(2px * var(--gs,1)) calc(3px * var(--gs,1)); color:#1b1510;
  background:var(--tmc,#d9a232); border:calc(1.5px * var(--gs,1)) solid var(--ink); border-radius:2px;}
/* a hairline of progress instead of a labelled row */
.achtile-bar{position:absolute; left:calc(4px * var(--gs,1)); right:calc(4px * var(--gs,1));
  bottom:calc(3px * var(--gs,1)); height:calc(3px * var(--gs,1));
  background:#1b151022; border-radius:2px; overflow:hidden;}
.achtile-bar>i{display:block; height:100%; background:var(--tac,#d9a232);}
/* a reward waiting shouts, because that is the one thing worth interrupting for */
.achtile.owed{box-shadow:0 0 0 calc(2px * var(--gs,1)) var(--gold,#d9a232), 0 calc(3px * var(--gs,1)) 0 #1b151055;}
.achtile-owed{position:absolute; top:calc(-6px * var(--gs,1)); right:calc(-6px * var(--gs,1));
  width:calc(17px * var(--gs,1)); height:calc(17px * var(--gs,1)); border-radius:50%;
  display:flex; align-items:center; justify-content:center; font-style:normal;
  font-family:var(--fontD); font-size:calc(10px * var(--gs,1)); color:#1b1510;
  background:var(--gold,#d9a232); border:calc(2px * var(--gs,1)) solid var(--ink);
  box-shadow:0 0 calc(7px * var(--gs,1)) #f4c542cc; animation:achOwed 1.05s steps(2) infinite;}

.ach-claimall{margin:calc(7px * var(--gs,1)) auto 0; display:block;}

/* ⭐ v0.437 — THE COUNT ON THE TAB POPS OUT INSTEAD OF BEING TRIMMED.
   It sat half inside the button and was clipped by its own corner, so a "1" read as a smudge. It is
   bigger, rounder, ringed in the page colour so it separates from whatever it overlaps, and it now
   sits properly OUTSIDE the button edge. ⚠ #achTabs and .ach-tab must not clip it — hence the
   overflow:visible below; without that, moving it further out just hides more of it. */
#achTabs, .ach-tabs, .ach-tab{overflow:visible;}
.ach-tabdot{top:calc(-9px * var(--gs,1)); right:calc(-8px * var(--gs,1));
  min-width:calc(22px * var(--gs,1)); height:calc(22px * var(--gs,1));
  border-radius:calc(11px * var(--gs,1)); font-size:calc(10px * var(--gs,1)); font-weight:bold;
  border-width:calc(2.5px * var(--gs,1));
  box-shadow:0 0 0 calc(2px * var(--gs,1)) #1b1510, 0 0 calc(9px * var(--gs,1)) #c93a22cc,
             inset 0 calc(2px * var(--gs,1)) 0 #ffffff55;
  z-index:6;}

/* ⭐ v0.443 — THE PLAYER CARD, and the favourite-game picker. */
.crl-tapav{cursor:pointer;}
.crl-tapav:active{transform:scale(.94);}
/* ============================================================
   ⭐ v0.460 — the SEND CHALLENGE? confirm. Deliberately tiny: a question, the name, two buttons.
   ⚠ It is an overlay INSIDE #modalBox (see crChallengeAsk) — never a dialog of its own, or it
   would replace the room and take the roster's live handlers with it. So it is absolutely
   positioned against the box, which needs a positioning context it does not otherwise have.
   ============================================================ */
#modalBox.askon{position:relative;}   /* ⚠ a CLASS, not :has() — this has to hold on older webviews */
/* ⚠⚠ v0.461 — NOTHING BEHIND THE QUESTION IS TOUCHABLE. The overlay covers the box, but "covered"
   and "inert" are not the same thing: a stray click can still be delivered straight to a roster row
   the instant the overlay stops being under the finger. Everything except the overlay is switched
   off outright while the question is up, so there is no live target back there to hit. */
#modalBox.askon>*:not(.crask-wrap){pointer-events:none;}
.crask-wrap{position:absolute; inset:0; z-index:40; display:flex; align-items:center;
  justify-content:center; padding:12px; background:#1b1510cc; backdrop-filter:blur(1.5px);
  pointer-events:auto;}
/* the shield the overlay leaves behind for ~380ms after it closes: invisible, still eating taps */
.crask-wrap.shield{background:transparent; backdrop-filter:none;}
.crask-wrap .crask{background:var(--cream); border:3px solid var(--ink); border-radius:3px;
  box-shadow:0 6px 0 #1b151055, 0 10px 24px -8px #000a; padding:16px 14px 14px;
  max-width:100%; width:min(300px,100%); max-height:100%; overflow:auto;}
/* ⭐ v0.462 — the forfeit price on the GO HOME? question. Deliberately the loudest thing in the
   dialog: the player is about to give up cups and should not have to read a sentence to find out. */
.phc-cost{display:flex; align-items:center; justify-content:center; gap:8px; margin:10px 0 2px;}
.phc-min{font-family:var(--fontD); font-size:40px; line-height:1; color:#c93a22;
  text-shadow:2px 2px 0 #1b151033; letter-spacing:1px;}
.phc-cup{width:34px; height:34px; display:inline-block;}
.phc-cup svg{width:100%; height:100%; display:block;}
.phc-why{font-family:'Press Start 2P',monospace; font-size:7px; letter-spacing:1px; color:#9e2a17;
  line-height:1.7; margin-bottom:2px;}
@media (max-width:399px){ .phc-min{font-size:34px;} .phc-cup{width:29px; height:29px;} }

/* ============================================================
   ⭐⭐ v0.463 — THE STANDARD PLAYER HUD: name + level on one line, face underneath.
   ⚠ In SOLO there is only a left cell, so the strip must not stretch it across the whole screen —
   `.vsnm-solo` keeps it hugging its own corner exactly as the left cell does in a match, or your
   name would be a banner in solo and a corner label online, which is the opposite of "identical".
   ============================================================ */
.vsnm-c{display:flex; align-items:center; gap:calc(5px * var(--gs,1));}
.vsnm-n{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* ⚠⚠⚠ v0.567 — THE LEVEL CHIP IS DELETED FROM THE REEL. v0.554 had just moved it into the film
   strip ahead of the username; the player level and XP are now removed from the game entirely, so
   .vsnm-lvt and the older .vsnm-lv are both gone. The reel is two names and nothing else.
   ⚠ vsNameFit still steps both cells down together — two long names must fit whole at 375px. */
/* ⚠ v0.469 — the right-hand cell reads LEVEL then NAME like the left, so its contents must not be
   reversed by the row's own alignment. Both cells lay their children out left to right; only the
   CELL sits on the right of the screen. */
.vsnm-c.r{flex-direction:row;}
/* the rival's name is a tap target for their stats card, exactly as their face is */
.vsnm-tap{cursor:pointer;}
.vsnm-tap:active{transform:scale(.95);}
/* ⚠⚠ v0.463 — THE SOLO STRIP IS A CORNER LABEL, NOT A BAR. In a match the strip spans the screen
   because it carries a name at each end; in solo it carries one, and left full-width it became an
   invisible 906px-wide band lying across the top of every playfield. The pre-flight caught it on
   four games ("playfield is drawn under the top HUD"). It now takes only the room its own text
   needs, so it occupies the corner the portrait is already in and nothing else. */
/* ⚠⚠ v0.554 — 66% → 52%. The corner label grew by the width of the level chip, and a solo strip is
   sized to its own CONTENT, so a long name pushed it far enough across to sit on the playfield —
   LUMEN INK failed the pre-flight at 906 with a 10-character name (29x29 shared). The cap is what
   vsNameFit measures against, so lowering it does not clip anything: the name simply steps down a
   point until it fits the narrower label whole. Half the screen is the most a CORNER may ever take. */
/* ⚠ AND A PERCENTAGE ALONE IS NOT ENOUGH ON THE UNFOLDED FOLD. 52% of 906 is 471px, so the cap did
   not bind and the label still reached 270px — 29px into LUMEN INK's #lumField, whose left edge sits
   at 241px. The playfields are CENTRED, so the room a corner label actually has stops growing once
   the screen is wide; a px ceiling that scales with --gs is what models that. 228px is measured
   against the narrowest of those left margins with air to spare. */
.vsnm.vsnm-solo{justify-content:flex-start; right:auto; width:max-content;
  max-width:min(52%, calc(228px * var(--gs,1)));
  pointer-events:none;}
.vsnm.vsnm-solo .vsnm-c.l{max-width:100%;}
/* the rival's face is a control now — it opens their card */
.vsfoe-tap{cursor:pointer;}
.vsfoe-tap:active{transform:scale(.94);}
.vfcard .pcard-grid{grid-template-columns:repeat(2,minmax(0,1fr));}

.crask{display:flex; flex-direction:column; align-items:center; gap:6px;}
.crask-h{font-family:'Press Start 2P',monospace; font-size:10px; letter-spacing:1px; color:var(--ink);
  line-height:1.7; text-align:center;}
.crask-nm{font-family:var(--fontD); font-size:19px; letter-spacing:1px; color:var(--ink);
  overflow-wrap:anywhere; text-align:center; margin-bottom:4px;}
.crask-acts{display:flex; gap:8px; width:100%; justify-content:center; flex-wrap:wrap;}
.crask-acts .btn{margin:0 !important;}
@media (max-width:399px){ .crask-h{font-size:9px;} .crask-nm{font-size:17px;} }

.pcard{display:flex; flex-direction:column; align-items:center; gap:4px;}
.pcard-av{width:84px; height:84px; position:relative; margin-bottom:2px;}
.pcard-av svg{width:100%; height:100%; image-rendering:pixelated;}
.pcard-nm{font-size:15px; letter-spacing:1px; overflow-wrap:anywhere; text-align:center;}
.pcard-lv{font-family:'Press Start 2P',monospace; font-size:7px; letter-spacing:1px; opacity:.7; margin-bottom:8px;}
.pcard-grid{display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; width:100%;}
.pcard-grid>i{display:flex; flex-direction:column; gap:3px; font-style:normal;
  background:#fdf6e3; border:2px solid #1b151022; border-left:4px solid #1b1510; border-radius:2px; padding:6px 8px;}
.pcard-grid>i>span{font-family:'Press Start 2P',monospace; font-size:6.5px; letter-spacing:.5px; color:#1b1510aa;}
.pcard-grid>i>b{font-family:var(--fontD); font-size:15px; color:#1b1510;}
.pcard-fav{width:100%; margin-top:8px; text-align:center; background:#f2e5c9; border:2px solid #1b1510;
  border-radius:2px; padding:7px 9px;}
.pcard-fav>span{display:block; font-family:'Press Start 2P',monospace; font-size:6.5px; letter-spacing:1px; opacity:.65;}
.pcard-fav>b{display:block; font-family:var(--fontD); font-size:14px; margin-top:3px; overflow-wrap:anywhere;}
.pcard .btn{margin-top:12px;}

.pf-fav{display:block; width:100%; text-align:left; cursor:pointer;
  background:#fdf6e3; border:2px solid #1b151022; border-left:4px solid var(--gold,#d9a232);
  border-radius:2px; padding:8px 10px;}
.pf-fav>b{display:block; font-family:var(--fontD); font-size:16px; color:#1b1510;}
.pf-fav>span{display:block; font-family:'Press Start 2P',monospace; font-size:6.5px; letter-spacing:.5px; color:#1b1510aa; margin-top:3px;}
.pf-favlist{display:flex; flex-direction:column; gap:5px; max-height:46vh; overflow-y:auto;}
.pf-favopt{font-family:var(--fontD); font-size:14px; text-align:left; cursor:pointer;
  background:var(--paper2,#f2e5c9); border:2px solid #1b1510; border-radius:2px; padding:8px 10px; color:#1b1510;}
.pf-favopt.on{background:var(--gold,#d9a232); box-shadow:inset 0 0 0 2px #1b1510;}

/* ============================================================
   ⭐⭐ v0.446 — DECK-A-THON: the ladder between the ten games.
   It is a CARD, not a playfield — the standings, the next act on the bill, and one button. Sized in
   fractions of the stage so the unfolded screen shows the same card BIGGER (the v0.445 rule), and
   it stops above the D-pad on both folds.
   ============================================================ */
.dk-wrap{position:absolute; left:50%; top:8px; transform:translateX(-50%);
  width:min(94%,420px); display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:10px 10px 12px; box-sizing:border-box;
  border:3px solid var(--ink); background:var(--paper2); box-shadow:3px 3px 0 #1b151055;}
.dk-head{text-align:center; line-height:1;}
.dk-score{font-family:var(--fontD); font-size:30px; color:var(--ink); display:flex; align-items:baseline; justify-content:center; gap:7px;}
.dk-score .dk-w{color:#1f7a3f;}
.dk-score .dk-l{color:#a8241a;}
.dk-score i{font-style:normal; opacity:.5; font-size:18px;}
.dk-sub{font-family:var(--fontD); font-size:8px; letter-spacing:2px; color:#3a2f26; margin-top:3px; white-space:nowrap;}
.dk-pips{display:flex; justify-content:center; flex-wrap:wrap; gap:4px; margin:2px 0 1px;}
.dk-pip{width:13px; height:13px; display:block; border:2px solid var(--ink); background:var(--cream);}
.dk-pip.won{background:#2e9e4f;}
.dk-pip.lost{background:#c93a22;}
.dk-pip.now{background:var(--gold); animation:dkNow 1.1s ease-in-out infinite;}
@keyframes dkNow{0%,100%{transform:scale(1)}50%{transform:scale(1.22)}}
.dk-up{width:100%; text-align:center; border:3px solid var(--ac,#1b1510); background:var(--cream);
  padding:6px 6px 7px; box-sizing:border-box;}
.dk-uplab{font-family:var(--fontD); font-size:7px; letter-spacing:2px; color:#3a2f26;}
/* ⚠ THE NAME IS INK, NOT THE GAME'S COLOUR. Half the accents on the boulevard — Superstars' gold
   at 2.1:1, Lumen's purple at 4.4:1 — cannot be read on cream, and crReadable rightly fails a build
   that tries. The accent earns its keep as the BORDER and the label instead. */
.dk-upnm{font-family:var(--fontD); font-size:14px; color:var(--ink); line-height:1.2; margin:2px 0 1px;}
/* ⚠ THE LABEL BAND IS INK, NOT THE ACCENT. Cream on Superstars gold is 2.1:1 — the same trap the
   name fell into. The accent stays as the box BORDER, where contrast does not apply. */
.dk-uplab{background:var(--ink); color:var(--cream); padding:2px 6px; display:inline-block; margin:-7px auto 3px;}
.dk-upjp{font-size:9px; color:#3a2f26; opacity:.8;}
.dk-uptg{font-family:var(--fontD); font-size:7px; letter-spacing:.5px; color:#3a2f26; margin-top:4px;}
.dk-go{width:100%;}
.dk-out{width:100%;}
.dk-done{font-family:var(--fontD); font-size:13px; letter-spacing:1px; color:var(--ink); padding:4px 0;}
.dk-glitch{font-family:var(--fontD); font-size:8px; letter-spacing:2px; color:#3a2f26; opacity:.85;}
/* the door + the sudden-death card */
.dk-door .dk-h{font-size:19px; letter-spacing:1px; margin-bottom:0;}
.dk-door .dk-jp{font-size:10px; color:#3a2f26; margin-bottom:6px;}
.dk-door .dk-rules{font-family:var(--fontD); font-size:8px; line-height:1.9; letter-spacing:.5px; margin:0 0 8px;}
.dk-door .dk-prize{font-family:var(--fontD); font-size:9px; letter-spacing:1px; color:var(--ink);
  border:2px solid var(--ink); background:var(--gold); padding:4px 8px; margin:0 auto 10px; display:inline-block;}
@media (min-width:560px){   /* the unfolded stage: the SAME card, bigger — never a different one */
  .dk-wrap{width:min(72%,560px); gap:9px; padding:14px 14px 16px;}
  .dk-score{font-size:44px;} .dk-sub{font-size:11px;}
  .dk-pip{width:19px; height:19px;}
  .dk-uplab{font-size:9px;} .dk-upnm{font-size:20px;} .dk-upjp{font-size:12px;} .dk-uptg{font-size:9px;}
  .dk-done{font-size:18px;} .dk-glitch{font-size:11px;}
}

/* ============================================================
   ⭐⭐ v0.447 — THE BABY-BLUE SKY, ON THE ONLINE BUTTON AND IN THE ROOM.
   Odaroo asked for the ONLINE button to drift little pixel clouds like Crown Deluxe's sky, and for
   the lobby behind it to wear the same sky so the two feel like one place.
   ⚠ THE CLOUDS ARE A BACKGROUND, NOT ELEMENTS. Crown draws its sky on canvas, which is right for a
   canvas; a button cannot. These are one inline data-URI tile scrolled with background-position, so
   there is nothing to lay out, nothing to measure, nothing to clean up, and no extra DOM inside a
   button whose text still has to be read.
   ⚠ NO EXTERNAL ANYTHING — the tile is inline SVG, same as every other pixel asset in the game.
   ============================================================ */
:root{
  /* two chunky rubberhose clouds on a transparent tile, drawn as flat pixel blocks */
  --inksky-clouds:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="120" height="48" shape-rendering="crispEdges"><g fill="%23ffffff" opacity="0.92"><rect x="14" y="14" width="24" height="6"/><rect x="10" y="20" width="32" height="6"/><rect x="20" y="8" width="12" height="6"/><rect x="74" y="30" width="20" height="5"/><rect x="70" y="35" width="28" height="5"/><rect x="80" y="25" width="10" height="5"/></g><g fill="%23dceefa" opacity="0.8"><rect x="10" y="26" width="32" height="3"/><rect x="70" y="40" width="28" height="3"/></g></svg>');
}
@keyframes inkSkyDrift{ from{ background-position:0 0, 0 0; } to{ background-position:240px 0, 120px 0; } }
.btn.opt.sky{
  background-image:var(--inksky-clouds), var(--inksky-clouds);
  background-repeat:repeat-x, repeat-x;
  background-size:120px 100%, 120px 100%;
  background-position:0 0, 60px 0;
  animation:inkSkyDrift 14s linear infinite;
}
.btn.opt.sky span{position:relative; z-index:1;}   /* the word sits in front of the weather */
.btn.opt.sky.soon{animation:none; background-image:none;}   /* an honest disabled button keeps still */
/* ⚠⚠ v0.454 — THE SKY CAME OFF `.lobby`, AND THIS IS WHY IT WAS WRONG THERE.
   v0.447 put it here meaning "the room", but `.lobby` is not the room — it is the CONNECTING / VS
   stage (crOnlineConnect and brawlLobby both build `<div class="center lobby">`). So what actually
   grew a baby-blue plate and a 3px frame was the matchup screen, which Odaroo asked to be clean and
   frameless. The room is `.crlPop`, and it wears the sky in crown.css where it belongs.
   Do not re-add a background to `.lobby`: the VS stage draws its own field. */
#modalBox .lobby{ background:transparent; border:0; }
#modalBox .lobby>*{position:relative; z-index:1;}
/* ⭐ v0.454 — MATCH FOUND LANDS WITHOUT MOVING ANYTHING.
   The line under the stage held a 9px Japanese caption and was then replaced by a 13px heading plus
   its own caption — taller than what it replaced, so the whole card grew and the two inklings
   jumped at the exact moment the player is looking at them. The box is given the taller of the two
   heights from the start and centres whatever is in it, so the swap changes the words and nothing
   else. */
#modalBox .lobby .connLine{
  min-height:34px; display:flex; flex-direction:column; align-items:center; justify-content:center;
}
@media (prefers-reduced-motion:reduce){ .btn.opt.sky,#modalBox .lobby{animation:none;} }

/* ============================================================
   ⭐⭐ v0.454 — THE NATURE TRAIL, ON THE SOLO BUTTON.
   Odaroo: ONLINE drifts clouds through a baby-blue sky, so SOLO should drift a little pixel forest
   past you — grass, trees and cactuses, "walking solo through nature". Same trick as the sky and for
   the same reasons: ONE inline data-URI tile scrolled with background-position, so there is nothing
   to lay out, nothing to measure, nothing to tear down, and no extra DOM inside a button whose word
   still has to be read.
   ⚠ TWO LAYERS AT DIFFERENT SPEEDS, NOT ONE. A single scrolling tile reads as a repeating stamp;
   a near layer and a far layer moving at different rates read as travel. The far layer is smaller,
   paler and slower — the same parallax the sky uses, one step further.
   ⚠ THE GRASS IS PART OF THE TILE, NOT A SEPARATE RULE. A border-bottom would have sat under the
   button's own frame and disappeared on the pressed state.
   ============================================================ */
:root{
  /* near: two chunky trees and a cactus standing on a grass line */
  --inktrail-near:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="140" height="48" shape-rendering="crispEdges"><g fill="%23256b32"><rect x="10" y="12" width="18" height="6"/><rect x="6" y="18" width="26" height="7"/><rect x="12" y="25" width="14" height="5"/></g><rect x="17" y="30" width="5" height="10" fill="%235a3a1e"/><g fill="%232f7d3a"><rect x="96" y="16" width="6" height="24"/><rect x="88" y="24" width="8" height="5"/><rect x="88" y="20" width="4" height="8"/><rect x="102" y="28" width="8" height="5"/><rect x="106" y="22" width="4" height="10"/></g><g fill="%233d9440"><rect x="0" y="40" width="140" height="4"/></g><g fill="%2352b04e"><rect x="4" y="38" width="3" height="3"/><rect x="46" y="38" width="3" height="3"/><rect x="70" y="38" width="3" height="3"/><rect x="122" y="38" width="3" height="3"/></g></svg>');
  /* far: smaller, paler hills and saplings, drifting slower */
  --inktrail-far:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="48" shape-rendering="crispEdges"><g fill="%231d5528" opacity="0.55"><rect x="20" y="26" width="10" height="4"/><rect x="17" y="30" width="16" height="4"/><rect x="23" y="34" width="4" height="4"/><rect x="68" y="28" width="8" height="4"/><rect x="66" y="32" width="12" height="4"/><rect x="70" y="36" width="4" height="3"/></g></svg>');
}
@keyframes inkTrailDrift{ from{ background-position:0 100%, 0 100%; } to{ background-position:-280px 100%, -100px 100%; } }
.btn.opt.trail{
  background-color:#7ec86a;
  background-image:var(--inktrail-near), var(--inktrail-far);
  background-repeat:repeat-x, repeat-x;
  background-size:140px 100%, 100px 100%;
  background-position:0 100%, 0 100%;
  animation:inkTrailDrift 18s linear infinite;
}
.btn.opt.trail span{position:relative; z-index:1;}   /* the word walks in front of the scenery */
.btn.opt.trail.soon{animation:none; background-image:none;}
@media (prefers-reduced-motion:reduce){ .btn.opt.trail{animation:none;} }


/* ============================================================
   ⭐ v0.447 — THE VICTORY CARD'S BOTTOM ROW ALWAYS FITS.
   Odaroo photographed HOME clipped against the bottom edge of the card on the unfolded screen. The
   row is pinned to the bottom of the card's own scroll area with room under it, so however tall the
   receipt gets the way out is on screen and fully drawn.
   ============================================================ */
#modalBox .endrow{display:flex; align-items:center; justify-content:center; gap:8px; flex-wrap:nowrap;
  position:sticky; bottom:0; z-index:3; padding:8px 2px 10px; margin-top:4px;
  background:linear-gradient(to bottom, transparent, var(--cream) 35%);}
#modalBox .endrow .btn{flex:1 1 auto; min-width:0; margin:0;}
#modalBox .endrow .inkico{flex:0 0 auto;}
#modalBox{padding-bottom:6px;}

/* ============================================================
   ⭐⭐⭐ v0.560 — THE END-OF-RUN BUTTONS: TWO ROWS, CENTRED, ALL THREE ON SCREEN.
   Odaroo photographed a solo rink run ending on a lone red BACK button. The block is his shape now:

       [  SAVE  ][ SHARE ]     <- row 1, the two exactly the same width
       [      HOME       ]     <- row 2, the width of the pair above it

   ⚠⚠ THE BLOCK IS STICKY AND IT CARRIES ITS OWN FADE, AND BOTH HALVES MATTER. A bill grows with the
   game that printed it (Ink-Put lists seven figures), so on the folded screen the buttons are below
   the fold of the dialog's own scroll area — pinned to the bottom they are always reachable, and the
   cream fade under them stops the receipt's rows showing through the gap between the two rows.
   ⚠ z-index 4 is WITHIN THE DIALOG and that is all it claims — it lifts the block over the receipt
   and the ticket's torn edge. The RECONNECTING sign cannot be outranked from in here (it is
   `position:fixed` at z-index 9990, css/crown.css) and it does not need to be: it is pinned to the
   MIDDLE of the screen and is `pointer-events:none`, so it neither covers the bottom of the dialog
   nor swallows a tap meant for HOME. Verified at both folds, not assumed.

   ⚠ `max-width:250px` IS MEASURED, NOT PICKED. It is the printed width of the ticket stub itself
   (`.runCard.ticket`), so the buttons finish flush with the bill they belong to on both folds.
   ============================================================ */
.endBtns{position:sticky; bottom:0; z-index:4; display:flex; flex-direction:column; align-items:center;
  gap:7px; padding:9px 2px 12px; margin-top:6px;
  background:linear-gradient(to bottom, transparent, var(--cream) 38%);}
.endBtns .endBtnRow{display:flex; align-items:stretch; justify-content:center; gap:8px;
  width:100%; max-width:250px; margin:0 auto;}
/* every child of a row splits it evenly — that is what makes SAVE and SHARE one size, and it is the
   same rule that makes HOME (an only child) span the pair's whole width. */
.endBtns .endBtnRow>*{flex:1 1 0; min-width:0; width:auto; margin:0;}
.endBtns .endBtnRow.pair .inkico{flex:1 1 0; min-width:0; width:auto;}
.endBtns .endHome{width:100%;}
/* ⚠ v0.568 — THE CARET IS GONE EVERYWHERE (see css/home.css). This override existed only to pull it
   back inside the button so it did not measure as clipped text; with nothing drawn there is nothing
   to pull. The gold ring alone is the cursor. */
/* a save/share button under the cursor reads like any other selected button */
.endBtns .inkico.gpsel{background:var(--gold,#d9a232);}

/* ⭐⭐⭐ v0.790 — THE BILL HOLDS ITS BUTTONS BACK FOR FIVE SECONDS.
   The way out of a results screen does not appear until the player has had time to actually see
   the results — a match ends mid-mash and the very next Ⓐ used to press HOME. The countdown takes
   the buttons' place so the pause is explained rather than felt as a frozen screen.
   ⚠⚠ THIS IS ONLY THE PICTURE. The refusal lives at the two input doors — `inkBillHeld()` in
   gpad() (js/ui.js) and in the keydown handler (js/arcade.js). Hiding buttons is not a lock: the
   D-pad, the keyboard and the hardware back button never touched them in the first place.
   ⚠ `visibility` and not `display`, so the block keeps its height and the card does not jump
   upwards and then back down when the buttons arrive. */
/* ⚠ `.endBtns` is already `position:sticky` above, so it is a containing block and the countdown
   can stand exactly in its place. Do not "tidy" that sticky away. */
/* ⭐⭐⭐ v0.802 — THE HOLD IS INVISIBLE NOW (Odaroo, 2026-08-13). The buttons are THERE, in place,
   fully drawn, from the moment the bill lands — they simply do not answer for three seconds. The
   old "HOLD ON — 3 / 2 / 1" countdown standing in their place is deleted, and so is hiding the
   block: a receipt that visibly withholds its own buttons reads as a frozen game, which is the
   fault the countdown was invented to explain rather than to avoid.
   ⚠⚠ `pointer-events:none` IS THE WHOLE PICTURE LAYER NOW, and it is the only thing here that
   matters: with the buttons visible, a finger can reach them, and the two input doors
   (`inkBillHeld()` in gpad() and in the keydown handler) never see a direct tap. It is not
   belt-and-braces — it is the third door.
   ⚠ Do NOT dim, grey, disable or otherwise mark the buttons during the hold. Odaroo asked for a
   quiet guard: nothing on screen may announce it. */
#modalBox.billhold .endBtns{pointer-events:none;}
#modalBox.billhold .btn{pointer-events:none;}

/* ============================================================
   ⭐ v0.447 — THE SOLO VS CARD. Your inkling, a rival rolled on the spot, and a beat to read the
   two names before the burning VS swallows the screen (see inkSoloVs).
   Sized in fractions of the glass so the unfolded screen shows the SAME card BIGGER — the v0.445
   rule — and it never reaches the D-pad because it is centred on the upper two thirds.
   ============================================================ */
.svs{position:fixed; inset:0; z-index:520; display:flex; align-items:center; justify-content:center;
  gap:4vw; background:#1b1510e8; pointer-events:none; animation:svsIn .28s ease-out both;}
@keyframes svsIn{from{opacity:0}to{opacity:1}}
.svs-side{display:flex; flex-direction:column; align-items:center; gap:4px; width:34%; max-width:220px;}
.svs-side.l{animation:svsL .42s var(--pop,cubic-bezier(.2,1.4,.4,1)) both;}
.svs-side.r{animation:svsR .42s var(--pop,cubic-bezier(.2,1.4,.4,1)) both;}
@keyframes svsL{from{transform:translateX(-60vw)}to{transform:translateX(0)}}
@keyframes svsR{from{transform:translateX(60vw)}to{transform:translateX(0)}}
.svs-ink{width:min(30vw,150px); height:min(34vw,168px); display:flex; align-items:flex-end; justify-content:center;}
.svs-ink svg{width:100%; height:100%;}
.svs-nm{font-family:var(--fontD); font-size:11px; letter-spacing:1px; color:var(--cream); text-align:center;
  text-shadow:2px 2px 0 #000; word-break:break-all; line-height:1.3;}
.svs-tag{font-family:var(--fontD); font-size:7px; letter-spacing:2px; color:var(--gold); text-shadow:2px 2px 0 #000;}
.svs-v{font-family:var(--fontD); font-size:34px; color:var(--red); text-shadow:3px 3px 0 #000, 0 0 18px #c93a2288;
  animation:svsV .5s var(--pop,cubic-bezier(.2,1.4,.4,1)) both;}
@keyframes svsV{0%{transform:scale(3); opacity:0}60%{transform:scale(1); opacity:1}100%{transform:scale(1); opacity:1}}
@media (min-width:560px){
  .svs-nm{font-size:16px;} .svs-tag{font-size:10px;} .svs-v{font-size:54px;}
  .svs-ink{width:min(22vw,210px); height:min(25vw,235px);}
}

/* ⭐⭐ v0.448 — THE UNIVERSAL VS INDICATOR SLOT (see vsSlot in js/online.js).
   Third in the standard corner stack: film-strip name, then inkling, then this. Its `top` is set in
   JS by measuring the portrait, so it follows --gs and the fold; everything else lives here. */
.vsind{position:absolute; z-index:10; display:flex; flex-direction:column; align-items:center; gap:3px;
  pointer-events:none; max-width:40vw;}
.vsind.l{left:6px; align-items:flex-start;}
.vsind.r{right:6px; align-items:flex-end;}

/* ============================================================
   ⭐⭐ v0.518 — THE BADGE ALBUM FILLS THE UNFOLDED SCREEN
   ------------------------------------------------------------
   Odaroo's unfolded screenshots: the wall's title, the tab strip and above all the badges
   themselves were still drawn at folded size on a 906px screen — a grid of ~13 miniature medals
   with the words underneath them unreadable. --gs cannot fix this: it is a DEVICE-PIXEL correction
   (see vpNormalise in js/core.js) and comes out at exactly 1 on the Fold, folded or open. The size
   of a thing you LOOK at has to key on the width of the screen, which is what this block does.
   ⚠ Everything below is a plain step-up of the same rules, never a different layout — unfolding
   zooms in, it does not re-shape (the rink learned this the hard way, see the CLAUDE.md note).
   ============================================================ */
/* the header's own type — a class, not an inline style, so a media query can reach it */
.ach-hdnm{font-size:calc(13px * var(--gs,1)); letter-spacing:calc(1px * var(--gs,1));}
.ach-hdjp{opacity:.55; display:block;}

@media (min-width:640px){
  /* ---- the wall's title block ---- */
  .ach-hdnm{font-size:calc(21px * var(--gs,1)); letter-spacing:calc(1.6px * var(--gs,1));}
  .ach-hdjp{font-size:calc(11px * var(--gs,1));}
  .ach-hdmed{width:calc(46px * var(--gs,1)); height:calc(46px * var(--gs,1));}
  .ach-hdrow{gap:calc(14px * var(--gs,1));}
  .ach-bar{height:calc(15px * var(--gs,1));}
  .ach-owed{font-size:calc(11px * var(--gs,1)); padding:calc(6px * var(--gs,1));}

  /* ---- the tab strip ---- */
  .ach-tabn{font-size:calc(13px * var(--gs,1));}
  .ach-tab.ach-long .ach-tabn{font-size:calc(11px * var(--gs,1));}
  .ach-tabjp{font-size:calc(9px * var(--gs,1));}
  .ach-tab{padding:calc(11px * var(--gs,1)) calc(6px * var(--gs,1)) calc(10px * var(--gs,1));}
  .ach-tabdot{min-width:calc(21px * var(--gs,1)); height:calc(21px * var(--gs,1)); font-size:calc(10px * var(--gs,1));}

  /* ---- the game headings down the wall ---- */
  .ach-sec{font-size:calc(15px * var(--gs,1)); margin:calc(18px * var(--gs,1)) 0 calc(9px * var(--gs,1));}
  .ach-sec-ic{width:calc(26px * var(--gs,1)); height:calc(26px * var(--gs,1));}
  .ach-sec-n{font-size:calc(13px * var(--gs,1));}

  /* ---- ⭐ THE BADGES THEMSELVES. 64px -> 108px, and the wall is allowed to grow with the screen
       the way the album does rather than staying pinned to a phone-width column. ---- */
  .ach-grid.tight{grid-template-columns:repeat(auto-fill, minmax(calc(108px * var(--gs,1)), 1fr));
    gap:calc(12px * var(--gs,1)); max-width:min(1180px, 96%);}
  .ach-grid{gap:calc(12px * var(--gs,1)); max-width:min(1180px, 96%);}
  .achtile{padding:calc(7px * var(--gs,1)); border-radius:calc(6px * var(--gs,1));}
  .achtile-pip{font-size:calc(9px * var(--gs,1)); padding:calc(3px * var(--gs,1)) calc(5px * var(--gs,1));}
  .achtile-bar{height:calc(5px * var(--gs,1)); left:calc(7px * var(--gs,1)); right:calc(7px * var(--gs,1));}
  .achtile-owed{width:calc(24px * var(--gs,1)); height:calc(24px * var(--gs,1)); font-size:calc(14px * var(--gs,1));}
  .achmedal{width:calc(74px * var(--gs,1)); height:calc(74px * var(--gs,1));}
  .achmedal.big{width:calc(104px * var(--gs,1)); height:calc(104px * var(--gs,1));}

  /* ---- the scroller gets breathing room instead of a 11px gutter on a 906px screen ---- */
  #achList{padding-left:calc(22px * var(--gs,1)); padding-right:calc(22px * var(--gs,1));
    padding-top:calc(12px * var(--gs,1));}

  /* ---- the rolling showcase on the hub bar, same story ---- */
  .gsb{max-height:calc(88px * var(--gs,1));}
  .gigshow-cnt{font-size:calc(10px * var(--gs,1));}
  .gigshow-cnt b{font-size:calc(9px * var(--gs,1));}
}

/* ============================================================
   ⭐⭐⭐ v0.772 — THE SEALED VAULT DIALOG (Odaroo, 2026-08-09).
   The one message in the game printed as a negative: a black stop sign on white, matching the
   carousel tile it was opened from. Markup from vaultSealed() in js/cards.js.
   ⚠⚠ THE SIGN IS INVERTED, NOT REDRAWN. MICON.brawl() paints its own #06080b plate behind the white
   octagon (every crest does — see cartCrest); `filter:invert(1)` turns that plate white and the sign
   black in one move, so the dialog and the tile can never drift apart. Same reasoning as the tile's
   .vaultinv rule above; do not fork a second, dark copy of the glyph.
   ⚠ The card paints its own white field over the modal's cream — this is the inverse of every other
   dialog in Inkville and it is meant to be. The type is #06080b, i.e. 19:1 on white; crReadable is
   the judge here as everywhere.
   ============================================================ */
.vseal{background:#ffffff; margin:calc(-14px * var(--gs,1)); padding:calc(20px * var(--gs,1)) calc(14px * var(--gs,1)) calc(16px * var(--gs,1)); border:calc(3px * var(--gs,1)) solid #06080b;}
.vseal-sign{width:calc(74px * var(--gs,1)); height:calc(74px * var(--gs,1)); margin:0 auto calc(10px * var(--gs,1)); filter:grayscale(1) invert(1); animation:vsealBreath 2.6s ease-in-out infinite;}
.vseal-sign svg{width:100%; height:100%; display:block;}
@keyframes vsealBreath{0%,100%{scale:1}50%{scale:1.06}}
@media (prefers-reduced-motion:reduce){ .vseal-sign{animation:none;} }
.vseal-h{font-size:calc(15px * var(--gs,1))!important; color:#06080b; letter-spacing:calc(1.5px * var(--gs,1)); margin:0 0 calc(10px * var(--gs,1)); line-height:1.5;}
.vseal-p{font-family:var(--fontD); font-size:calc(8.5px * var(--gs,1)); color:#06080b; line-height:2.1; letter-spacing:calc(.4px * var(--gs,1)); margin:0 calc(4px * var(--gs,1)) calc(12px * var(--gs,1));}
.vseal-n{font-family:var(--fontD); font-size:calc(8px * var(--gs,1)); color:#c93a22; letter-spacing:calc(.6px * var(--gs,1)); margin:0 0 calc(14px * var(--gs,1));}

/* ============================================================
   ⭐⭐ v0.845 — MATCH MAKING, THE THIRD ANIMATED DOOR BUTTON.
   Odaroo: a button at the foot of the online roster, the same SIZE and the same KIND of animation
   as SOLO (.trail) and ONLINE (.sky) on the doorstep, but in rainbow.
   ⚠⚠ IT IS BUILT THE SAME WAY THOSE TWO ARE, AND FOR THE SAME REASON: two inline gradient tiles
   scrolled with `background-position`. That is a PAINT-only property — the v0.832/v0.834 rule.
   A hue-rotate FILTER cycling on a button would re-rasterise it every frame, which is exactly the
   family of fault this project has spent five builds removing. Never animate this with a filter.
   ⚠ TWO LAYERS AT DIFFERENT SPEEDS, like the sky and the trail — one bold band and one paler,
   wider band drifting the other way, so it reads as motion rather than as a sliding stamp.
   ⚠ THE BANDS ARE PASTEL ON PURPOSE. The word sits on top in dark ink, and a saturated rainbow
   under 18px display type fails contrast on the red and blue bands (crReadable catches it).
   ============================================================ */
:root{
  --inkrbw-near:repeating-linear-gradient(66deg,
    #ff9f9f 0 14px, #ffd79a 14px 28px, #fff29a 28px 42px, #a8e6a1 42px 56px,
    #9fd4ff 56px 70px, #c3a8f0 70px 84px, #ffa8d8 84px 98px);
  --inkrbw-far:repeating-linear-gradient(66deg,
    #ffffff66 0 22px, #ffffff00 22px 44px);
}
@keyframes inkRbwDrift{ from{ background-position:0 0, 0 0; } to{ background-position:196px 0, -88px 0; } }
.btn.opt.rainbow{
  background-color:#ffe9c8;
  color:#1b1510; border-color:#1b1510;
  background-image:var(--inkrbw-far), var(--inkrbw-near);
  background-repeat:repeat, repeat;
  background-size:auto, auto;
  background-position:0 0, 0 0;
  animation:inkRbwDrift 9s linear infinite;
}
.btn.opt.rainbow span{position:relative; z-index:1; color:#1b1510;}   /* the word rides in front of the colours */
.btn.opt.rainbow:active{filter:brightness(.92);}
.btn.opt.rainbow.soon{animation:none; background-image:none; opacity:.85;}
@media (prefers-reduced-motion:reduce){ .btn.opt.rainbow{animation:none;} }

/* ⭐ the room's own copy: full width under the roster, the same height as the doorstep's buttons.
   ⚠⚠ `white-space:nowrap` IS LOAD-BEARING, NOT TIDINESS. "MATCH MAKING" is two words, and on the
   375px pop-up it wrapped onto a second line — a 72px button beside a 49px SOLO, i.e. NOT the same
   size, which is the one thing Odaroo asked for by name. It stays one line and the padding gives
   way instead. Measured 49px at every fold size. */
/* ⚠⚠ THE SELECTOR CARRIES `#modalBox` ON PURPOSE. `#modalBox .btn.opt{width:62%; min-height:46px}`
   (css/home.css) is an ID rule, so a plain `.crlPop .crlPop-mm` lost to it: the button came out
   164px wide inside a 335px window and "MATCHMAKING" was squeezed onto two lines. Do not lower it.

   ⭐⭐ v0.846 — AND IT IS A ROSTER ROW NOW, NOT A BUTTON. Odaroo: "same box length, same height —
   it's just another row in the list." So every number here is `.crl-row`'s own (css/crown.css):
   min-height 38, the pop-up's 6px/8px padding, a 2px ink border, and the same 13px name type
   stepped down to 12.5 so one word sits comfortably in a 38px row.
   ⚠⚠ THE SKEW AND THE DROP SHADOW COME OFF. Every `.btn` in this game is `skewX(-6deg)` with a
      4px offset shadow, and a leaning, floating row in a column of square flat ones does not read
      as part of the list — which is the whole of what was asked for. The span's counter-skew has
      to come off with it, or the word leans the other way inside a square box.
   ⚠ `white-space:nowrap` stays: the word never breaks, the padding gives way first. */
#modalBox .crlPop .crlPop-mm{
  display:flex; align-items:center; justify-content:center;
  width:100%; min-width:0; max-width:100%; box-sizing:border-box;
  margin:6px 0 0 !important;
  /* ⚠⚠ 46px, NOT the 38px `min-height` .crl-row declares. A player row is TALLER than its own
     minimum because of what is inside it: the 34px .lookchip, plus 6px of padding and a 2px border
     on each side = 46. Copying the declared 38 gave a row visibly shorter than the ones above it.
     The number is written down here on purpose and tools/test-matchmaking-btn.mjs measures the two
     against each other, so if the chip ever changes size this fails the build instead of drifting. */
  min-height:46px; padding:6px 8px; border:2px solid #0a1020;
  transform:none; box-shadow:none;
  font-size:12.5px; letter-spacing:1px; white-space:nowrap;
}
#modalBox .crlPop .crlPop-mm>span{transform:none;}
/* the row presses like the player rows do, not like a doorstep button */
#modalBox .crlPop .crlPop-mm:hover{transform:none; background-color:#ffe9c8;}
#modalBox .crlPop .crlPop-mm:active{transform:scale(.985); filter:brightness(1.12);}

/* ============================================================
   ⭐⭐⭐ v0.846 — THE MATCHMAKING ROULETTE.
   One slot, centred over the dimmed room, flicking through random inklings and taken-looking names
   and slowing to a stop on the person you drew (crMatchRoulette, js/online.js).
   ⚠⚠ THE FLICK IS A `transform`, AND THE SLOT OWNS transform ALONE. Every other candidate here is
      a LAYOUT property — `top`, `height`, `margin` — and this element changes twelve to twenty
      times in two seconds, so a layout property in motion would re-solve the page on every one of
      those frames (the v0.832/v0.834 rule).
   ⚠ THE WINDOW HAS A FIXED HEIGHT AND THE NAME A FIXED LINE BOX. A slot sized by its contents
      would jump every time a 5-letter name was followed by a 14-letter one — the v0.447 family of
      fault, where a position depends on what the words say.
   ============================================================ */
.crmm-wrap{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  background:#0a1020cc; z-index:40; padding:12px;}
.crmm{background:#f2ead6; border:3px solid var(--ink); box-shadow:0 4px 0 var(--ink);
  padding:12px 14px; text-align:center; max-width:100%;}
.crmm-h{font-family:var(--fontD); font-size:12px; letter-spacing:1px; color:#10243f;
  margin-bottom:9px; white-space:nowrap;}
.crmm-win{height:46px; width:min(240px,72vw); overflow:hidden; background:#ffffff;
  border:2px solid var(--ink); display:flex; align-items:center; justify-content:center;}
.crmm-slot{display:flex; align-items:center; justify-content:center; gap:8px; width:100%;
  padding:0 8px; box-sizing:border-box;}
.crmm-slot .lookchip{width:30px; height:30px;}
.crmm-nm{font-family:var(--fontD); font-size:12.5px; letter-spacing:1px; color:#1b1510;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; line-height:20px;}
/* the flick: the new face arrives from below, the way a reel drum turns over */
@keyframes crmmTick{ from{ transform:translateY(20px); opacity:.25; } to{ transform:translateY(0); opacity:1; } }
.crmm-slot.tick{animation:crmmTick .085s linear;}
/* and it settles with a bump when the wheel stops */
@keyframes crmmWon{ 0%{transform:scale(1)} 40%{transform:scale(1.13)} 100%{transform:scale(1)} }
.crmm-slot.won{animation:crmmWon .34s var(--pop,ease-out);}
.crmm-slot.won .crmm-nm{color:#1f6b45;}
@media (prefers-reduced-motion:reduce){ .crmm-slot.tick,.crmm-slot.won{animation:none;} }
