/* =========================================================
   STANDARD GAME SHELL — shell.css
   Shared + BYTE-IDENTICAL in every game. Never fork this to recolour a game —
   per-game colours live in that game's theme.css, which loads AFTER this file
   and overrides the :root variables below.

   ---- WHERE THIS CAME FROM ----
   This is the Overtake 2 UI, carbon-copied: the same theme tokens, the same
   button ramps, the same top bar, garage, bottom nav, result popups, shop,
   out-of-lives, league board and more-games grid. It was ported HERE rather than
   into one game so that every game inherits it — which is the whole point of
   having a shell at all.

   ---- THE SCALING PATTERN (read UI Kit/UI-SCALING-STANDARD.md) ----
   Two layers, handled completely differently:
     • the 3D world  → the TRUE full viewport, never capped, never scaled.
     • all 2D UI     → a fixed 450×800 "design card" scaled by ONE JS transform.
   🚨 Nothing inside a card may use vw/vh/%-of-viewport/clamp()-against-the-viewport.
   The card is scaled as a whole; anything that also reads the real window gets
   scaled TWICE. If you need "half the card's height", that is 400px, not 50vh.
   ========================================================= */

/* Single game font — Fredoka One, bundled locally. It used to come from Google Fonts,
   which is third-party content and an automatic the host portal rejection
   (Standards/the QA checklist §4). Do not put it back. */
@font-face { font-family:'Fredoka One'; src:url('./fonts/FredokaOne-Regular.ttf') format('truetype');
  font-weight:400 900; font-display:swap; }

/* ═══ THEME ═══════════════════════════════════════════════════════════════════
   Every colour in the UI comes from here, so a game re-skins by overriding these
   in its own theme.css and nothing else.

   Two tokens are SEMANTIC and must NOT be re-pointed by a theme:
     --gold  always means CURRENCY.
     --green always means "this button plays a REWARDED AD".
   A player who has met one of our games already knows what a green button does,
   and rewarded video is where the money comes from. A game that makes green mean
   something else is training them wrong and will cost itself views. */
:root{
  --accent:#b6f23a; --accent-dk:#8fc71f; --accent-lt:#d0ff70; --accent-base:#6d9a12;
  --green:#2fbf6b;  --green-dk:#1d8c4c;                       /* rewarded ads — semantic */
  --gold:#ffce3a;   --gold-dk:#e8a417;                        /* currency / xp — semantic */
  --ink:#0d1016;                                              /* the outline on everything */
  /* ── OUTLINE WIDTHS — Astrocade/_ui/UI-STYLE.md ──────────────────────────────
     The spec is 2.2px on buttons, panels and title tabs; 1.65px on smaller chrome
     (badges, pips, tiles, rows). The shell had these hardcoded at 4px / 3px / 2px,
     which reads noticeably heavier than the rest of the studio's work.
     Change them HERE, not at the call sites — that is the point of having them. */
  --ink-w:2.2px;    /* buttons, panels, title tabs */
  --ink-w2:1.65px;  /* badges, pips, tiles, list rows */
  --panel:#232833;  --panel-2:#1a1e27; --tab:#39414f; --text:#e8edf5;
  --nav1:#1b2029;   --nav2:#12161d; --nav-line:#0a0d12;

  /* Button gradient ramps. These used to be hardcoded hexes on .btn.primary/.green/.gold,
     which meant buttons silently ignored the theme — the single biggest gap when reskinning.
     1 = top of the face, 2 = bottom of the face, 3 = the hard shadow underneath, ink = label.
     Change 1/2/3 together or the button stops reading as a physical key you can press. */
  --pri-1:#c8f95e; --pri-2:#a5e02b; --pri-3:#6d9a12; --pri-ink:#16200a;
  --go-1:#5ee08d;  --go-2:#2fbf6b;  --go-3:#1d8c4c;  --go-ink:#08240f;
  --gd-1:#ffca4d;  --gd-2:#f0aa22;  --gd-3:#cf8d0b;  --gd-ink:#3b2f06;
  --gh-1:#2c3242;  --gh-2:#232833;  --gh-3:#141821;
  --ava-1:#2c3242; --ava-2:#1a1e27;

  /* Text drawn straight over the 3D scene rather than on a panel. It must stay legible
     against the game whatever the panels are doing, so it does NOT follow --text. */
  --text-scene:#eef2f8;
}

/* Kill mobile-web tap/hold cruft: no grey tap-flash, no long-press callout/selection
   ("trying to highlight the screen"), no image drag. This is what makes buttons feel
   like native taps instead of hollow. */
/* ⚠️ box-sizing:border-box is LOAD-BEARING, not tidiness. Overtake sets it globally
   (`* { margin:0; padding:0; box-sizing:border-box }`) and the original port kept only the
   tap-cruft half of that rule — so every padded, width:100% element in the shell came out
   24px WIDER than the 450px card. Real symptom: the coin/gem chips and the right edge of the
   bottom nav ran off the screen on a 375px-wide phone, which is a the host portal §3 rejection
   ("no gameplay-relevant element may be cut off at any ratio").
   Scoped to the shell's own subtrees rather than a bare `*`, because shell.css loads
   alongside each game's own DOM and must never silently restyle it. */
#gameShell, .col, .sh-ov, #shNav, #shSplash, #shTut,
#gameShell *, .col *, .sh-ov *, #shNav *, #shSplash *, #shTut *{ box-sizing:border-box; }
#gameShell *, .col *, .sh-ov *{ -webkit-tap-highlight-color:transparent; -webkit-touch-callout:none;
  -webkit-user-select:none; user-select:none; }
#gameShell img, .col img, .sh-ov img{ -webkit-user-drag:none; pointer-events:none; }
#gameShell button, .sh-ov button{ -webkit-tap-highlight-color:transparent; touch-action:manipulation; }

/* ═══ THE DESIGN CARD ═════════════════════════════════════════════════════════
   #gameShell and any game-side `.col` are the same fixed 450×800 box, scaled and
   centred by shell.js's layoutUI(). Author everything inside in plain px. */
#gameShell{ position:fixed; top:0; left:0; width:450px; height:800px;
  transform-origin:top left; z-index:10; pointer-events:none;
  font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; color:var(--text); }
#gameShell button{ font-family:inherit; }
.col{ position:fixed; top:0; left:0; width:450px; height:800px;
  transform-origin:top left; pointer-events:none; }
.col > *{ pointer-events:auto; }
/* The overlays and the nav are deliberately SIBLINGS of the card, not children — so this
   has to cover them too. Scoping `.hidden` to `#gameShell` alone silently broke every
   show/hide inside a popup: the store showed its offers AND its "no offers" message at
   once, and the rewarded buttons could never be hidden on a device without ads. */
#gameShell .hidden, .col .hidden, .sh-ov .hidden, #shAd .hidden{ display:none !important; }

/* ═══ BUTTONS ═════════════════════════════════════════════════════════════════
   Flat/matte key with a hard offset shadow — it should read as a physical thing you
   press, which is why :active moves it DOWN into its own shadow. */
#gameShell .btn, .sh-ov .btn{ position:relative; cursor:pointer; font-weight:800; color:#fff;
  padding:0 22px; border-radius:19px; --c1:#ffffff; --c2:#e9eef2; --base:#c3ccd4;
  border:var(--ink-w) solid var(--ink);
  background:linear-gradient(180deg,var(--c1) 0 44%,var(--c2) 56% 100%);
  box-shadow:0 5px 0 var(--base),0 8px 9px rgba(0,0,0,.18);
  text-shadow:0 2px 0 rgba(0,0,0,.16); transition:transform .07s,box-shadow .07s; }
#gameShell .btn:active, .sh-ov .btn:active{ transform:translateY(4px);
  box-shadow:0 1px 0 var(--base),0 3px 5px rgba(0,0,0,.18); }
#gameShell .btn.primary, .sh-ov .btn.primary{ --c1:var(--pri-1); --c2:var(--pri-2); --base:var(--pri-3); color:var(--pri-ink); }
#gameShell .btn.green,   .sh-ov .btn.green  { --c1:var(--go-1);  --c2:var(--go-2);  --base:var(--go-3);  color:var(--go-ink); text-shadow:none; }
#gameShell .btn.gold,    .sh-ov .btn.gold   { --c1:var(--gd-1);  --c2:var(--gd-2);  --base:var(--gd-3);  color:var(--gd-ink); text-shadow:none; }
#gameShell .btn.ghost,   .sh-ov .btn.ghost  { --c1:var(--gh-1);  --c2:var(--gh-2);  --base:var(--gh-3);  color:var(--text);   text-shadow:none; }
#gameShell .btn.circ, .sh-ov .btn.circ{ border-radius:50%; padding:0; }

/* ═══ TOP BAR ═════════════════════════════════════════════════════════════════
   A child of the card, NOT independently fixed/vw-clamped: once the card has its own
   scale transform a fixed-position child would be contained by it, but vw/vh would
   still read the REAL window rather than the card. So this is a plain absolute box
   sized to 100% of its actual parent. */
#gameShell .top{ position:absolute; top:0; left:0; right:0; z-index:4; width:100%;
  display:flex; align-items:center; gap:8px; padding:30px 12px 4px; pointer-events:none; }
#gameShell .top > *{ pointer-events:auto; }
#gameShell .profile{ flex:1; min-width:0; display:flex; align-items:center; gap:9px;
  background:var(--panel); border:var(--ink-w) solid var(--ink); border-radius:16px;
  padding:5px 12px 5px 5px; box-shadow:0 3px 0 #0002; }
#gameShell .ava{ width:40px; height:40px; border-radius:12px; flex:none; border:var(--ink-w) solid var(--ink);
  background:var(--panel); display:grid; place-items:center; overflow:hidden; }
#gameShell .ava img{ width:92%; height:92%; object-fit:contain; }
#gameShell .pmeta{ flex:1; min-width:0; }
#gameShell .pname{ font-size:12px; font-weight:800; color:var(--text); line-height:1.1;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
#gameShell .xp{ position:relative; height:11px; border-radius:999px; background:var(--panel-2);
  border:var(--ink-w2) solid var(--ink); margin-top:3px; overflow:hidden; }
#gameShell .xp > i{ position:absolute; left:0; top:0; bottom:0; width:0; border-radius:999px;
  background:linear-gradient(var(--gold),var(--gold-dk)); transition:width .3s; }
#gameShell .chip{ display:flex; align-items:center; gap:4px; background:var(--panel);
  border:var(--ink-w) solid var(--ink); border-radius:14px; padding:2px 9px 2px 4px;
  font-size:13px; font-weight:800; color:var(--text); box-shadow:0 3px 0 #0002; }
#gameShell .chip img{ width:22px; height:22px; object-fit:contain; }

/* Mission pill — the profile slot doubles as the daily objective tracker. */
#gameShell .mrow{ display:flex; align-items:center; gap:6px; margin-top:3px; }
#gameShell .mrow .xp{ flex:1; margin-top:0; }
#gameShell .mrew{ display:flex; align-items:center; gap:1px; flex:none;
  font-weight:800; font-size:12px; color:var(--text); }
#gameShell .mrew img{ width:17px; height:17px; object-fit:contain; }
/* Mostly still, then a quick attention wiggle every couple of seconds — draws the eye to
   COLLECT without being distracting for the whole time the mission sits complete. */
#gameShell .profile.done{ animation:sh-pillShake 2.2s ease-in-out infinite; }
@keyframes sh-pillShake{ 0%,82%,100%{transform:translateX(0) rotate(0)}
  85%{transform:translateX(-3px) rotate(-1.2deg)} 89%{transform:translateX(3px) rotate(1.2deg)}
  93%{transform:translateX(-2px) rotate(-.6deg)}  96%{transform:translateX(2px) rotate(.6deg)} }
#gameShell .collect{ display:flex; align-items:center; justify-content:center; gap:3px; margin-top:3px;
  width:100%; border:var(--ink-w2) solid var(--ink); border-radius:11px; padding:3px 8px; cursor:pointer;
  background:linear-gradient(var(--go-1),var(--go-2)); color:var(--go-ink);
  font-weight:800; font-size:12px; box-shadow:0 2px 0 var(--go-3);
  animation:sh-collectPulse 1.1s ease-in-out infinite; }
#gameShell .collect img{ width:15px; height:15px; object-fit:contain; }
#gameShell .collect:active{ transform:translateY(2px); box-shadow:0 0 0 var(--go-3); }
@keyframes sh-collectPulse{ 0%,100%{transform:scale(1)} 50%{transform:scale(1.05)} }

#gameShell .mtoast{ position:absolute; top:96px; left:50%; transform:translateX(-50%) translateY(-12px);
  background:linear-gradient(var(--accent),var(--accent-dk)); color:var(--pri-ink); font-weight:800;
  font-size:14px; border:var(--ink-w) solid var(--ink); border-radius:14px; padding:8px 18px; z-index:6;
  opacity:0; pointer-events:none; box-shadow:0 5px 0 var(--accent-dk);
  transition:opacity .25s,transform .25s; white-space:nowrap; }
#gameShell .mtoast.show{ opacity:1; transform:translateX(-50%) translateY(0); }

/* ═══ SIDE RAIL ═══════════════════════════════════════════════════════════════ */
#gameShell .rail{ position:absolute; top:150px; display:flex; flex-direction:column; gap:16px; z-index:3; }
#gameShell .rail.l{ left:10px; } #gameShell .rail.r{ right:10px; }
#gameShell .round{ position:relative; width:60px; height:60px; padding:0; background:none; border:0;
  cursor:pointer; display:grid; place-items:center; pointer-events:auto;
  filter:drop-shadow(0 3px 3px rgba(0,0,0,.35)); }
#gameShell .round img{ width:100%; height:100%; object-fit:contain; }
#gameShell .round .cap{ position:absolute; bottom:-7px; left:50%; transform:translateX(-50%);
  background:var(--accent-dk); color:var(--pri-ink); font-size:9px; font-weight:800;
  padding:1px 7px; border-radius:999px; white-space:nowrap; }
#gameShell .round.big{ width:74px; height:74px; }

/* ═══ THE HOME SCREEN / GARAGE ════════════════════════════════════════════════ */
#gameShell .screen{ position:absolute; inset:0; display:none; }
#gameShell .screen.on{ display:block; }
#gameShell .gTitle{ position:absolute; top:92px; left:0; right:0; text-align:center;
  font-size:40px; font-weight:800; letter-spacing:1px; color:var(--text-scene);
  -webkit-text-stroke:5px var(--ink); paint-order:stroke fill;
  text-shadow:0 4px 0 rgba(0,0,0,.18); pointer-events:none; }
#gameShell .gName{ position:absolute; top:168px; left:0; right:0; text-align:center;
  color:var(--text-scene); font-size:27px; font-weight:800; letter-spacing:1px;
  -webkit-text-stroke:4px var(--ink); paint-order:stroke fill; pointer-events:none; }
/* The equipped item's perk, under its name — "+5 <trophy> +1 <gem>". Gold, because it
   names a reward. Icons are sized to the text so the line reads as one phrase. */
#gameShell .gPerk{ position:absolute; top:198px; left:0; right:0; text-align:center;
  color:var(--gold); font-size:16px; font-weight:800; letter-spacing:.4px;
  -webkit-text-stroke:3.5px var(--ink); paint-order:stroke fill; pointer-events:none; }
#gameShell .gPerk img{ width:1.15em; height:1.15em; object-fit:contain;
  vertical-align:-0.22em; margin:0 1px 0 3px; -webkit-text-stroke:0;
  filter:drop-shadow(0 2px 0 rgba(0,0,0,.35)); }
#gameShell .gStatus{ position:absolute; top:206px; left:0; right:0; text-align:center;
  color:var(--text-scene); font-size:15px; font-weight:800; letter-spacing:1px;
  text-shadow:0 2px 5px rgba(0,0,0,.5); pointer-events:none; }
#gameShell .gStatus img{ height:1.05em; width:auto; vertical-align:-.19em; margin:0 2px 0 3px; }

#gameShell .garrow{ position:absolute; top:44%; transform:translateY(-50%); width:58px; height:58px;
  border-radius:50%; font-size:36px; font-weight:800;
  background:linear-gradient(180deg,var(--pri-1) 0 45%,var(--pri-2) 55% 100%); color:var(--pri-ink);
  border:var(--ink-w) solid var(--ink); cursor:pointer; pointer-events:auto;
  display:flex; align-items:center; justify-content:center; line-height:1; padding-bottom:4px;
  box-shadow:0 4px 0 var(--pri-3),0 6px 9px rgba(0,0,0,.3); }
#gameShell .garrow.prev{ left:14px; } #gameShell .garrow.next{ right:14px; }

/* ═══ THE TOY SHOP — garage.shop:true ═════════════════════════════════════════
   It is a normal .sh-ov panel, so it already has the outline, the radius, the title tab,
   the pop-in and the close button. Everything below is the ONE difference: it sits at the
   bottom with no scrim, so the top of the screen stays clear and the game's own 3D model
   is the hero above it.

   The first version was a bare screen with the title, grid and button floating loose on
   the staircase. It read as a different game — every other surface in this shell is an
   outlined card with a title tab, and that one shared none of it. Reusing the component
   is the fix, and it also means the shop cannot drift from the rest later. */
.sh-ov.sheet{ align-items:flex-end; padding-bottom:88px; }
/* 🚨 NO SCRIM AT ALL — the white-out is done in 3D, not here.
   A CSS scrim is painted OVER the canvas, so it dims the hero model along with everything
   else and the toy ends up behind the wash looking dead. The game drops a white plane
   parented to the camera, just beyond the toy, so the staircase disappears and the model
   stands in FRONT of the white-out, fully lit. See `shopBg` in the game.
   The home screen behind is hidden by .shopping below instead of being dimmed. */
.sh-ov.sheet.show{ background:rgba(20,14,8,0); }
/* The home screen still has its own tile strip and PLAY button, and with no scrim they
   were plainly visible around the panel — two selectors on screen at once. Hidden rather
   than dimmed, because a dimmed duplicate is still a duplicate. */
#gameShell.shopping .screen{ opacity:0; pointer-events:none; transition:opacity .18s; }
/* Grow upward from the bottom edge rather than from the middle, or the pop-in animation
   slides the panel off the bottom of the screen on the way in. */
.sh-ov.sheet .panel{ transform-origin:50% 100%; }

.sh-ov .tsName{ font-size:22px; font-weight:800; letter-spacing:.04em; color:var(--text); }
.sh-ov .tsProg{ width:100%; display:flex; align-items:center; gap:10px; margin-top:-4px; }
.sh-ov .tsBar{ flex:1; height:14px; border-radius:999px; background:var(--gh-3);
  border:var(--ink-w2) solid var(--ink); overflow:hidden; }
.sh-ov .tsBar i{ display:block; height:100%; border-radius:999px;
  background:linear-gradient(var(--gd-1),var(--gd-2)); }
.sh-ov .tsProg span{ font-size:13px; font-weight:800; letter-spacing:.04em;
  color:var(--text); opacity:.7; white-space:nowrap; }

/* The grid scrolls inside the panel so the panel never grows past the screen, however
   many items a game has. Four across; the panel is a fixed 340px design-space width. */
/* 🚨 minmax(0,1fr), NOT 1fr. A 1fr track has an automatic MINIMUM of min-content, and the
   tile contains a 128px-natural thumbnail — so the first track blew out to 128px and the
   other three were crushed to slivers. minmax(0,…) lets a track be narrower than its
   content, which is what makes the image scale down instead of setting the column width. */
.sh-ov.sheet .panel{ width:376px; }
.sh-ov .tsGrid{ width:100%; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:9px;
  /* 🚨 EXPLICIT ROW HEIGHT — `aspect-ratio:1` on the tile is NOT enough. With auto-sized
     implicit rows the row collapsed to its content (~29px) while the tile still painted
     its full square, so every row overlapped the one above and the grid rendered as a
     stack of nested rounded rectangles. 100px: 376 panel - 36 padding - 4 border/pad
     - 2 gaps of 9 = 318 / 3 = 106, rounded down so the row is never taller than its cell. */
  grid-auto-rows:100px; align-content:start;
  /* Four rows visible, the fifth peeking so the grid reads as scrollable. */
  max-height:344px; overflow-y:auto; overflow-x:hidden; padding:2px;
  scrollbar-width:none; }
.sh-ov .tsGrid::-webkit-scrollbar{ display:none; }
/* Only the SIZE — everything else comes from the shared .gTile rules above. */
.sh-ov .tsGrid .gTile{ width:auto; min-width:0; height:100px; border-radius:18px;
  box-shadow:0 3px 0 rgba(0,0,0,.18); }
.sh-ov .tsGrid .gTile .pr{ font-size:12px; padding:2px 0 3px; }
.sh-ov .tsGrid .gTile .pr img{ width:11px; height:11px; }

/* SPIN — two ways to draw a random locked item, side by side and equal weight. */
.sh-ov .tsSpin{ width:100%; display:flex; gap:9px; }
.sh-ov .tsSpin .btn{ flex:1; font-size:16px; padding:11px 6px; }
.sh-ov .tsSpin .btn.dim{ filter:grayscale(.55) brightness(.9); }
.sh-ov .tsSpin .btn img{ width:19px; height:19px; object-fit:contain; vertical-align:-4px;
  margin-right:5px; }
/* The tile the spinner is rattling through. Deliberately NOT the .sel treatment — the
   player must never mistake the rattle for "this one is yours" before it lands. */
.gTile.spin{ border-color:var(--gd-2);
  box-shadow:0 0 0 3px rgba(255,224,77,.55), 0 3px 0 rgba(0,0,0,.18); }

/* ═══ THE ITEM STRIP — garage.strip:true ══════════════════════════════════════
   A scrollable row of every item, sitting directly above PLAY. Replaces the prev/next
   arrows: arrows tell the player there is one more thing, the strip tells them how many
   there are and what they cost, without opening anything.

   ⚠️ NO vw/vh ANYWHERE IN HERE. This lives inside the 450x800 design card, which is
   already scaled by one JS transform — viewport units inside the card get scaled twice.
   See UI-SCALING-STANDARD.md. */
#gameShell .gStrip{ position:absolute; left:0; right:0; bottom:150px; z-index:3;
  display:flex; gap:8px; align-items:center; padding:4px 18px 6px;
  overflow-x:auto; overflow-y:hidden; pointer-events:auto;
  -webkit-overflow-scrolling:touch; scrollbar-width:none; }
/* 🚨 NO `scroll-behavior:smooth` HERE. With it set in CSS, every programmatic scroll —
   including the one that centres the selected tile on open — is forced through the smooth
   path and the strip simply never moved: scrollLeft read 0 before and after. Smoothness is
   decided per call in scrollStripToPicked(), which wants it on a tap and OFF on first
   paint (a smooth scroll on open animates from zero every time the menu is shown, which
   reads as the strip lurching). */
/* A visible scrollbar on a 60px-tall row is thicker than the thing it scrolls. */
#gameShell .gStrip::-webkit-scrollbar{ display:none; }

/* 🚨 THE TILE IS DELIBERATELY NOT SCOPED TO #gameShell. It is used in two places — the
   home strip (inside the card) and the toy shop panel (an .sh-ov overlay, OUTSIDE the
   card). While this was `#gameShell .gTile` the shop's tiles lost `position:relative`, so
   every price pill escaped to the nearest positioned ancestor and stacked up at the bottom
   of the panel; they also lost `overflow:hidden`, so nothing was clipped to the rounding.
   Only the SIZE differs between the two, so only the size is scoped. */
.gTile{ position:relative; flex:0 0 auto; width:60px; height:60px;
  border-radius:14px; background:var(--panel); border:var(--ink-w) solid var(--ink);
  padding:0; cursor:pointer; overflow:hidden;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 3px 0 rgba(0,0,0,.22); }
.gTile img.th{ width:80%; height:80%; object-fit:contain; pointer-events:none; }

/* LOCKED — the same picture, blacked out. One image serves both states: no second render,
   no second cache, and the silhouette is what makes a row of locked tiles pull. */
.gTile.lock{ background:var(--gh-3); }
.gTile.lock img.th{ filter:brightness(0) opacity(.42); }
/* The picture shifts up to make room for the price, but ONLY on locked tiles — an owned
   tile has nothing under it and should use the whole square. */
.gTile.lock img.th{ transform:translateY(-5px); }
.gTile .pr{ position:absolute; left:0; right:0; bottom:0; font-style:normal;
  font-size:10px; font-weight:800; letter-spacing:.2px; color:var(--gold);
  background:rgba(0,0,0,.42); padding:1px 0 2px;
  display:flex; align-items:center; justify-content:center; gap:2px; }
.gTile .pr img{ width:9px; height:9px; object-fit:contain; }

/* SELECTED — brighter surface and a primary rim. It is scaled UP rather than the others
   being dimmed: dimming 23 tiles to highlight one makes the collection look switched off,
   which is the opposite of what the strip is for. */
.gTile.sel{ background:var(--tab); border-color:var(--pri-1);
  box-shadow:0 0 0 2px rgba(255,255,255,.18), 0 3px 0 rgba(0,0,0,.22); }
/* The scale-up is STRIP ONLY. In the shop's grid a scaled cell overlaps its neighbours,
   because grid cells have no spare room either side the way a scrolling row does. */
#gameShell .gStrip .gTile.sel{ transform:scale(1.08); }

/* `bottom` is the FALLBACK only. renderUpgrades() overwrites it with a value measured from
   the actual upgrade stack, because the stack grows upward from bottom:88px and a hard 262px
   here only clears three rows — a fourth upgrade slid straight under the PLAY button. */
#gameShell .gPlay{ position:absolute; bottom:262px; left:50%; transform:translateX(-50%); z-index:3;
  display:flex; flex-direction:column; align-items:center; gap:10px; width:300px; pointer-events:auto; }
#gameShell .gPlay .btn{ width:100%; }

/* ── NO UPGRADE STRIP: bring the whole play cluster down to the nav ──────────────
   Set by liftPlayAbove() when a game has no upgrades. Without an upgrade strip to
   fill it, the bottom ~170px of the card was dead space and PLAY floated mid-screen,
   out of thumb reach, with the item selector stranded further up still.
   Here the selector (name, status, prev/next arrows) drops down to sit directly above
   the button, so picking an item and starting a run are one motion in one place.
   Games WITH upgrades are untouched — the arrows stay at 44%, flanking the item. */
#gameShell.noupg .gName  { top:auto; bottom:196px; }
#gameShell.noupg .gPerk  { top:auto; bottom:174px; }
#gameShell.noupg .gStatus{ top:auto; bottom:154px; }
#gameShell.noupg .garrow { top:auto; bottom:158px; transform:none; }
/* layoutGarageLabels() overwrites this with a measured value — see the note there. */
#gameShell.noupg .gStrip { bottom:150px; }
/* Gentle idle "breathe" so the menu never reads as a static screenshot. Overridden by
   .shake (higher specificity) for the one-shot "can't afford this" feedback. */
/* playButton:false — the level as plain text over the scene, with the drag prompt under it.
   Drawn like the game title (scene text + ink stroke), NOT as a panel: it is a label, not a
   control, and giving it a button's chrome would invite a tap that does nothing. */
#gameShell .gLvl{ color:var(--text-scene); font-size:38px; font-weight:800; letter-spacing:2px;
  -webkit-text-stroke:5px var(--ink); paint-order:stroke fill;
  text-shadow:0 4px 0 rgba(0,0,0,.18); pointer-events:none; }
#gameShell .gDrag{ color:var(--text-scene); font-size:16px; font-weight:800; letter-spacing:2px;
  -webkit-text-stroke:3px var(--ink); paint-order:stroke fill; margin-top:-2px;
  pointer-events:none; animation:sh-pulse 1.1s ease-in-out infinite; }

#gameShell .gGo{ height:64px; font-size:25px; letter-spacing:2px;
  animation:sh-breathe 2.6s ease-in-out infinite; }
#gameShell .gAlt{ height:48px; font-size:16px; letter-spacing:3px; display:flex;
  align-items:center; justify-content:center; gap:8px; }
#gameShell .gAlt img{ width:20px; height:20px; object-fit:contain; }
#gameShell .gGo.shake{ animation:sh-shake .4s; }
@keyframes sh-shake{ 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-8px)} 40%,80%{transform:translateX(8px)} }
@keyframes sh-breathe{ 0%,100%{transform:scale(1)} 50%{transform:scale(1.035)} }

/* The home-screen logo breathes, on a slower and shallower cycle than the play button so
   the two never look like they are pulsing in time — that reads as a glitch. Offset start
   so they drift apart rather than syncing up. */
#gameShell .gTitle.has-mark img{
  animation:sh-logoBreathe 4.2s ease-in-out -1.4s infinite;
  transform-origin:center bottom; will-change:transform; }
@keyframes sh-logoBreathe{
  0%,100%{ transform:scale(1)    rotate(0deg); }
  50%    { transform:scale(1.022) rotate(-0.35deg); }
}

#gameShell .gPaint{ position:absolute; bottom:396px; left:72%; transform:translateX(-50%);
  pointer-events:auto; width:50px; height:50px; border-radius:14px; border:var(--ink-w) solid var(--ink);
  padding:0; background:linear-gradient(180deg,var(--pri-1) 0 45%,var(--pri-2) 55% 100%);
  cursor:pointer; display:grid; place-items:center; box-shadow:0 3px 0 var(--pri-3); }
#gameShell .gPaint img{ width:62%; height:62%; object-fit:contain; }
#gameShell .gPaint:active{ transform:translateX(-50%) translateY(2px); box-shadow:0 1px 0 #0002; }

/* ═══ BOTTOM NAV ══════════════════════════════════════════════════════════════
   Pinned to the TRUE viewport bottom, independent of the card's own centring — this is
   the one element that should hug the real screen edge like a native dock. It is
   deliberately NOT a descendant of the card: a position:fixed element is contained by
   the nearest ancestor with a transform, so nested inside the card it could never
   escape to the true viewport.
   z-index 25 = ABOVE the .sh-ov scrim (20), so the nav stays tappable while a popup is
   open and the player can move Store↔Home↔Cup without closing first. */
#shNav{ position:fixed; left:0; bottom:0; width:450px; transform-origin:bottom left; z-index:25;
  pointer-events:auto; display:flex; justify-content:space-around; align-items:center;
  background:linear-gradient(180deg,var(--nav1),var(--nav2));
  border:3px solid var(--nav-line); border-bottom:0; border-radius:24px 24px 0 0;
  padding:9px 8px 10px; box-shadow:inset 0 3px 0 rgba(255,255,255,.20);
  font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; }
#shNav.hidden{ display:none !important; }
#shNav .tabb{ position:relative; flex:1; max-width:80px; height:48px; background:none; border:0;
  padding:0; cursor:pointer; font-family:inherit; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:1px; transition:transform .15s; }
#shNav .tabb img{ width:44px; height:44px; object-fit:contain;
  filter:drop-shadow(0 2px 2px rgba(0,0,0,.3)); transition:.15s; pointer-events:none; }
#shNav .tabb b{ height:0; opacity:0; font-size:11px; font-weight:800; color:#fff;
  text-shadow:0 1px 1px rgba(0,0,0,.35); transition:.15s; }
#shNav .tabb.on{ transform:translateY(-6px); }
/* Active-tab bump: rounded top, bottom runs off-screen. Uses the ACTION ramp so the selected
   tab matches the play button — pressable things share one colour across the whole UI. */
#shNav .tabb.on::before{ content:''; position:absolute; z-index:-1; left:50%; transform:translateX(-50%);
  top:-10px; width:88px; height:92px; border-radius:20px 20px 0 0;
  background:linear-gradient(180deg,var(--pri-1) 0%,var(--pri-2) 55%,var(--pri-3) 100%);
  border:3px solid var(--nav-line); border-bottom:0; }
#shNav .tabb.on img{ width:46px; height:46px; }
#shNav .tabb.on b{ height:auto; opacity:1; color:var(--pri-ink); text-shadow:none; }

/* ═══ OVERLAY POPUPS ══════════════════════════════════════════════════════════
   The scrim must cover the TRUE viewport, so these can't be children of the design
   card. Their .panel is scaled by var(--ui-scale) instead — otherwise on a short or
   landscape screen the card shrinks but the panels stay full size and blow up huge
   over a tiny game. */
/* 🚨 VISIBILITY, NOT JUST OPACITY.
   There are ELEVEN of these overlays and they all sit at position:fixed;inset:0. With only
   `opacity:0` they stay in the paint tree, so the browser composites eleven full-screen
   transparent layers on top of the game's WebGL canvas EVERY FRAME. Harmless on a desktop
   GPU, brutal on a phone — it is pure overdraw for something the player cannot see.

   `visibility:hidden` takes them out of painting entirely while keeping the fade: the
   transition delays visibility until the opacity fade has finished, so closing still
   animates instead of snapping. Do not "simplify" this to display:none — that kills the
   transition — and do not drop the visibility back out, which is what made it slow. */
.sh-ov{ position:fixed; inset:0; z-index:20; display:flex; align-items:center; justify-content:center;
  padding:20px; background:rgba(20,14,8,0); opacity:0; pointer-events:none;
  visibility:hidden;
  transition:background .25s, opacity .25s, visibility 0s .25s;
  font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; color:var(--text); }
.sh-ov.show{ background:rgba(20,14,8,.55); opacity:1; pointer-events:auto;
  visibility:visible; transition:background .25s, opacity .25s, visibility 0s 0s; }
.sh-ov button{ font-family:inherit; }

/* TRAP #1/#3: the panel is a FIXED design-space width and rests AT the card scale.
   `transform:none` on the keyframe end would snap it back to full raw size the instant
   the animation finished, silently undoing the fix. */
.sh-ov .panel{ position:relative; width:340px; flex:none; background:var(--panel);
  border:var(--ink-w) solid var(--ink); border-radius:26px; padding:22px 18px 20px;
  box-shadow:0 12px 0 rgba(0,0,0,.18),0 20px 40px rgba(0,0,0,.4);
  transform:translateY(24px) scale(calc(var(--ui-scale,1) * .85)); opacity:0; }
.sh-ov.show .panel{ animation:sh-popIn .4s cubic-bezier(.34,1.56,.64,1) forwards; }

@keyframes sh-popIn{ to{ transform:scale(var(--ui-scale,1)); opacity:1 } }

.sh-ov .phead{ position:absolute; top:-20px; left:50%; transform:translateX(-50%); white-space:nowrap;
  background:linear-gradient(var(--accent),var(--accent-dk)); color:var(--pri-ink);
  border:var(--ink-w) solid var(--ink); border-radius:16px; padding:7px 26px; font-size:18px; font-weight:800;
  letter-spacing:.06em; box-shadow:0 5px 0 var(--accent-dk); }
.sh-ov .phead.grey{ background:linear-gradient(#8390a0,#5a6675); color:#fff;
  box-shadow:0 5px 0 #47515e; text-shadow:0 2px 0 #47515e; }
/* 🚨 `.btn.pclose`, NOT `.pclose` — THE RED WAS NEVER WINNING.
   Every close button is marked up `class="btn pclose ghost"`, and `.sh-ov .btn.ghost` (three
   classes) out-specifies `.sh-ov .pclose` (two), so the neutral ghost ramp overrode the red ramp
   defined right here and every close button rendered pale grey. Measured: --c1 resolved to
   #f2fbff instead of #ff7a68.
   Matching `.btn.pclose` ties the specificity and this rule is later in the file, so it wins.
   CLOSE IS RED EVERYWHERE — it is the one control that leaves a panel rather than doing something
   in it, and nothing else in the button language distinguishes those two jobs. */
.sh-ov .btn.pclose{ position:absolute; top:-16px; right:-12px; width:42px; height:42px; padding:0;
  display:grid; place-items:center; z-index:6; --c1:#ff7a68; --c2:#ec4a34; --base:#c5321f;
  border-radius:50%; color:#fff; }
.sh-ov .pclose img{ width:58%; filter:brightness(0) invert(1); }
.sh-ov .pbody{ margin-top:16px; display:flex; flex-direction:column; align-items:center; gap:14px; text-align:center; }
.sh-ov .hero{ width:110px; height:110px; object-fit:contain; filter:drop-shadow(0 8px 10px rgba(0,0,0,.28)); }
.sh-ov .big-btn{ width:100%; height:58px; border-radius:20px; font-size:19px; letter-spacing:.05em;
  display:flex; align-items:center; justify-content:center; gap:9px; }
.sh-ov .big-btn img{ width:24px; height:24px; object-fit:contain; }
.sh-ov .sub{ width:100%; height:46px; border-radius:16px; font-size:15px; }
.sh-ov .row2{ display:flex; gap:10px; width:100%; }
/* The AD badge on the shoulder of every rewarded button. */
.sh-ov .rewtag{ position:absolute; top:-12px; right:9px; background:var(--ink); color:#fff;
  font-size:12px; font-weight:800; padding:3px 11px; border-radius:999px;
  display:flex; align-items:center; gap:4px; }
.sh-ov .rewtag img{ width:16px; height:16px; }
.sh-ov .rewtag.big{ top:-15px; right:8px; font-size:15px; padding:4px 13px; gap:5px; }
.sh-ov .rewtag.big img{ width:22px; height:22px; }
.sh-ov .muted{ color:var(--text); opacity:.72; font-size:12px; font-weight:700; }

.sh-ov .stars{ display:flex; gap:10px; margin:2px 0; }
.sh-ov .stars img{ width:50px; height:50px; object-fit:contain; transform:scale(0);
  animation:sh-starPop .5s cubic-bezier(.34,1.56,.64,1) forwards; }
.sh-ov .stars img.dim{ filter:grayscale(1) brightness(1.15) opacity(.85); }
.sh-ov.show .stars img:nth-child(1){ animation-delay:.12s; }
.sh-ov.show .stars img:nth-child(2){ animation-delay:.26s; }
.sh-ov.show .stars img:nth-child(3){ animation-delay:.40s; }
@keyframes sh-starPop{ 0%{transform:scale(0) rotate(-35deg)} 70%{transform:scale(1.25) rotate(8deg)} 100%{transform:scale(1)} }

.sh-ov .reward-pills{ display:flex; gap:10px; }
.sh-ov .rpill{ display:flex; align-items:center; gap:6px; background:var(--panel-2);
  border:var(--ink-w) solid var(--ink); border-radius:14px; padding:6px 14px;
  font-size:16px; font-weight:800; color:var(--text); }
.sh-ov .rpill img{ width:24px; height:24px; object-fit:contain; }
.sh-ov .confetti{ position:absolute; top:-6px; left:50%; transform:translateX(-50%);
  width:150%; max-width:none; opacity:0; pointer-events:none; }
.sh-ov.show .confetti{ animation:sh-conf .6s ease-out .05s forwards; }
@keyframes sh-conf{ from{opacity:0;transform:translateX(-50%) translateY(-24px) scale(.9)}
  to{opacity:1;transform:translateX(-50%) translateY(0) scale(1)} }

/* PAUSED is a bare full-stop screen: near-opaque blackout + a single badge, NO buttons.
   z-index 30 puts it above EVERYTHING (incl. the nav at 25) so nothing behind it is
   visible or tappable while paused. */
#shPaused{ z-index:30; }
#shPaused.show{ background:rgba(10,9,14,.92); }
#shPaused .paused-badge{ background:linear-gradient(var(--accent),var(--accent-dk));
  color:var(--pri-ink); border:var(--ink-w) solid var(--ink); border-radius:18px; padding:16px 46px;
  font-size:34px; font-weight:800; letter-spacing:.14em; box-shadow:0 6px 0 var(--accent-dk);
  transform:scale(calc(var(--ui-scale,1) * .9)); opacity:0; }
#shPaused.show .paused-badge{ animation:sh-popIn .4s cubic-bezier(.34,1.56,.64,1) forwards; }

/* ═══ STORE ═══════════════════════════════════════════════════════════════════
   TRAP #3: `max-height:52vh` here would be the REAL window and then scaled AGAIN.
   416px is 52% of the card's own 800. */
.sh-ov .shop-scroll{ width:100%; display:flex; flex-direction:column; gap:11px;
  max-height:416px; overflow-y:auto; padding:2px; }
.sh-ov .offer{ display:flex; align-items:center; gap:12px; background:var(--panel-2);
  border:var(--ink-w) solid var(--ink); border-radius:18px; padding:9px 11px; }
.sh-ov .offer img.oi{ width:50px; height:50px; object-fit:contain; flex:none;
  filter:drop-shadow(0 3px 3px #0003); }
.sh-ov .offer .oinfo{ flex:1; text-align:left; }
.sh-ov .offer .otitle{ font-size:15px; font-weight:800; color:var(--text); }
.sh-ov .offer .odesc{ font-size:11px; font-weight:700; color:var(--text); opacity:.65; }
.sh-ov .pricebtn{ height:40px; min-width:82px; border-radius:13px; font-size:14px; padding:0 12px;
  display:flex; align-items:center; justify-content:center; gap:5px; }
.sh-ov .pricebtn img{ width:16px; height:16px; }
.sh-ov .pricebtn[disabled]{ opacity:.5; }
.sh-ov .offer .otitle small{ font-size:11px; opacity:.6; font-weight:800; }
/* Upgrade level pips — how far along a track is, readable at a glance without reading a number. */
.sh-ov .pips{ display:flex; gap:3px; margin-top:5px; }
.sh-ov .pips i{ flex:1; height:5px; border-radius:3px; background:rgba(255,255,255,.16);
  border:var(--ink-w2) solid var(--ink); }
.sh-ov .pips i.on{ background:linear-gradient(var(--gold),var(--gold-dk)); }
#shUpgrades{ display:flex; flex-direction:column; gap:9px; margin-bottom:4px; }

/* ═══ OUT OF LIVES ════════════════════════════════════════════════════════════ */
.sh-ov .hearts{ display:flex; gap:7px; }
.sh-ov .hearts img{ width:34px; height:34px; object-fit:contain; }
.sh-ov .hearts img.empty{ filter:grayscale(1) brightness(1.3) opacity(.55); }
.sh-ov .timer-big{ font-size:14px; font-weight:800; color:var(--text); }
.sh-ov .timer-big b{ color:var(--accent-dk); }

/* ═══ LEAGUE ══════════════════════════════════════════════════════════════════ */
.sh-ov .league-head{ display:flex; flex-direction:column; align-items:center; gap:2px; }
.sh-ov .league-head img{ width:46px; height:46px; object-fit:contain; filter:drop-shadow(0 3px 3px #0003); }
.sh-ov .league-tier{ font-size:26px; font-weight:800; letter-spacing:1px;
  -webkit-text-stroke:3px var(--ink); paint-order:stroke fill; }
.sh-ov .league-yourrank{ font-size:13px; font-weight:700; color:var(--text); opacity:.8; }
.sh-ov .league-yourrank b{ color:var(--accent-dk); }
.sh-ov .league-promo{ width:100%; display:flex; flex-direction:column; align-items:center; gap:4px; }
.sh-ov .league-bar{ position:relative; width:100%; height:16px; border-radius:999px;
  background:var(--panel-2); border:var(--ink-w) solid var(--ink); overflow:hidden; }
.sh-ov .league-bar > i{ position:absolute; left:0; top:0; bottom:0; width:0; border-radius:999px;
  background:linear-gradient(var(--gold),var(--gold-dk)); transition:width .4s; }
.sh-ov .league-promo-txt{ font-size:12px; font-weight:800; color:var(--text); }
/* 320px is 40% of the card's own 800 — never 40vh (TRAP #3). */
.sh-ov .league-board{ width:100%; display:flex; flex-direction:column; gap:4px;
  max-height:320px; overflow-y:auto; padding:2px; }
.sh-ov .lrow{ display:flex; align-items:center; gap:10px; background:var(--panel-2);
  border:var(--ink-w) solid var(--ink); border-radius:12px; padding:7px 12px; }
.sh-ov .lrow .lr{ width:26px; text-align:center; font-weight:800; font-size:14px; color:var(--text); opacity:.7; }
.sh-ov .lrow .lname{ flex:1; text-align:left; font-weight:800; font-size:14px; color:var(--text);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* The score column and the YOU row follow the game's PRIMARY, not --accent. --accent is a
   decorative colour a game may point anywhere; "your row on the leaderboard" is the player's
   own identity and should be the same colour as their play button. Ants Collect had a blue UI
   and a lime-green leaderboard because of this. */
.sh-ov .lrow .lpts{ font-weight:800; font-size:14px; color:var(--pri-1); }
/* Column header — says ONCE what the number means, so it doesn't have to be repeated on
   twenty rows (which would not fit and would read as noise). */
.sh-ov .lhead{ display:flex; align-items:center; gap:10px; padding:2px 12px 6px;
  font-size:11px; letter-spacing:1.2px; opacity:.55; color:var(--text); }
.sh-ov .lhead .lr{ width:26px; text-align:center; }
.sh-ov .lhead .lname{ flex:1; text-align:left; }
/* The YOU row lives in its OWN container BELOW the scrolling board rather than sticky
   inside it, which overlapped the last row. Always visible, cleanly separated. */
.sh-ov .league-you{ width:100%; padding:0 2px; margin-top:6px; }
.sh-ov .lrow.you{ background:linear-gradient(var(--pri-1),var(--pri-2));
  box-shadow:0 4px 10px rgba(0,0,0,.3); }
.sh-ov .lrow.you .lr, .sh-ov .lrow.you .lname, .sh-ov .lrow.you .lpts{ color:var(--pri-ink); opacity:1; }
.sh-ov .lrow.promo{ box-shadow:inset 0 0 0 2px var(--green); }

/* A landed reward bumps its destination so the eye follows it home. */
#shNav .tabb.sh-bump{ animation:sh-bump .34s ease-out; }
@keyframes sh-bump{ 0%{transform:scale(1)} 40%{transform:scale(1.22)} 100%{transform:scale(1)} }

/* ═══ MORE GAMES (cross-promo) ════════════════════════════════════════════════ */
.sh-ov .mg-grid{ width:100%; display:grid; grid-template-columns:1fr 1fr; gap:11px;
  max-height:448px; overflow-y:auto; padding:2px; }
.sh-ov .mg-tile{ display:flex; flex-direction:column; gap:7px; background:var(--panel-2);
  border:var(--ink-w) solid var(--ink); border-radius:16px; padding:7px; cursor:pointer;
  box-shadow:0 3px 0 #0002; }
.sh-ov .mg-tile:active{ transform:translateY(2px); box-shadow:none; }
.sh-ov .mg-cover{ display:block; width:100%; aspect-ratio:1/1; border-radius:11px;
  overflow:hidden; border:var(--ink-w2) solid var(--ink); }
/* 🚨 THE COVER IMAGE MUST BE SIZED. This rule was MISSING from the shell (Overtake City Run has
   it — `.mg-cover img { width:100%; height:100%; object-fit:cover }` — and that is the build that
   shipped a working cross-promo grid). Without it each <img> lays out at its natural size, so a
   512px cover blows its tile out of the 2-column grid and the whole panel becomes one column of
   giant thumbnails. Only shows up once real cover art is configured, which is why it survived:
   with `moreGames: []` or letter placeholders there is no <img> to mis-size. */
.sh-ov .mg-cover img{ width:100%; height:100%; object-fit:cover; display:block; }
.sh-ov .mg-ph{ width:100%; height:100%; display:grid; place-items:center; font-size:30px;
  font-weight:800; color:#fff; -webkit-text-stroke:2px var(--ink); paint-order:stroke fill; }
.sh-ov .mg-name{ font-size:12px; font-weight:800; color:var(--text); text-align:center; line-height:1.15; }

/* ═══ IN-GAME HUD STRIP ═══════════════════════════════════════════════════════
   The shell's own in-run furniture: the progress bar, the objective chip and the way
   out. Games draw their own readouts BELOW this band — see the game's .col card. */
#gameShell .ghud{ position:absolute; inset:0; display:none; }
#gameShell .ghud.on{ display:block; }
/* `left:72px` clears the HOME button, which now sits in the top-left corner (see .quit).
   The bar used to start at 14px, where the button is. */
#gameShell .prog{ position:absolute; top:21px; left:72px; right:62px; height:20px;
  background:var(--panel-2); border:var(--ink-w) solid var(--ink); border-radius:999px; overflow:hidden; }
#gameShell .prog > i{ display:block; height:100%; width:0;
  background:linear-gradient(90deg,var(--gold),var(--gold-dk)); border-radius:999px;
  transition:width .1s linear; }
#gameShell .gflag{ position:absolute; top:15px; right:14px; width:26px; height:26px;
  background-image:conic-gradient(var(--ink) 90deg,#fff 0 180deg,var(--ink) 0 270deg,#fff 0);
  background-size:13px 13px; border:var(--ink-w) solid var(--ink); border-radius:7px; }
#gameShell .obj{ position:absolute; top:70px; left:14px; display:flex; align-items:center; gap:7px;
  background:var(--panel); border:var(--ink-w) solid var(--ink); padding:5px 13px 5px 8px;
  border-radius:14px; box-shadow:0 3px 0 #0002; }
#gameShell .obj img{ width:24px; height:24px; object-fit:contain; flex:none; }
#gameShell .obj .txt{ color:var(--text); font-weight:800; font-size:14px; line-height:1.05; letter-spacing:.5px; }

/* HOME — top-LEFT corner. Back/home lives top-left in every app a player has ever used;
   it was top-right, under the thumb that is busy steering, and read as a close-the-ad X.
   The progress bar starts at 72px to clear it, and .obj sits below it. */
#gameShell .quit{ position:absolute; top:15px; left:14px; width:44px; height:44px; padding:0;
  border-radius:14px; border:var(--ink-w) solid var(--ink); background:var(--panel); cursor:pointer;
  display:grid; place-items:center; box-shadow:0 3px 0 #0002; pointer-events:auto; }
#gameShell .quit img{ width:66%; height:66%; object-fit:contain; }

/* ── NO PROGRESS BAR: Shell.init({ progressBar:false }) ──────────────────────────
   Not every game has a "how far through the run am I" axis. A hole-eater scored on
   a target has nothing linear to fill, so the bar sat permanently empty and the
   chequered flag promised a race that wasn't happening. Games that DO have one
   (the racers) are unaffected — this is opt-out, the default stays on. */
#gameShell.noprog .prog,
#gameShell.noprog .gflag{ display:none; }
/* LEVEL CARD — "LEVEL n" across the middle as a run begins (Shell.init({levelIntro:true})).
   Sits over the scene, not on a panel, so it uses --text-scene with an ink stroke like the
   game title does. pointer-events:none — it must never block the first tap. */
/* Fail screen "got / needed", the got half counted up by countUp(). The target is
   deliberately smaller and muted — the number the player is looking for is their own. */
.sh-ov .scoreline{ display:flex; align-items:baseline; justify-content:center; gap:2px;
  font-weight:800; line-height:1; margin:-2px 0 2px; }
.sh-ov .scoreline b{ font-size:44px; color:var(--accent-dk); }
.sh-ov .scoreline i{ font-size:24px; font-style:normal; color:var(--text); opacity:.6; }

#gameShell .lvcard{ position:absolute; top:50%; left:0; right:0; text-align:center;
  transform:translateY(-50%); pointer-events:none; opacity:0;
  color:var(--text-scene); font-size:44px; font-weight:800; letter-spacing:3px;
  -webkit-text-stroke:6px var(--ink); paint-order:stroke fill;
  text-shadow:0 5px 0 rgba(0,0,0,.2); }
#gameShell .lvcard.show{ animation:sh-lvcard 1.25s ease-out forwards; }

/* 3 · 2 · 1 · GO! — same place and weight as the level card, but each tick punches in
   and shrinks away rather than fading, so the beat is legible at 600ms per number. */
#gameShell .cdown{ position:absolute; top:50%; left:0; right:0; text-align:center;
  transform:translateY(-50%); pointer-events:none; opacity:0;
  color:var(--text-scene); font-size:88px; font-weight:800; line-height:1;
  -webkit-text-stroke:8px var(--ink); paint-order:stroke fill;
  text-shadow:0 6px 0 rgba(0,0,0,.22); }
#gameShell .cdown.go{ font-size:64px; letter-spacing:3px; color:var(--gold); }
#gameShell .cdown.show{ animation:sh-cdown .6s cubic-bezier(.34,1.56,.64,1) forwards; }
@keyframes sh-cdown{
  0%   { opacity:0; transform:translateY(-50%) scale(1.9); }
  22%  { opacity:1; transform:translateY(-50%) scale(1); }
  70%  { opacity:1; transform:translateY(-50%) scale(1); }
  100% { opacity:0; transform:translateY(-50%) scale(.72); }
}
@keyframes sh-lvcard{
  0%   { opacity:0; transform:translateY(-50%) scale(.72); }
  18%  { opacity:1; transform:translateY(-50%) scale(1.06); }
  28%  { opacity:1; transform:translateY(-50%) scale(1); }
  72%  { opacity:1; transform:translateY(-50%) scale(1); }
  100% { opacity:0; transform:translateY(-50%) scale(1.12); }
}

#gameShell .hint{ position:absolute; bottom:52px; left:50%; transform:translateX(-50%);
  background:var(--panel); border:var(--ink-w) solid var(--ink); color:var(--text); padding:9px 20px;
  border-radius:999px; font-size:15px; font-weight:800; white-space:nowrap;
  box-shadow:0 3px 0 #0002; animation:sh-pulse 1.1s ease-in-out infinite; }
@keyframes sh-pulse{ 0%,100%{opacity:.82} 50%{opacity:1} }

/* ═══ TOAST + FLYING REWARDS ══════════════════════════════════════════════════ */
/* ⚠️ EVERY floating element must scale with the design card, not sit at raw px.
   These live OUTSIDE #gameShell (they must cover the true viewport), so they don't
   inherit the card's transform and have to apply var(--ui-scale) themselves. Without
   it the toast rendered 266px wide over a 210px card in landscape — 1.26x the whole
   UI — which is what "the popups are too big when rotated" was. */
#shToast{ position:fixed; left:50%; top:46%;
  transform:translate(-50%,-50%) scale(var(--ui-scale,1)); z-index:60;
  pointer-events:none; background:var(--panel); border:var(--ink-w) solid var(--ink); border-radius:14px;
  padding:10px 20px; font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; font-weight:800;
  font-size:15px; color:var(--text); opacity:0; transition:opacity .2s;
  box-shadow:0 4px 0 #0003; white-space:nowrap; }
#shToast.on{ opacity:1; }
.sh-pop{ animation:sh-countPop .3s ease; }
@keyframes sh-countPop{ 0%,100%{transform:scale(1)} 45%{transform:scale(1.35)} }

/* ═══ AD PLACEHOLDER ══════════════════════════════════════════════════════════
   Only ever shown when there is NO real ad SDK (i.e. local dev). On a real the host portal
   build the platform's own rewarded/interstitial takes over — see shell.js. */
#shAd{ position:fixed; inset:0; z-index:80; background:#0a0a12; display:none;
  flex-direction:column; align-items:center; justify-content:center; color:var(--text);
  pointer-events:auto; font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; }
#shAd.on{ display:flex; }
/* The scrim stays true-viewport (it must cover everything); the CONTENT scales with
   the card so the countdown isn't a giant number on a short landscape screen. */
#shAd > *{ transform:scale(var(--ui-scale,1)); transform-origin:center; }
#shAd .tag{ font-size:13px; letter-spacing:.2em; opacity:.7; }
#shAd .cnt{ font-size:64px; font-weight:800; color:var(--accent); }
#shAd .lbl{ font-size:12px; opacity:.55; }

/* The one line on the menu that says what the player is actually trying to do. */
#gameShell .gGoal{ position:absolute; top:136px; left:0; right:0; text-align:center;
  font-size:14px; font-weight:800; letter-spacing:.06em; color:var(--text-scene);
  -webkit-text-stroke:3px var(--ink); paint-order:stroke fill; pointer-events:none; }
#gameShell .gGoal b{ color:var(--gold); }

/* ═══ MENU UPGRADE STRIP ══════════════════════════════════════════════════════
   The permanent progression, on the main screen under the play button, where the
   player lands after every run. Compact rows — icon, name + level, pips, price —
   because this has to sit in the ~160px between the play stack and the nav bar
   without crowding the vehicle. Deliberately NOT in a tab: a progression the
   player has to go looking for may as well not exist. */
/* Upgrades are VERTICAL CARDS side by side, not a stacked list of rows. A card puts the
   art, the level and the price in one glanceable column, which is how every hybrid-casual
   upgrade shop the players already know is laid out — and a row of cards along the bottom
   reads as "things to buy" at a glance where a list of rows read as settings. */
#gameShell .gUpg{ position:absolute; left:14px; right:14px; bottom:88px; z-index:3;
  display:flex; flex-direction:row; align-items:stretch; gap:8px; pointer-events:auto; }
#gameShell .ucard{ flex:1 1 0; min-width:0; position:relative; display:flex; flex-direction:column;
  align-items:center; gap:5px; background:var(--panel); border:var(--ink-w) solid var(--ink);
  border-radius:16px; padding:8px 6px 7px; box-shadow:0 4px 0 #0002; }
/* The level badge overhangs the corner so it reads as a stamp ON the card. */
#gameShell .ucard .ulv{ position:absolute; top:-10px; right:-7px; width:27px; height:27px;
  border-radius:50%; display:grid; place-items:center; font-size:13px; font-weight:800;
  color:#fff; background:linear-gradient(var(--accent),var(--accent-dk));
  border:var(--ink-w) solid var(--ink); box-shadow:0 2px 0 var(--accent-dk); }
#gameShell .ucard .un{ font-size:11px; font-weight:800; color:var(--text); line-height:1;
  letter-spacing:.02em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
#gameShell .ucard .uart{ width:100%; aspect-ratio:1.15; border-radius:10px; background:var(--panel-2);
  border:var(--ink-w2) solid var(--ink); display:grid; place-items:center; overflow:hidden; }
#gameShell .ucard .uart img{ width:62%; height:62%; object-fit:contain; }
#gameShell .ucard .pips{ display:flex; gap:2px; width:100%; }
#gameShell .ucard .pips i{ flex:1; height:5px; border-radius:2px;
  background:rgba(255,255,255,.16); border:var(--ink-w2) solid var(--ink); }
#gameShell .ucard .pips i.on{ background:linear-gradient(var(--gold),var(--gold-dk)); }
#gameShell .ucard .ubuy{ width:100%; height:32px; border-radius:11px; font-size:13px; padding:0 4px;
  display:flex; align-items:center; justify-content:center; gap:3px; }
#gameShell .ucard .ubuy img{ width:14px; height:14px; }
#gameShell .ucard .ubuy[disabled]{ opacity:.45; }

/* ═══════════════════════════════════════════════════════════════════════════
   PORTED FROM OVERTAKE CITY RUN (2026-08-01) — the three pieces the original
   port left behind: the loading splash, the flying rewards and the tutorial
   coachmark.

   ⚠️ ALL THREE LIVE OUTSIDE THE 450×800 DESIGN CARD, ON PURPOSE.
   The splash must cover the REAL viewport (a card-sized splash would leave the
   letterboxing bare), and the flying coins + coachmark are positioned from
   getBoundingClientRect(), which already returns real screen pixels — putting
   either inside the card would scale them twice. See UI-SCALING-STANDARD.md.
   That is also why these rules are NOT nested under #gameShell.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ---------- loading splash ---------- */
#shSplash{ position:fixed; inset:0; z-index:9998; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:26px; padding:8vh 6vw;
  background:linear-gradient(var(--panel-2),var(--panel));
  transition:opacity .4s ease; }
#shSplash.ls-hide{ opacity:0; pointer-events:none; }

/* ═══ SUBSCRIBE BANNER ════════════════════════════════════════════════════════
   the host red, white type, NO logo — the wordmark is theirs and using it would be the
   third-party-content rejection (BWS_01) on top of everything else.
   pointer-events:none throughout: it is a message, not a button. Nothing to tap means
   nothing that can navigate away, which is the only version of this worth shipping.
   Slides down from the top on an ease-back, holds, slides away. Scales with the card
   like every other floating element. */
/* Always on screen. `pointer-events:none` is on the bar AND its children — this is a
   message, not a button, and there must be no way to tap it. */
#shSub{ position:fixed; left:50%; top:0; z-index:65; pointer-events:none;
  display:flex; align-items:center; gap:9px;
  background:#ff0033; color:#fff; border-radius:0 0 16px 16px;
  padding:9px 20px 10px; box-shadow:0 6px 16px rgba(0,0,0,.3);
  font-family:'Fredoka One','Baloo 2',system-ui,sans-serif; white-space:nowrap;
  transform:translate(-50%,-140%) scale(var(--ui-scale,1)); transform-origin:top center;
  opacity:0; }
#shSub *{ pointer-events:none; }
#shSub .sub-l{ font-size:13px; font-weight:800; letter-spacing:.12em; opacity:.92; }
#shSub .sub-h{ font-size:17px; font-weight:800; letter-spacing:.02em; }
/* Permanent: it settles at rest and stays. One drop-in on first show so it is noticed,
   then it simply lives there. */
#shSub.in{ opacity:1; transform:translate(-50%,0) scale(var(--ui-scale,1));
  animation:sh-subDrop .55s cubic-bezier(.34,1.5,.64,1) both; }
@keyframes sh-subDrop{
  0%  { opacity:0; transform:translate(-50%,-140%) scale(var(--ui-scale,1)); }
  70% { opacity:1; transform:translate(-50%,6%)    scale(var(--ui-scale,1)); }
  100%{ opacity:1; transform:translate(-50%,0)     scale(var(--ui-scale,1)); }
}

/* ── ROOM FOR THE BAR ────────────────────────────────────────────────────────
   The bar sits at the TRUE viewport top; the card's top-anchored elements start at
   the card's top. Without an offset they sit underneath it — the mission pill and the
   coin chips were half-covered. Everything anchored to the card's top moves down by
   the bar's height (card units), and nothing else in the layout has to know. */
#gameShell.subbar .top   { padding-top:76px; }
/* The in-run buttons only need to clear the subscribe bar, and on most aspect ratios the
   card already starts below it — 61px was pushing them a further 108px down, right onto
   the board. 22px keeps them clear of the bar on every ratio while sitting in the empty
   strip above the play area instead of on top of it. */
#gameShell.subbar .quit  { top:22px; }
#gameShell.subbar .spd   { top:22px; }
#gameShell.subbar .obj   { top:116px; }
#gameShell.subbar .prog  { top:67px; }
#gameShell.subbar .gflag { top:61px; }
#gameShell.subbar .gTitle{ top:138px; }
#gameShell.subbar .gGoal { top:182px; }

/* ═══ TRANSITION WIPE ═════════════════════════════════════════════════════════
   The loading splash's twin, flashed over a level change so the board is never seen
   being rebuilt. Same white, same logo, so startup and transitions look like one
   thing. Above everything except the ad overlay (z 80), and pointer-events:none the
   moment it starts fading so it can never swallow a tap. */
#shWipe{ position:fixed; inset:0; z-index:70; display:none;
  align-items:center; justify-content:center; background:#fff;
  opacity:0; transition:opacity .2s ease; pointer-events:auto; }
#shWipe.on{ display:flex; opacity:1; }
#shWipe.out{ opacity:0; pointer-events:none; transition:opacity .36s ease; }
#shWipe img{ max-width:82%; height:auto; object-fit:contain;
  transform:scale(var(--ui-scale,1)); transform-origin:center; }
/* Logo splash: plain WHITE, just the mark. A drawn logo is artwork — the themed
   panel gradient behind it reads as a mistake, and white is what a loading screen
   wants anyway (it also stops the splash flashing a colour the game never uses). */
#shSplash.ls-white{ background:#fff; }
/* Scaled like everything else: the width is set in px by JS, so the card's scale is
   applied here. Without it the logo was 225px over a 219px card in landscape — wider
   than the whole UI. */
#shSplash .ls-mark{ max-width:82%; height:auto; object-fit:contain;
  transform:scale(var(--ui-scale,1)); transform-origin:center; }
/* Studio mark — centred in the bottom half (75% down = the middle of it). Scales with
   the card like everything else. */
#shSplash .ls-studio{ position:absolute; left:50%; top:75%; max-width:60%; height:auto;
  object-fit:contain; transform:translate(-50%,-50%) scale(var(--ui-scale,1));
  transform-origin:center; opacity:.92; }
/* The home-screen wordmark becomes a plain image holder — no text stroke, no shadow,
   nothing that would draw a border around a transparent PNG. */
#gameShell .gTitle.has-mark{ -webkit-text-stroke:0; text-shadow:none; line-height:0; }
#gameShell .gTitle.has-mark img{ max-width:88%; height:auto; object-fit:contain;
  filter:drop-shadow(0 4px 6px rgba(0,0,0,.28)); }
#shSplash .ls-logo{ font-size:40px; font-weight:800; letter-spacing:1px; text-align:center;
  color:var(--text); -webkit-text-stroke:6px var(--ink); paint-order:stroke fill; }
#shSplash .ls-sub{ font-size:15px; font-weight:800; letter-spacing:.06em; color:var(--text); opacity:.75; }
#shSplash .ls-spin{ width:46px; height:46px; border:5px solid rgba(0,0,0,.12);
  border-top-color:var(--accent); border-radius:50%; animation:sh-lsSpin .85s linear infinite; }
@keyframes sh-lsSpin{ to{ transform:rotate(360deg) } }

/* ---------- flying rewards ---------- */
/* transition (not @keyframes) because every coin flies a DIFFERENT distance —
   the JS writes a per-coin translate and this just times it. */
/* Size is set inline by flyReward()/flyPts(), scaled by --ui-scale — a flying coin has
   to be the same size relative to the UI on a tall phone and a squat landscape screen.
   (This rule was also DUPLICATED higher up in the file; the dead copy is gone.) */
.sh-fly{ position:fixed; object-fit:contain; pointer-events:none;
  z-index:9999; will-change:transform,opacity; filter:drop-shadow(0 2px 3px #0005);
  transition:transform .56s cubic-bezier(.4,0,.6,1), opacity .13s ease; }

/* ---------- tutorial coachmark ---------- */
/* pointer-events:none on the WHOLE thing — the player's tap must reach the game
   underneath, or the coachmark blocks the very action it is teaching. */
#shTut{ position:fixed; left:50%; top:62%;
  transform:translate(-50%,-50%) scale(var(--ui-scale,1));
  z-index:9997; display:none; flex-direction:column; align-items:center; gap:10px;
  pointer-events:none; }
#shTut.show{ display:flex; }
#shTut .tut-hand{ width:72px; height:72px; }
#shTut .tut-hand img{ width:100%; height:100%; object-fit:contain;
  filter:drop-shadow(0 5px 6px rgba(0,0,0,.45));
  animation:sh-tutTap 1.15s ease-in-out infinite; transform-origin:50% 20%; }
#shTut .tut-pill{ background:var(--panel); border:var(--ink-w) solid var(--ink); color:var(--text);
  font-weight:800; font-size:17px; letter-spacing:.5px; padding:11px 24px; border-radius:999px;
  white-space:nowrap; box-shadow:0 4px 0 #0002; }
#shTut .tut-pill b{ color:var(--accent-dk); }
#shTut.done{ animation:sh-tutDone .9s ease-out forwards; }
@keyframes sh-tutTap{ 0%,100%{ transform:translateY(0) scale(1) } 45%{ transform:translateY(11px) scale(.9) } }
@keyframes sh-tutDone{ 0%{ transform:translate(-50%,-50%) scale(1); opacity:1 }
  30%{ transform:translate(-50%,-50%) scale(1.18); opacity:1 }
  100%{ transform:translate(-50%,-50%) scale(.7); opacity:0 } }

/* ── FAST-FORWARD BUTTON: Shell.init({ speeds:[1,2,3] }) ────────────────────────
   Top-RIGHT, mirroring the home button top-left, so the two run controls bookend the
   HUD instead of crowding each other. Sits inside the design card, so it scales with
   everything else — no vw/vh here, ever. */
#gameShell .spd{ position:absolute; top:15px; right:14px; min-width:56px; height:44px;
  padding:0 10px; border-radius:14px; border:var(--ink-w) solid var(--ink);
  background:var(--gh-2); color:var(--text); cursor:pointer; pointer-events:auto;
  font:inherit; font-size:19px; letter-spacing:.5px;
  display:grid; place-items:center; box-shadow:0 3px 0 #0002;
  transition:background .16s, color .16s, transform .1s; }
#gameShell .spd:active{ transform:translateY(2px); box-shadow:0 1px 0 #0002; }
/* Lit while wound on, so "why is this racing?" is answerable at a glance. */
#gameShell .spd.fast{ background:var(--pri-2); color:var(--pri-ink); border-color:var(--ink);
  animation:sh-spdPulse 1.1s ease-in-out infinite; }
@keyframes sh-spdPulse{ 0%,100%{ box-shadow:0 3px 0 #0002, 0 0 0 0 var(--pri-1); }
                        50%    { box-shadow:0 3px 0 #0002, 0 0 0 5px transparent; } }


/* ── OVERTAKE LINE on the win panel: Shell.init({ leagueMetric:'levels' }) ──────
   Inside the design card, so no vw/vh. Hidden when a level overtook nobody — a row
   reading "+0 places" is worse than no row. */
#shWin .overtake{ margin:2px 0 8px; padding:9px 14px; border-radius:12px;
  background:var(--gh-2); border:var(--ink-w2) solid var(--ink); color:var(--text);
  font-size:15px; line-height:1.35; letter-spacing:.2px;
  display:flex; flex-direction:column; align-items:center; gap:3px;
  animation:sh-otIn .45s cubic-bezier(.34,1.4,.64,1) both .18s; }
#shWin .overtake .ot-main{ display:inline-flex; align-items:center; gap:7px; flex-wrap:wrap;
  justify-content:center; }
/* The return reason. Quieter than the result — it is a nudge, not the headline. */
#shWin .overtake .ot-sub{ font-size:12.5px; opacity:.62; letter-spacing:.4px; }
#shWin .overtake.hidden{ display:none; }
#shWin .overtake b{ color:var(--pri-1); font-weight:inherit; }
#shWin .overtake i{ font-style:normal; color:var(--go-1); background:#0003;
  padding:1px 7px; border-radius:8px; font-size:13px; }
@keyframes sh-otIn{ from{ opacity:0; transform:translateY(8px) scale(.94); }
                    to  { opacity:1; transform:none; } }

/* ── MINI LEAGUE on the win panel: Shell.init({ leagueMetric:'levels' }) ────────
   Five rows of the ladder around the player, replacing the stars. Lives inside the
   design card, so no vw/vh — every size here is card px. */
#shWin .minilg{ width:100%; margin:2px 0 8px; display:flex; flex-direction:column; gap:3px; }
#shWin .minilg.hidden{ display:none; }
#shWin .minilg .mhead{ display:flex; align-items:center; justify-content:space-between;
  font-size:12px; letter-spacing:1.3px; opacity:.6; padding:0 6px 3px; color:var(--text); }
#shWin .minilg .mhead b{ color:var(--go-1); background:#0003; opacity:1;
  padding:2px 9px; border-radius:8px; font-size:12.5px; letter-spacing:.6px; }
#shWin .minilg .mrow{ display:flex; align-items:center; gap:9px; padding:6px 11px;
  border-radius:10px; background:var(--gh-2); font-size:14px; color:var(--text);
  animation:sh-mlIn .3s ease-out both; }
#shWin .minilg .mrow:nth-child(2){ animation-delay:.04s }
#shWin .minilg .mrow:nth-child(3){ animation-delay:.08s }
#shWin .minilg .mrow:nth-child(4){ animation-delay:.12s }
#shWin .minilg .mrow:nth-child(5){ animation-delay:.16s }
#shWin .minilg .mrow:nth-child(6){ animation-delay:.20s }
#shWin .minilg .mr{ width:22px; text-align:center; opacity:.6; font-size:12.5px; }
#shWin .minilg .mn{ flex:1; text-align:left; }
#shWin .minilg .mv{ color:var(--pri-1); }
/* The player. Rises into place, so the movement is felt rather than just stated. */
#shWin .minilg .mrow.you{ background:linear-gradient(var(--pri-1),var(--pri-2));
  border:var(--ink-w2) solid var(--ink); animation:sh-mlRise .5s cubic-bezier(.34,1.4,.64,1) both .22s; }
#shWin .minilg .mrow.you .mr, #shWin .minilg .mrow.you .mn, #shWin .minilg .mrow.you .mv{
  color:var(--pri-ink); opacity:1; }
/* The one directly above — who you are chasing next. */
#shWin .minilg .mrow.target{ box-shadow:inset 0 0 0 2px var(--go-2); }
@keyframes sh-mlIn  { from{ opacity:0; transform:translateY(-5px); } to{ opacity:1; transform:none; } }
@keyframes sh-mlRise{ from{ opacity:0; transform:translateY(26px) scale(.96); } to{ opacity:1; transform:none; } }

/* ── BIG SCORE + COMBO: Shell.init({ bigScore:true }) ───────────────────────────
   For games where a running number IS the game. Sits at the top of the design card,
   below the home/speed buttons, and drops further when the subscribe bar is up.
   Card units only — no vw/vh inside the card, ever. */
/* Hard against the top of the card. It was sitting mid-screen, which puts the number in
   the middle of the play area — distracting, and it fights the thing the player is actually
   looking at. Up here it sits between the home and speed buttons and reads as chrome. */
#gameShell .bigscore{ position:absolute; top:16px; left:0; right:0; text-align:center;
  font-size:54px; line-height:1; font-weight:800; letter-spacing:-1px;
  color:var(--text-scene); pointer-events:none;
  -webkit-text-stroke:5px var(--ink); paint-order:stroke fill; }
#gameShell .bigscore.bump{ animation:sh-scoreBump .18s ease-out; }
@keyframes sh-scoreBump{ 0%{transform:scale(1)} 45%{transform:scale(1.13)} 100%{transform:scale(1)} }

/* The callout. Hidden until something happens — an empty pill sitting under the score
   for a whole run is clutter, and it stops the real one meaning anything. */
#gameShell .combo{ position:absolute; top:74px; left:0; right:0; text-align:center;
  font-size:21px; font-weight:800; letter-spacing:.06em; pointer-events:none;
  color:var(--pri-1); opacity:0;
  -webkit-text-stroke:4px var(--ink); paint-order:stroke fill; }
#gameShell .combo b{ color:var(--gd-1); font-weight:inherit; }
#gameShell .combo.show{ animation:sh-comboPop .9s cubic-bezier(.34,1.5,.64,1) both; }
@keyframes sh-comboPop{
  0%  { opacity:0; transform:translateY(10px) scale(.7); }
  18% { opacity:1; transform:translateY(0) scale(1.12); }
  30% { transform:scale(1); }
  75% { opacity:1; }
  100%{ opacity:0; transform:translateY(-12px) scale(1); }
}
/* With the subscribe bar up, clear it but stay level with the home/speed buttons. */
#gameShell.subbar .bigscore{ top:20px; }
#gameShell.subbar .combo   { top:78px; }
