/* =============================================================================
 * style.css - shell, HUD, loading + welcome screens and menu chrome.
 * All gameplay art is drawn on the canvas; this file is only the UI around it.
 * No image or font files: icons are inline SVG, the wordmark is system type
 * with a stroke, and the paper background is two CSS gradients.
 * ========================================================================== */

:root {
  --ink:       #2b3a67;   /* deep navy - outlines and text */
  /* Secondary text. Was #6b7aa3, which measured 3.8:1 to 4.25:1 on white -
     under the 4.5:1 needed at the small sizes it is actually used at (skin
     names, the /333 suffix, the best line). Darkened until every one of those
     clears it, which is a shade the eye barely registers as different. */
  --ink-soft:  #56679a;
  --paper:     #f2fbff;
  --grid:      #cbeaff;

  --blue:      #3aa9ff;
  --blue-dk:   #1b7fd4;
  --green:     #4dd865;
  --green-dk:  #2fa845;
  --cash:      #3fbf6a;
  --cash-dk:   #1e7a45;
  --cash-lt:   #9ff0be;
  --yellow:    #ffc93c;
  --yellow-dk: #d99a06;
  --coral:     #ff7b7b;
  --coral-dk:  #d9484f;
  --violet:    #a78bfa;
  --violet-dk: #7c5ce0;
  --mint:      #8ee6c8;
  --pink:      #ff8fc8;

  --panel:     #ffffff;
  --panel-2:   #f2f6fb;

  --r-lg: 28px;
  --r-md: 18px;
  --r-sm: 13px;

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* ========================== SCROLLBARS ==============================
   Drawn, not inherited. A stock grey Windows scrollbar down the side of a
   hand-drawn cartoon panel is the single most obvious seam between "a game"
   and "a web page with a game in it", and the shop has a permanent one.

   Two syntaxes, because there is no single standard: Chrome, Edge, Brave,
   Safari and every Poki-relevant mobile browser take the ::-webkit-scrollbar
   pseudo-elements, while Firefox ignores those entirely and honours only
   scrollbar-width / scrollbar-color. Both are set, so it is styled everywhere
   and merely thin-and-tinted on Firefox rather than wrong.

   The thumb is a rounded pill in the same violet as the reward buttons, on a
   faint ink track - the same shape language as every other bar in the game
   (the load bar, the run bar, the offer timer). */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--violet-dk) rgba(43,58,103,0.10);
}
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track {
  background: rgba(43,58,103,0.09);
  border-radius: 999px;
  /* Insets the track from the ends so it reads as a floating pill rather than
     a channel cut into the panel edge. */
  margin: 4px 0;
}
::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: linear-gradient(180deg, #a78bfa 0%, #7c5ce0 100%);
  /* Transparent border + padding-box clip is what puts real breathing room
     between the thumb and the track; a plain inset would just shrink it. */
  border: 2px solid transparent;
  background-clip: padding-box;
  min-height: 34px;              /* never a dot on a long list */
}
::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, #b9a3ff 0%, #8b6cf0 100%);
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active {
  background: linear-gradient(180deg, #8b6cf0 0%, #6845d0 100%);
  background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }

/* Over a dark card the ink track disappears, so those get a light one. */
.screen-solid ::-webkit-scrollbar-track,
.cf-card ::-webkit-scrollbar-track { background: rgba(255,255,255,0.16); }

html, body {
  margin: 0; padding: 0;
  width: 100%; height: 100%;
  overflow: hidden;
  background: #9fdcff;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  color: var(--ink);
  user-select: none; -webkit-user-select: none;
  overscroll-behavior: none;
}

#app { position: fixed; inset: 0; touch-action: none; }
#game { display: block; width: 100%; height: 100%; }
#ui { position: absolute; inset: 0; pointer-events: none; }

.hidden { display: none !important; }

/* Shared icon behaviour: SVGs inherit the button's colour. */
.ico { display: block; width: 100%; height: 100%; fill: currentColor; stroke: currentColor; }
.ico[viewBox] { pointer-events: none; }
.ico path[fill="none"], .ico rect[fill="none"] { fill: none; }

/* ============================== HUD ================================== */
.hud {
  position: absolute;
  top: calc(10px + var(--safe-t));
  left: calc(12px + var(--safe-l));
  right: calc(12px + var(--safe-r));
  display: none;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.hud.active { display: flex; }


.hud-side { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.hud-right { justify-content: flex-end; }
/* The left group carries the level counter, with the skip switch hung under
   it. The switch is taken OUT of the flex row on purpose: it is wider than the
   pill, and while it was in flow it set the left group's width, which pushed
   the score and the right-hand group along and overflowed the row at 320px -
   worse in Spanish and Russian, where the label is longer. Hanging it off the
   bottom means the row is measured by the pill alone, whatever the label says. */
.hud-left { position: relative; }
.hud-toggle {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
}

/* The in-game skip switch.
   Built from `.pill` rather than from the menu's plate, because in here it has
   to match the thing it sits under - the level counter - and that is a pill.
   Same shell, same border, same radius, same type; the switch is the same
   component as the menu's so the two controls are visibly one setting. */
.hud-toggle {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: 9px;
  background: rgba(255,255,255,0.94);
  border: 3px solid var(--ink);
  border-radius: 999px;
  /* 40px is the tap-target floor, and the switch inside is only 23px tall. */
  min-height: 40px;
  padding: 4px 6px 4px 13px;
  font-family: inherit;
  font-weight: 800;
  font-size: clamp(10px, 2.1vw, 12px);
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  box-shadow: 0 3px 0 rgba(43,58,103,0.25);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.hud-toggle:active { transform: translateY(1px); }
.hud-toggle:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.hud-toggle .plate-switch { width: 40px; height: 23px; margin-right: 0; }
.hud-toggle .plate-switch i { width: 19px; height: 19px; top: 2px; left: 2px; }
.hud-toggle[aria-checked="true"] .plate-switch { background: var(--green-dk); }
.hud-toggle[aria-checked="true"] .plate-switch i { transform: translateX(17px); }
.hud-toggle[aria-checked="false"] .hud-toggle-label { opacity: 0.55; }

/* Under 640px the label is dropped and the switch stands alone.
   The progress bar is centred at 46vw and the labelled pill reaches far enough
   right to sit on it below about 620px - and the Russian label reaches further
   still. The button keeps its aria-label, so it is named either way, and it
   stays square so the tap target does not shrink with the text. */
@media (max-width: 640px) {
  .hud-toggle { padding: 4px 6px; gap: 0; min-width: 40px; justify-content: center; }
  .hud-toggle .hud-toggle-label { display: none; }
}

.hud-center {
  flex: 1 1 auto;
  text-align: center;
  font-size: clamp(34px, 6.5vw, 58px);
  font-weight: 900;
  color: var(--blue);
  -webkit-text-stroke: 5px #fff;
  paint-order: stroke fill;
  line-height: 1;
  letter-spacing: 0.02em;
}

.pill {
  display: inline-flex; align-items: center; gap: 7px;
  background: rgba(255,255,255,0.94);
  border: 3px solid var(--ink);
  border-radius: 999px;
  padding: 5px 13px;
  font-weight: 800;
  font-size: clamp(12px, 2.6vw, 16px);
  box-shadow: 0 3px 0 rgba(43,58,103,0.25);
  white-space: nowrap;
}
.pill.big { font-size: clamp(15px, 3.2vw, 20px); padding: 7px 16px; }
.pill-cash { color: var(--cash-dk); }

/* Level counter. Showing the total alongside turns "level 25" into "25 of
   333", which is a target rather than a label. */
.pill-level { gap: 5px; color: var(--ink); }
/* The darker blue, not --blue: the pill behind it is near-white, and the light
   blue measured 2.5:1 on it - the level number was the least readable thing in
   the HUD despite being one of the three things the HUD is for. */
.pill-level b { color: #1769b8; font-variant-numeric: tabular-nums; }
.pill-level .lv-of { opacity: 0.5; font-weight: 700; }

/* One-shot bump, retriggered from JS whenever a HUD number changes. */
.bump { animation: bump 0.26s cubic-bezier(.25,1.7,.5,1); }
@keyframes bump {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.22); }
  100% { transform: scale(1); }
}
.hud-center .bump { display: inline-block; }

/* Cash note icon - a little green banknote, matching the pickups on canvas. */
.cash-ico {
  width: 22px; height: 14px; flex: 0 0 auto;
  border-radius: 3px;
  background: var(--cash);
  border: 2px solid var(--cash-dk);
  position: relative;
  display: inline-block;
}
.cash-ico::after {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--cash-lt);
  box-shadow: 0 0 0 1.5px rgba(255,255,255,0.7);
}
.cash-ico.small { width: 18px; height: 12px; margin-right: 5px; }

.icon-btn {
  pointer-events: auto;
  width: 46px; height: 46px;
  border-radius: var(--r-sm);
  background: #fff;
  border: 3px solid var(--ink);
  box-shadow: 0 4px 0 rgba(43,58,103,0.3);
  cursor: pointer;
  display: grid; place-items: center;
  padding: 11px;
  color: var(--coral);
}
.icon-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 rgba(43,58,103,0.3); }
.ico-pause { stroke: none; }

/* Audio has one home for the whole game: top right, above every screen,
   never moving. Sitting outside #hud is what makes that work - the HUD is
   hidden the moment a panel opens, and this must stay reachable there.
   During play the HUD's right group is padded to make room for it.

   Effects and music are two switches, stacked in a column. A column is what
   keeps the rest of this file true: every clearance below - the HUD's right
   padding, the menu cash pill's margin - is written against the width of ONE
   button, and a second button beside it would have meant re-tuning all of it
   at three breakpoints for no gain. */
.audio-fixed {
  position: absolute;
  top: calc(10px + var(--safe-t));
  right: calc(12px + var(--safe-r));
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.audio-fixed .icon-btn { color: var(--violet-dk); }
.audio-fixed .ico-sound { fill: currentColor; stroke: currentColor; }
.audio-fixed .ico-music { fill: currentColor; stroke: currentColor; }
/* Outline only, unlike the two above it - a filled house at this size turns
   into a blob, and the roofline is what makes it read as Home. */
.audio-fixed .ico-home { fill: none; stroke: currentColor; }
/* Home is hidden on the home screen and on the loading splash. A control that
   takes you where you already are is noise in a three-button column. It stays
   up everywhere else, including over the pause and game-over panels, because
   that is the whole point of this column: it does not move. */
body.at-home #btnHome { display: none; }
.hud .hud-right { padding-right: 54px; }
/* The menu's cash pill shares that corner, so it steps aside too. */
.menu-cash { margin-right: 54px; }

@media (max-width: 460px) {
  .audio-fixed .icon-btn { width: 40px; height: 40px; padding: 9px; }
  .audio-fixed { gap: 6px; }
  .hud .hud-right { padding-right: 46px; }
  /* .menu-cash sizing lives with its base rule further down - a media query
     up here loses to a plain rule below it, media queries add no specificity. */

  /* On a narrow phone the HUD row does not fit: level pill + score + cash +
     pause + the space reserved for the sound button comes to more than the
     available width, the row overflows and pause slides under sound.
     The word "LEVEL" is the cheapest thing to drop - "25/333" still reads as
     a level counter - and tightening the paddings recovers the rest. */
  .pill-level #hudLevelWord { display: none; }
  .pill { padding: 5px 10px; gap: 5px; }
  .hud { gap: 6px; }
  .hud-side { gap: 6px; }
}

/* Folds and narrow embeds. Still not enough room at 280, so the round buttons
   and the score come down as well. */
@media (max-width: 340px) {
  /* 40px, not the 36px this used to be. The shrink was to stop the HUD row
     overflowing, but the tighter pill paddings below recovered enough width
     that the buttons no longer have to pay for it - measured down to 280px,
     where the row still fits with the two corner buttons 4px apart. 36px is
     under a fingertip, and these are the only two controls during play. */
  .icon-btn, .audio-fixed .icon-btn { width: 40px; height: 40px; padding: 9px; }
  .hud .hud-right { padding-right: 44px; }
  .hud-center { font-size: clamp(26px, 9vw, 40px); -webkit-text-stroke-width: 4px; }
  .pill { font-size: 11px; padding: 4px 8px; }
  .cash-ico { width: 18px; height: 12px; }
}

/* Level progress. Hyper-casual players need to see the goal, not just the
   next obstacle - it is the cheapest "why am I doing this" fix there is. */
.progress {
  position: absolute;
  top: calc(64px + var(--safe-t));
  left: 50%;
  transform: translateX(-50%);
  width: min(46vw, 260px);
  height: 9px;
  border-radius: 999px;
  background: rgba(255,255,255,0.55);
  box-shadow: 0 0 0 1px rgba(43,58,103,0.14), 0 2px 5px -2px rgba(20,32,66,0.3);
  display: none;
}
.progress.active { display: block; }
.progress-fill {
  height: 100%; width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, #6fd3ff, #3aa9ff);
  transition: width 0.12s linear;
}
.progress-flag {
  position: absolute; right: -3px; top: 50%;
  width: 10px; height: 10px; margin-top: -5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 2px var(--blue);
}

/* The player's own head rides the fill. Painted by the game's renderer, so it
   always wears the equipped skin. */
.progress-head {
  position: absolute;
  top: 50%;
  left: 0;
  width: 36px; height: 36px;
  margin: -18px 0 0 -18px;
  pointer-events: none;
  will-change: transform;
}
.progress-head.hop { animation: headHop 0.34s cubic-bezier(.3,1.5,.5,1); }
@keyframes headHop {
  0%   { transform: translateY(0) scale(1); }
  35%  { transform: translateY(-7px) scale(1.14); }
  100% { transform: translateY(0) scale(1); }
}

/* Tutorial banner - the one control, spelled out over the first hops. */
.tap-hint {
  position: absolute;
  left: 50%; top: 20%;
  transform: translate(-50%, -50%);
  padding: 10px 22px;
  font-size: clamp(17px, 3.6vw, 34px);
  font-weight: 900;
  letter-spacing: 0.01em;
  color: #5c6cc4;
  -webkit-text-stroke: 7px #fff;
  paint-order: stroke fill;
  opacity: 0;
  transition: opacity 0.25s;
  text-align: center;
  white-space: nowrap;
  max-width: 94vw;
}
/* The desktop wording is long; let it wrap rather than run off a phone. */
@media (max-width: 620px) {
  .tap-hint { white-space: normal; line-height: 1.15; max-width: 88vw; }
}
.tap-hint.show { opacity: 1; animation: pulse 1.15s ease-in-out infinite; }
/* After the first tap it stays up for the rest of level 1, but smaller, dimmer
   and still, so it reads as a reminder rather than an instruction. */
.tap-hint.show.calm {
  opacity: 0.62;
  animation: none;
  font-size: clamp(13px, 2.4vw, 22px);
  -webkit-text-stroke-width: 5px;
  transition: opacity 0.4s, font-size 0.4s;
}
@keyframes pulse {
  0%,100% { transform: translate(-50%,-50%) scale(1); }
  50%     { transform: translate(-50%,-50%) scale(1.055); }
}

/* ============================== ADS ==================================
   An ad is a full takeover and has to be the top layer, full stop.

   Without this the level-clear overlay (z-index 30) painted straight over the
   placeholder card: LEVEL COMPLETE, the stars, the cash and a dead DOUBLE CASH
   button all sitting on top of the ad. The overlay is not a .screen, so it is
   not in the stack showScreen() manages, and the stub card is switched on with
   a raw classList call that never goes through showScreen at all.

   Belt and braces, because the z-index alone only protects the STUB. A real
   portal SDK renders its ad outside our DOM entirely, where our stacking order
   means nothing, so `body.in-ad` also hides every floating thing we own for
   the duration. Set from Ads.onAdStart / onAdEnd. */
#screen-ad { z-index: 90; }

body.in-ad .clear-flash,
body.in-ad .story-card,
body.in-ad .audio-fixed,
body.in-ad .toast,
body.in-ad .hud,
body.in-ad .progress { visibility: hidden; }

/* ============================ STORY CARD =============================
   One line, once, over the live world while he is still flying. It never
   waits for a tap and never blocks anything - the player is mid-launch and
   the card is gone before the first platform arrives. */
.story-card {
  position: absolute;
  left: 50%; top: 34%;
  transform: translate(-50%, -50%);
  padding: 0 16px;
  max-width: 92vw;
  text-align: center;
  font-size: clamp(19px, 4.6vw, 40px);
  font-weight: 900;
  letter-spacing: 0.03em;
  line-height: 1.08;
  color: #ff4d7d;
  -webkit-text-stroke: 9px #fff;
  paint-order: stroke fill;
  filter: drop-shadow(0 5px 0 rgba(20,32,66,0.22));
  opacity: 0;
  pointer-events: none;
  z-index: 32;
}
.story-card.show { animation: storyIn 2.6s cubic-bezier(.16,1.5,.4,1) both; }
@keyframes storyIn {
  0%   { opacity: 0; transform: translate(-50%,-50%) scale(0.7); }
  14%  { opacity: 1; transform: translate(-50%,-50%) scale(1.06); }
  22%  { opacity: 1; transform: translate(-50%,-50%) scale(1); }
  78%  { opacity: 1; transform: translate(-50%,-50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%,-50%) scale(1.1); }
}
@media (prefers-reduced-motion: reduce) {
  .story-card.show { animation: none; opacity: 1; }
}

/* ============================ GAME OVER ==============================
   Built to mirror the level-clear overlay exactly - same anchors, same widths,
   same button stack - so the two ends of a run feel like one system rather
   than a cheerful overlay and an unrelated red box.

   The scrim is a soft red vignette instead of a card on a wash. The body is
   lying on screen where it landed, and letting the player see it is both more
   honest about what happened and far less heavy than covering it up. */
/* Two classes deep on purpose. This block sits ABOVE `.screen` in the file, so
   a plain `.screen-fail` would tie on specificity and lose the cascade - the
   panel would centre itself in a heap instead of spreading, and the body it is
   meant to be framing would end up behind the buttons. */
/* The same compact card as the level clear, so the two ends of a run are one
   piece of furniture in two colours.

   Centred, unlike the level-clear card. That one hugs the bottom to keep the
   doll celebrating on the goal pad in view above it; here there is no such
   moment to protect - the run is over - so the card sits in the middle of the
   vignette where the eye already is, and MENU stops crowding the bottom rim. */
.screen.screen-fail {
  background: radial-gradient(125% 95% at 50% 44%,
              rgba(70,20,30,0.26) 0%, rgba(24,10,16,0.70) 100%);
  justify-content: center;
}
.over-card { gap: 8px; }
.over-title {
  margin: 0;
  font-size: clamp(20px, 3.8vw, 34px);
  font-weight: 900;
  letter-spacing: 0.045em;
  line-height: 1;
  color: #ff6b7f;
  text-shadow: 0 2px 0 rgba(6,10,22,0.5);
}
.over-level {
  margin: -2px 0 0;
  font-size: 12px; font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.72);
}
.over-level b { color: #fff; font-size: 1.3em; margin-left: 4px; }
.over-run { width: 100%; }
.over-cash {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: clamp(17px, 3.6vw, 22px); font-weight: 900;
  color: #ffe07a;
  text-shadow: 0 2px 0 rgba(6,10,22,0.5);
}

/* The way back out, kept quiet on purpose. It is the one thing on this screen
   nobody should be nudged toward, but trapping the player would be worse. */
.over-menu {
  pointer-events: auto;
  background: none; border: 0;
  /* Padded to a 42px tall hit area. It measured 22px, which is half a
     fingertip - quiet is a visual weight, not a reason to be hard to press. */
  padding: 11px 20px;
  min-height: 42px;
  font: inherit;
  font-size: 13px; font-weight: 800; letter-spacing: 0.14em;
  text-transform: uppercase;
  /* Quiet, but still legible against a bright sky - the earlier 0.6 white
     vanished completely on the pale themes. Quiet means underplayed, not
     unreadable. */
  color: #fff;
  opacity: 0.82;
  text-shadow: 0 2px 6px rgba(10,14,28,0.75);
  cursor: pointer;
}
.over-menu:active { opacity: 1; }

/* The run bar now sits over the world rather than on a red slab, so it needs
   its own contrast instead of borrowing the band's. */
.over-run .run-bar {
  height: 12px;
  margin: 0 auto 7px;
  width: 100%;
  background: rgba(255,255,255,0.24);
  box-shadow: 0 0 0 2px rgba(0,0,0,0.22) inset;
}
.over-run .run-bar-cap {
  text-align: center;
  color: rgba(255,255,255,0.92);
  font-size: 12px;
}
.over-run .run-bar-cap b { font-size: 17px; color: #fff; margin-right: 2px; }

@media (max-height: 520px) {
  .over-card { gap: 6px; padding: 10px 14px 11px; }
  .over-title { font-size: clamp(17px, 4vw, 24px); }
  .over-level { display: none; }
}

/* ========================= LEVEL CLEAR ==============================
   An overlay, not a screen. There is no scrim and no card: the world stays
   fully visible and the doll keeps celebrating on the goal pad, so clearing a
   level reads as a beat in one continuous run rather than a scene change.

   pointer-events stay off for the whole layer; only the rewarded button turns
   them back on. Everything else here is a label, and a label that eats taps
   during gameplay is a bug waiting to happen. */
.clear-flash {
  position: absolute; inset: 0;
  display: none;
  flex-direction: column;
  /* Centred, matching the game-over card. It cannot go to the TOP - this
     overlay is not a .screen, so showScreen() never hides the HUD, and a
     banner up there lands on the progress bar and its head marker. */
  justify-content: center;
  align-items: center;
  padding: 0 calc(14px + var(--safe-r))
           calc(18px + var(--safe-b)) calc(14px + var(--safe-l));
  pointer-events: none;
  z-index: 30;
}
.clear-flash.active { display: flex; }

/* One card, not floating text. A gradient wash was too soft to separate
   white-stroked letters from a bright sky - the readout looked like debris
   scattered over the level. A defined card with its own shadow reads as a
   piece of UI at a glance, and being a fixed compact box is also what stops
   this from sprawling down the screen. */
.cf-card {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: 7px;
  width: min(330px, 86vw);
  padding: 13px 16px 14px;
  border-radius: 22px;
  background: rgba(20,26,46,0.74);
  box-shadow: 0 14px 34px -10px rgba(8,12,26,0.7),
              0 0 0 2px rgba(255,255,255,0.10) inset;
  text-align: center;
  animation: cfRise 0.42s cubic-bezier(.18,1.5,.42,1) both;
}
/* Sized to be read at a glance, not to fill the screen. The old 52px ceiling
   on a desktop viewport made a level-clear notice as loud as the game's own
   logo, for something that is gone again in three seconds. */
.cf-title {
  margin: 0;
  font-size: clamp(19px, 3.4vw, 32px);
  font-weight: 900;
  letter-spacing: 0.045em;
  line-height: 1;
  /* On the dark card the heavy white outline is no longer doing any work, so
     the colour goes bright and carries itself. */
  color: #4ce38c;
  text-shadow: 0 2px 0 rgba(6,10,22,0.5);
}
.cf-stars { margin: 0; gap: 6px; }
.cf-stars .star {
  width: 22px; height: 22px;
  background: rgba(255,255,255,0.20);
  box-shadow: none;
}
.cf-stars .star:nth-child(2) { width: 28px; height: 28px; margin-top: -6px; }
/* On the world rather than on a coloured band, so the empty socket needs a
   light rim to stay legible against sky, cliffs or a dark theme alike. */
.cf-stars .star {
  background: rgba(255,255,255,0.55);
  box-shadow: 0 0 0 2px rgba(43,58,103,0.18);
}

.cf-card > .btn, .cf-card > .cf-timer { width: 100%; }
.cf-cash {
  display: flex; align-items: center; gap: 7px;
  font-size: clamp(20px, 4vw, 28px);
  font-weight: 900;
  color: #ffe07a;
  text-shadow: 0 2px 0 rgba(6,10,22,0.5);
}
.cf-double { pointer-events: auto; width: 100%; }

/* The window to take the offer, drawn as the thing it is: a bar that runs
   out. A bare countdown number would compete with the digits already on the
   cash line, and a button that simply vanishes reads as a glitch. */
.cf-timer {
  width: 100%; height: 8px;
  border-radius: 999px;
  background: rgba(255,255,255,0.20);
  overflow: hidden;
}
.cf-timer i {
  display: block; height: 100%; width: 100%;
  transform-origin: left center;
  border-radius: 999px;
  background: linear-gradient(90deg, #b39bff 0%, #8b6cf0 100%);
}
/* On the dark end of the scrim now, so it drops the white outline and simply
   goes light - a stroked dark label on a dark ground was unreadable. */
.cf-auto {
  margin: 0;
  font-size: 12px; font-weight: 800; letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #fff;
  opacity: 0.7;
}
/* Once the offer is taken or has run out, the card steps aside rather than
   snapping off - the next level is already loading behind it. */
.clear-flash.leaving .cf-card { animation: cfLeave 0.26s ease-in both; }

@keyframes cfRise {
  from { opacity: 0; transform: translateY(34px) scale(0.9); }
  to   { opacity: 1; transform: none; }
}
@keyframes cfLeave    { to { opacity: 0; transform: translateY(24px) scale(0.94); } }

@media (max-height: 480px) {
  .cf-card { gap: 5px; padding: 10px 14px 11px; }
  .cf-title { font-size: clamp(17px, 4vw, 24px); }
  .cf-stars .star { width: 18px; height: 18px; }
  .cf-stars .star:nth-child(2) { width: 23px; height: 23px; margin-top: -5px; }
  .cf-cash { font-size: clamp(17px, 4vw, 22px); }
  .cf-auto { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .cf-card, .clear-flash.leaving .cf-card { animation: none; }
}

/* ============================ SCREENS ================================ */
.screen {
  position: absolute; inset: 0;
  display: none;
  flex-direction: column;
  align-items: center; justify-content: center;
  padding: calc(16px + var(--safe-t)) calc(16px + var(--safe-r)) calc(16px + var(--safe-b)) calc(16px + var(--safe-l));
  pointer-events: auto;
  overflow-y: auto;
  /* Explicit, because overflow-y:auto on its own makes the OTHER axis auto
     too. The menu's two brand dolls lean in past the edges on purpose, and on
     a 380px phone that left the title screen draggable 30px sideways - the
     first screen of the game, wobbling under the thumb. Screens scroll
     vertically or not at all. */
  overflow-x: hidden;
}
.screen.active { display: flex; }
/* A vignette rather than a flat wash: the panel sits in the bright middle and
   the corners fall away, so the eye lands on the card instead of the tint. */
.screen-dim {
  background: radial-gradient(125% 95% at 50% 44%,
              rgba(30,42,64,0.34) 0%, rgba(14,20,34,0.76) 100%);
  backdrop-filter: blur(3px);
}
.screen-solid { background: #10141f; }

/* Graph paper, like a sketchbook - used by loading and the welcome screen. */
.screen-paper {
  background:
    linear-gradient(var(--grid) 1.2px, transparent 1.2px) 0 0 / 30px 30px,
    linear-gradient(90deg, var(--grid) 1.2px, transparent 1.2px) 0 0 / 30px 30px,
    var(--paper);
}

/* ============================== BRAND ================================ */
.brand {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  margin-bottom: 18px;
  flex: 0 0 auto;
}

.brand-art {
  position: relative;
  width: min(86vw, 380px);
  height: 104px;
  margin-bottom: -6px;
}
.brand-arc {
  position: absolute; left: 10%; top: 10px; width: 72%; height: 80px;
  opacity: 0.95;
}
/* The tumbling doll at the top of the arc, and the little one at its start.
   Both sit clear of the wordmark - the letters paint over anything that
   strays underneath them. */
.brand-doll {
  position: absolute;
  filter: drop-shadow(0 3px 0 rgba(43,58,103,0.16));
}
/* Both drift on their own loop. The rotation lives INSIDE the keyframes -
   a transform in the rule would be overwritten by the animation. */
.brand-doll-a {
  width: 116px; height: 130px; right: -9%; top: -34px;
  animation: tumbleA 3.6s ease-in-out infinite;
}
.brand-doll-b {
  width: 86px; height: 97px; left: -3%; bottom: 4px;
  animation: tumbleB 3.1s ease-in-out infinite;
}
@keyframes tumbleA {
  0%, 100% { transform: rotate(34deg)  translateY(0); }
  50%      { transform: rotate(24deg)  translateY(-13px); }
}
@keyframes tumbleB {
  0%, 100% { transform: rotate(-18deg) translateY(0); }
  50%      { transform: rotate(-8deg)  translateY(-9px); }
}

/* four-point sparkles */
.spark {
  position: absolute;
  width: 20px; height: 20px;
  clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
  animation: twinkle 2.4s ease-in-out infinite;
}
/* Kept above and beside the wordmark; anything below it gets painted over. */
.spark-1 { background: var(--pink);   right: 2%;   top: -12px;  width: 28px; height: 28px; }
.spark-2 { background: var(--yellow); left: 16%;   top: -4px;   width: 20px; height: 20px; animation-delay: .7s; }
.spark-3 { background: var(--mint);   right: 24%;  top: 24px;   width: 26px; height: 26px; animation-delay: 1.4s; }
@keyframes twinkle { 0%,100% { transform: scale(1) rotate(0deg); opacity: .95; } 50% { transform: scale(.7) rotate(45deg); opacity: .55; } }

.wordmark {
  margin: 0;
  display: flex; flex-direction: column; align-items: center;
  line-height: 0.86;
  font-weight: 900;
  letter-spacing: -0.005em;
  font-size: clamp(38px, 11vw, 74px);
  color: #4cb8f5;
  -webkit-text-stroke: 9px var(--ink);
  paint-order: stroke fill;
  filter: drop-shadow(0 5px 0 rgba(43,58,103,0.18));
}
/* ---- the wordmark performs the game --------------------------------------
   The logo acts out its own verb. SLAM rears back, drives down onto DOLL, and
   DOLL takes the hit: its letters squash and flop loose in a ripple before
   settling. SLAM stays rigid throughout because it is the hammer; DOLL is the
   ragdoll, and only the ragdoll is allowed to wobble.

   The cycle is deliberately mostly REST. About two thirds of it is a still
   logo with the faintest float, and the hit is over in under half a second.
   A logo that moves constantly stops reading as a logo and starts reading as
   a loading spinner, and it is on screen behind every menu in the game.

   Base rotations are baked into every keyframe: an animation replaces the
   rule's transform outright, so a rotate() left up in the rule would be
   dropped the moment the animation starts (same trap as the tumbling dolls). */
.wordmark .wm-1 {
  transform: rotate(-2deg);
  transform-origin: 50% 100%;
  animation: wmSlam var(--wm-cycle) cubic-bezier(.32,0,.36,1) infinite;
}
.wordmark .wm-2 {
  transform: rotate(1.5deg) translateX(6px);
}
.wordmark { --wm-cycle: 3.6s; animation: wmBreathe var(--wm-cycle) linear infinite; }
.wm-ch { display: inline-block; }

/* SLAM: anticipation up and back, then a fast heavy drive down, squashing
   wide and flat on contact. */
@keyframes wmSlam {
  0%, 55%   { transform: rotate(-2deg)   translateY(0)     scale(1, 1); }
  62%       { transform: rotate(-5deg)   translateY(-17px) scale(0.94, 1.10); }
  68%       { transform: rotate(-0.5deg) translateY(10px)  scale(1.12, 0.85); }
  74%       { transform: rotate(-2.6deg) translateY(-4px)  scale(0.98, 1.04); }
  80%, 100% { transform: rotate(-2deg)   translateY(0)     scale(1, 1); }
}

/* DOLL: hit from above, so each letter hangs from its top edge. The per-letter
   delay is what sells it - the ripple across the word is the difference
   between a floppy doll and a rigid block that happens to scale. */
.wordmark .wm-2 .wm-ch {
  transform-origin: 50% 0%;
  animation: wmFlop var(--wm-cycle) cubic-bezier(.22,1.35,.4,1) infinite;
  animation-delay: calc(var(--i) * 48ms);
}
@keyframes wmFlop {
  0%, 64%   { transform: translateY(0)    rotate(0deg)    scale(1, 1); }
  70%       { transform: translateY(8px)  rotate(0deg)    scale(1.16, 0.78); }
  77%       { transform: translateY(-6px) rotate(-5deg)   scale(0.93, 1.09); }
  84%       { transform: translateY(3px)  rotate(3.5deg)  scale(1.04, 0.97); }
  90%       { transform: translateY(-1px) rotate(-1.5deg) scale(0.99, 1.01); }
  95%, 100% { transform: translateY(0)    rotate(0deg)    scale(1, 1); }
}

/* Two jobs in one track on the whole lockup, because both want `transform`
   and the last animation to set it would win.

   The first two thirds are a slow breath. Without it the logo is genuinely
   motionless for seconds at a time between hits, which is the static look
   this whole thing exists to kill - the slam should be the moment the eye
   catches, not the only moment anything moves.

   The rest is the impact: four frames, linear, barely two pixels. Punctuation
   on the hit, not a move of its own. */
@keyframes wmBreathe {
  0%   { transform: translate(0, 0);     animation-timing-function: ease-in-out; }
  32%  { transform: translate(0, -5px);  animation-timing-function: ease-in-out; }
  64%  { transform: translate(0, 0);     animation-timing-function: linear; }
  68%  { transform: translate(-3px, 2px); }
  70%  { transform: translate(3px, -2px); }
  72%  { transform: translate(-2px, 1px); }
  76%  { transform: translate(1px, 0);   animation-timing-function: ease-out; }
  100% { transform: translate(0, 0); }
}

.brand-lg .wordmark { font-size: clamp(44px, 13vw, 88px); }

@media (prefers-reduced-motion: reduce) {
  .wordmark, .wordmark .wm-1, .wordmark .wm-2 .wm-ch { animation: none; }
}

/* ============================= LOADING =============================== */
.load-box { width: min(80vw, 380px); text-align: center; margin-top: 36px; }
.load-bar {
  height: 26px;
  background: #fff;
  border: 4px solid var(--ink);
  border-radius: 999px;
  padding: 4px;
  box-shadow: 0 4px 0 rgba(43,58,103,0.2);
}
.load-fill {
  height: 100%; width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--pink), #ffb3dc);
  transition: width 0.25s ease-out;
}
.load-label {
  margin: 14px 0 0;
  font-size: clamp(16px, 3.4vw, 24px);
  font-weight: 900;
  letter-spacing: 0.34em;
  color: var(--ink-soft);
}
.build-tag {
  position: absolute; left: 10px; bottom: 8px;
  margin: 0; font-size: 10px; color: var(--ink-soft); opacity: 0.5;
}

/* ============================ WELCOME ================================ */
.corner-btn {
  pointer-events: auto;
  position: absolute;
  top: calc(14px + var(--safe-t)); left: calc(14px + var(--safe-l));
  width: 52px; height: 52px; padding: 12px;
  border-radius: var(--r-sm);
  background: #fff;
  border: 4px solid var(--ink);
  box-shadow: 0 5px 0 rgba(43,58,103,0.25);
  color: var(--coral);
  cursor: pointer;
}
.corner-btn:active { transform: translateY(4px); box-shadow: 0 1px 0 rgba(43,58,103,0.25); }

/* Cash sits where it sits in-game: a pill in the top-right corner, level with
   the sound button beside it.

   It takes the sound button's `top` AND its height rather than being nudged
   into place with a hand-tuned offset. Matching the height is what does the
   work: two border-box elements of equal height sharing a `top` have their
   centres on the same line by construction, so they stay aligned at every
   breakpoint - including the two narrow ones below, where the button shrinks
   and a tuned offset would quietly drift out of true again.

   `right` matches the button's too, so the clearance between them lives in
   exactly one number (margin-right) instead of being split across two. */
.menu-cash {
  position: absolute;
  top: calc(10px + var(--safe-t)); right: calc(12px + var(--safe-r));
  margin-right: 58px;              /* 46px button + 12px gap */
  height: 46px;
  font-size: clamp(14px, 3vw, 18px);
  padding: 0 15px;
}
/* The button shrinks on narrow screens, so the pill shrinks with it. These
   live HERE, after the base rule, not up in the blocks that resize the button:
   a media query carries no extra specificity, so one placed earlier in the
   file loses to the plain .menu-cash rule above and silently does nothing. */
@media (max-width: 460px) {
  .menu-cash { height: 40px; margin-right: 50px; }   /* 40 + 10 */
}
@media (max-width: 340px) {
  .menu-cash { height: 40px; margin-right: 48px; }   /* 40 + 8 */
  /* The reward button carries the most in one row - glyph, label, pips and the
     AD stamp - and on a 280px fold its contents clipped by a couple of pixels. */
  .btn-reward { padding-left: 9px; padding-right: 9px; }
  .rv-ico { width: 24px; height: 18px; }
  .ad-badge { font-size: 9px; padding: 2px 4px; }
}

/* Level rides on a plate under the play button - it belongs to "what you are
   about to play", not in a row of identical boxes off to one side. */
.play-stack { position: relative; display: flex; flex-direction: column; align-items: center; }

.play-plate {
  /* Clear of the button rather than tucked under it: the button now carries a
     bloom and a pulse ring, and both were eating the level readout. */
  margin-top: 14px;
  position: relative;
  z-index: 2;
  background: #fff;
  border: 4px solid var(--ink);
  border-radius: 999px;
  box-shadow: 0 4px 0 rgba(43,58,103,0.22);
  padding: 4px 6px;
  font-size: clamp(12px, 2.7vw, 15px);
  font-weight: 800;
  color: var(--ink-soft);
  letter-spacing: 0.06em;
  white-space: nowrap;
  /* A row now, not a label: an arrow either side of the readout. */
  display: inline-flex; align-items: center; gap: 2px;
}
.play-plate b { color: var(--ink); font-size: 1.35em; font-weight: 900; margin-left: 4px; }
.plate-of { color: var(--ink-soft); opacity: 0.6; font-size: 0.95em; }
.plate-mid {
  display: inline-flex; flex-direction: column; align-items: center;
  gap: 1px; padding: 0 8px; min-width: 106px;
}
.plate-line { display: inline-flex; align-items: baseline; }

/* --- the Skip Intro plate ---------------------------------------------
   Deliberately the SAME shell as the level plate rather than a new control:
   the two sit one above the other and any difference in height, radius or
   border weight between them reads as a mistake. It only adds the things a
   <button> needs that a <div> did not, and a switch where the steppers are.

   The 40px-tall switch is what holds the row to the same height as the one
   above, where the steppers do that job. */
/* Two classes deep on purpose. The short-viewport blocks lower down re-set
   `.play-plate`'s margin-top, including to a negative value that tucks the
   level plate under the play button - and this row must keep its own gap from
   the plate above it at every height. A plain `.plate-toggle` loses to those,
   `.play-plate.plate-toggle` does not. */
.play-plate.plate-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  width: 100%;
  margin-top: 8px;
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.plate-toggle:active { transform: translateY(1px); }
.plate-toggle:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
/* The label is the wide half here, so it gets the room the steppers took.
   40px tall because that is what the steppers are: it is the inner height that
   makes both plates come out the same, not a number set on the plate. */
.plate-mid-wide {
  min-width: 0; flex: 1 1 auto;
  align-items: flex-start; justify-content: center;
  min-height: 40px;
  padding-left: 10px;
}

.plate-switch {
  flex: 0 0 auto;
  width: 52px; height: 30px;
  margin-right: 5px;
  border-radius: 999px;
  background: rgba(43,58,103,0.20);
  box-shadow: inset 0 2px 0 rgba(43,58,103,0.12);
  transition: background 0.16s;
  display: block;
  position: relative;
}
.plate-switch i {
  position: absolute;
  top: 3px; left: 3px;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 4px rgba(20,32,66,0.35);
  transition: transform 0.16s;
}
/* The darker green, not --green: the knob is white and sits on top of it. */
.plate-toggle[aria-checked="true"] .plate-switch { background: var(--green-dk); }
.plate-toggle[aria-checked="true"] .plate-switch i { transform: translateX(22px); }
/* Say it is off by dimming the words, not only the switch - the switch is
   small and this row is read at a glance on the way to the play button. */
.plate-toggle[aria-checked="false"] .plate-mid-wide { opacity: 0.55; }

/* The steppers. 40px square so they clear the tap-target floor even though
   the arrow inside them is small, and flat rather than buttoned - they sit
   inside the plate and a second border would make it look like a toolbar. */
.plate-step {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; margin: 0;
  background: none; border: 0; border-radius: 999px;
  cursor: pointer;
  color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
.plate-step .ico { width: 20px; height: 20px; stroke: currentColor; fill: none; }
.plate-step:hover:not(:disabled) { background: rgba(43,58,103,0.09); }
.plate-step:active:not(:disabled) { transform: translateY(1px); }
.plate-step:disabled { opacity: 0.22; cursor: default; }
.plate-step:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

/* Last rating for the selected level. Tiny, because it is a reminder rather
   than a score - but it is what turns the stepper from navigation into a
   reason to go back. */
.plate-stars { display: inline-flex; gap: 3px; }
.plate-stars .ps {
  width: 9px; height: 9px;
  background: rgba(43,58,103,0.20);
  clip-path: polygon(50% 0%, 62% 36%, 100% 38%, 70% 60%, 81% 98%, 50% 75%, 19% 98%, 30% 60%, 0% 38%, 38% 36%);
}
.plate-stars .ps.on { background: linear-gradient(180deg, #ffe27a 0%, #ffcb2e 55%, #f5a623 100%); }

.best-line {
  margin: 14px 0 18px;
  font-size: clamp(11px, 2.5vw, 13px);
  font-weight: 800;
  letter-spacing: 0.09em;
  color: var(--ink-soft);
}
.best-line b { color: var(--ink); font-size: 1.35em; margin-left: 5px; }

/* the big yellow play button */
/* The single most important pixel area in the game.
   It has to be the biggest thing on the menu and it has to move. A player who
   opens the game and sees one large breathing button knows what to do without
   reading a word, and the idle animation is not decoration: a still button on
   a still menu is the most common way a first session ends at the menu. */
.play-btn {
  pointer-events: auto;
  width: clamp(150px, 38vw, 196px);
  height: clamp(126px, 32vw, 164px);
  padding: clamp(28px, 7vw, 38px);
  border-radius: 34px;
  background: var(--yellow);
  border: 6px solid var(--ink);
  /* The second shadow is a coloured bloom, not a drop shadow - it makes the
     button the brightest thing on screen even against the pale sky. */
  box-shadow: 0 10px 0 var(--yellow-dk), 0 20px 38px -12px rgba(232,168,20,0.66);
  color: #fff;
  cursor: pointer;
  display: grid; place-items: center;
  transition: transform .07s, box-shadow .07s;
  transform-origin: center bottom;
  animation: playIdle 1.75s ease-in-out infinite;
  position: relative;        /* the pulse ring anchors to this box */
  flex: 0 0 auto;            /* never let a short viewport squash it */
}
/* Squash and stretch rather than a plain scale: it lands like a body, and the
   hold at the bottom of the cycle is what stops it reading as a throb. */
@keyframes playIdle {
  0%, 62%, 100% { transform: scale(1, 1) translateY(0); }
  74%           { transform: scale(1.055, 0.945) translateY(3px); }
  86%           { transform: scale(0.975, 1.05) translateY(-7px); }
  94%           { transform: scale(1.01, 0.99) translateY(0); }
}
/* A ring that pulses out from under it, so the eye is pulled there even in
   peripheral vision. Purely decorative, so it never eats a tap.

   Anchored to the BUTTON with inset:0 rather than drawn at hand-computed
   coordinates on the stack. The button is a flex item, and in a short viewport
   (landscape phones) the column shrinks it below its stated height; the ring
   was not a flex item, kept its full size, and ended up a fat halo sitting low
   and to one side of a squashed button. inset:0 cannot drift. */
.play-btn::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  border: 5px solid var(--yellow);
  pointer-events: none;
  animation: playRing 1.75s ease-out infinite;
}
@keyframes playRing {
  0%, 55% { opacity: 0; transform: scale(1); }
  70%     { opacity: 0.55; transform: scale(1); }
  100%    { opacity: 0; transform: scale(1.42); }
}
.play-btn:active {
  transform: translateY(6px) scale(0.97);
  box-shadow: 0 2px 0 var(--yellow-dk);
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .play-btn { animation: none; }
  .play-btn::after { display: none; }
}

/* Landscape phones. The menu is a tall column and 360px of height is not
   enough for it at full size - it overflowed and started scrolling, which on
   a main menu reads as broken. Everything shrinks together so the proportions
   hold; the play button stays the biggest thing on screen either way. */
@media (max-height: 440px) {
  /* The screen's own 16px top and bottom padding is a third of the shortfall
     on its own at this height. */
  .screen { padding-top: calc(8px + var(--safe-t)); padding-bottom: calc(8px + var(--safe-b)); }
  .brand { margin-bottom: 4px; }
  .brand-art { height: 54px; }
  .brand-doll-a { width: 74px; height: 83px; top: -18px; }
  .brand-doll-b { width: 56px; height: 63px; }
  .wordmark { font-size: clamp(26px, 7vw, 40px); -webkit-text-stroke-width: 6px; }
  .brand-lg .wordmark { font-size: clamp(28px, 8vw, 46px); }
  .play-btn {
    width: clamp(104px, 26vw, 132px);
    height: clamp(84px, 21vw, 108px);
    padding: clamp(18px, 5vw, 24px);
  }
  .play-plate { margin-top: 8px; padding: 4px 15px; }
  .best-line { margin: 5px 0 4px; }
  .tile-btn { width: 48px; height: 48px; padding: 11px; }
}
.play-btn .ico-play { stroke: var(--ink); }
.play-btn:focus-visible, .corner-btn:focus-visible, .tile-btn:focus-visible { outline: 4px solid var(--blue); outline-offset: 4px; }

.tile-row { display: flex; gap: 12px; }
.tile-btn {
  pointer-events: auto;
  width: 62px; height: 62px; padding: 13px;
  border-radius: var(--r-sm);
  background: var(--coral);
  border: 4px solid var(--ink);
  box-shadow: 0 6px 0 var(--coral-dk);
  color: #fff;
  cursor: pointer;
  display: grid; place-items: center;
}
.tile-btn:active { transform: translateY(5px); box-shadow: 0 1px 0 var(--coral-dk); }
.tile-btn .ico-shirt { fill: #fff; stroke: var(--ink); }
.tile-btn .ico-sound { fill: #fff; stroke: #fff; }
/* Ink, not white. The tile is coral, and white on coral measures 2.5:1 - fine
   for the shirt glyph next to it, which reads as a shape, but not for two
   letters that have to be read as letters. */
/* A touch darker than --ink: on coral the standard ink lands at 4.4:1, just
   under the bar, and this clears it at 5.6:1. */
.ico-lang { font-size: 17px; font-weight: 900; letter-spacing: 0.02em; color: #1e2a4d; }

/* Muted state. The speaker drops its waves and takes a cross where they were.
   The note has nothing to remove, so it gets struck through instead - and the
   strike is drawn twice, once fat in the button's own white and once thin in
   the ink on top. That white pass punches a gap through the note underneath,
   which is what stops a beamed pair plus a diagonal from collapsing into an
   unreadable scribble at 40px. */
.snd-slash { display: none; }
.muted .snd-wave { display: none; }
.muted .snd-slash { display: block; }
.audio-fixed .ico-music .slash-cut { stroke: #fff; }
/* Off is a real state, not a styling flourish: dim the whole control so it is
   obvious at arm's length which of the two is switched off. Grey enough to
   read as disabled beside the live violet, dark enough to clear 3:1 on the
   white button - the obvious lighter grey lands at 3.02:1, which is inside the
   measurement noise of passing. */
.audio-fixed .icon-btn.muted { color: #7c84ab; }


/* ============================= PANELS ================================ */
/* --- panels ------------------------------------------------------------
   Deliberately NOT the heavy-black-outline cartoon style the canvas art
   uses. Outlines that thick on UI read as a template; depth here comes from
   layered soft shadows and a hairline, the way shipped mobile games do it. */
.panel {
  width: min(100%, 400px);
  background: linear-gradient(180deg, #ffffff 0%, #f7fafd 100%);
  /* Under the gradient, which is opaque and hides it. It exists so the panel
     has a real background-color: a gradient alone computes as transparent, and
     anything measuring text contrast walks up looking for a colour, misses
     every panel in the game and lands on the sky behind them. */
  background-color: #ffffff;
  border: 0;
  border-radius: 26px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.9) inset,
    0 0 0 1px rgba(43,58,103,0.10),
    0 18px 40px -12px rgba(20,32,66,0.42),
    0 4px 12px -4px rgba(20,32,66,0.20);
  padding: 26px 22px 20px;
  text-align: center;
  animation: pop 0.26s cubic-bezier(.2,1.25,.4,1);
  max-height: 100%;
  overflow-y: auto;
}
/* The shop. Its two actions are pinned and only the grid scrolls.
   With 18 skins the grid runs to four rows, and previously FREE CASH and BACK
   sat underneath all of it - leaving the shop meant scrolling to the bottom
   first. The way out of a screen should never be the hardest thing on it. */
.panel-wide {
  width: min(100%, 560px);
  display: flex; flex-direction: column;
  overflow: hidden;              /* the grid owns the scrolling now */
}
.panel-wide .skin-grid {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  min-height: 0;                 /* or the grid refuses to shrink and scroll */
  margin-bottom: 12px;
}
.panel-wide > .btn { flex: 0 0 auto; }
@keyframes pop {
  from { transform: scale(0.94) translateY(10px); opacity: 0; }
  to   { transform: scale(1) translateY(0); opacity: 1; }
}

/* A coloured header band instead of a title floating on white. It gives the
   panel a shape, carries the win/lose state in colour rather than in wording,
   and stops the whole thing reading like a form. */
.panel-headed { padding: 0; overflow: hidden; }

/* The band's bottom edge is a shallow arc rather than a straight cut. It is
   one line of border-radius and it is most of the difference between "card
   with a coloured strip" and something that looks designed. */
.panel-head {
  padding: 22px 22px 24px;
  text-align: center;
  color: #fff;
  position: relative;
  border-radius: 0 0 50% 50% / 0 0 26px 26px;
  box-shadow: 0 9px 20px -10px rgba(20,32,66,0.55);
}
/* Faint rays behind the title - reads as celebration without a single asset. */
.panel-head::before {
  content: ''; position: absolute; inset: 0;
  background: repeating-conic-gradient(from 0deg at 50% 22%,
              rgba(255,255,255,0.10) 0deg 9deg, rgba(255,255,255,0) 9deg 18deg);
  -webkit-mask-image: radial-gradient(70% 78% at 50% 24%, #000 0%, transparent 72%);
          mask-image: radial-gradient(70% 78% at 50% 24%, #000 0%, transparent 72%);
  border-radius: inherit;
  pointer-events: none;
}
.panel-head > * { position: relative; z-index: 1; }

.head-win  { background: linear-gradient(180deg, #3ddb8a 0%, #22b56a 62%, #1a9d5b 100%); }
.head-fail { background: linear-gradient(180deg, #ff7b88 0%, #e8505f 62%, #d13f50 100%); }
.panel-body { padding: 16px 22px 18px; }

/* White is for a title sitting ON a coloured band. A plain panel - pause, shop
   - has a white body, so it keeps the ink colour or the heading disappears. */
.panel h2 {
  margin: 0 0 4px;
  line-height: 1.05;
  font-size: clamp(21px, 5.0vw, 30px);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--ink);
}
.panel-head h2 {
  color: #fff;
  text-shadow: 0 2px 0 rgba(20,32,66,0.18);
}
.panel-head .subtitle {
  margin: 0;
  color: rgba(255,255,255,0.85);
  font-weight: 700; font-size: 11.5px;
  letter-spacing: 0.16em; text-transform: uppercase; opacity: 1;
}

/* --- star rating -------------------------------------------------------
   Perfect landings were tracked but never rewarded. Across 333 levels a
   three-star chase is what turns "cleared" into "cleared properly", and it
   costs one number in the save file. */
.stars {
  display: flex; justify-content: center; align-items: center;
  gap: 10px; margin-top: 14px; margin-bottom: -2px;
}
.star {
  width: 30px; height: 30px;
  /* Unearned stars are a socket punched into the band, not a pale copy of an
     earned one - at a glance you count what you got, not what is missing. */
  background: rgba(10,36,24,0.26);
  box-shadow: 0 1px 0 rgba(255,255,255,0.22);
  clip-path: polygon(50% 0%, 62% 36%, 100% 38%, 70% 60%, 81% 98%, 50% 75%, 19% 98%, 30% 60%, 0% 38%, 38% 36%);
  transform: scale(0.84);
}
.head-fail .star { background: rgba(58,12,20,0.26); }
.star:nth-child(2) { width: 38px; height: 38px; margin-top: -8px; }
.star.on {
  background: linear-gradient(180deg, #ffe27a 0%, #ffcb2e 55%, #f5a623 100%);
  filter:
    drop-shadow(0 0 9px rgba(255,214,80,0.75))
    drop-shadow(0 2px 3px rgba(20,32,66,0.30));
  box-shadow: none;
  animation: starPop 0.44s cubic-bezier(.2,1.7,.4,1) both;
}
/* The stagger is driven from JS so each star can carry its own sound and
   confetti puff; no CSS delay, or the two offsets compound. */
@keyframes starPop {
  0%   { transform: scale(0.2) rotate(-40deg); opacity: 0; }
  70%  { transform: scale(1.2) rotate(6deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}

/* ======================= LEVEL CLEAR SCENE ===========================
   A full-screen celebration rather than a card. The confetti canvas sits
   behind everything, the character dances on the left of a two-column row,
   and the numbers and buttons run down the right. Below 720px the row
   collapses and the dancer shrinks to a band above the numbers. */
.screen-win {
  background: radial-gradient(120% 100% at 50% 30%,
              rgba(26,38,64,0.62) 0%, rgba(10,15,28,0.90) 100%);
  backdrop-filter: blur(5px);
  padding: 0;
}

.win-confetti {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}

.win-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: min(100%, 720px);
  max-height: 100%;
  padding: 14px 16px;
  overflow-y: auto;
  animation: winIn 0.4s cubic-bezier(.2,1.2,.35,1) both;
}
@keyframes winIn {
  from { transform: translateY(18px) scale(0.96); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* Tilted banner, like a sticker slapped on the screen. */
.win-banner {
  transform: rotate(-2.2deg);
  background: linear-gradient(180deg, #3ddb8a, #1fae65);
  background-color: #2ec477;      /* see the note on .panel */
  border-radius: 14px;
  padding: 9px 30px 11px;
  box-shadow: 0 10px 22px -10px rgba(10,40,24,0.8), 0 2px 0 rgba(255,255,255,0.4) inset;
  text-align: center;
}
.win-banner h2 {
  margin: 0;
  color: #fff;
  font-size: clamp(23px, 5.6vw, 40px);
  font-weight: 900;
  letter-spacing: 0.05em;
  line-height: 1.02;
  text-shadow: 0 3px 0 rgba(12,60,36,0.34);
}
/* Deep green, not white.
   White measured 2.1:1 on this banner - the greens are bright enough to carry
   a 40px title in white with a drop shadow behind it, but nothing like enough
   for an 11.5px tracked-out label, which is the size that actually needs the
   help. The dark ink clears 5.7:1 and reads as the secondary line it is. */
.win-sub {
  margin: 2px 0 0;
  color: #06381f;
  font-weight: 800; font-size: 11.5px;
  letter-spacing: 0.16em; text-transform: uppercase;
}
.win-sub b { font-size: 15px; }
/* 0.65 took the "/333" back under the bar on its own. */
.win-sub .lv-of { opacity: 0.8; }

/* Bigger than in a header band, and the socket has to be scoped to :not(.on)
   or it wins on specificity and greys out the earned stars too. */
.screen-win .stars { margin-top: 12px; gap: 14px; }
.screen-win .star:not(.on) { background: rgba(255,255,255,0.19); box-shadow: none; }
.screen-win .star { width: 42px; height: 42px; }
.screen-win .star:nth-child(2) { width: 54px; height: 54px; margin-top: -10px; }

.win-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin-top: 6px;
}

.win-doll {
  flex: 0 0 38%;
  width: 38%;
  height: auto;
  aspect-ratio: 5 / 6;
  pointer-events: none;
}

.win-side {
  flex: 1 1 auto;
  min-width: 0;
  background: linear-gradient(180deg, #ffffff, #f5f9fd);
  background-color: #fafcfe;      /* see the note on .panel */
  border-radius: 22px;
  padding: 14px 16px 14px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.9) inset,
    0 18px 40px -14px rgba(8,16,36,0.7);
}
.win-side .score-list { margin-bottom: 10px; }

/* The hero number. Cash is the currency the meta layer runs on, so it is the
   one figure that gets the size. */
.win-hero {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  background: linear-gradient(180deg, #eafbf1, #d8f5e5);
  background-color: #e1f8eb;      /* see the note on .panel */
  border-radius: 14px;
  padding: 8px 14px;
  margin-bottom: 6px;
}
.win-hero-label {
  font-weight: 800; font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
  /* One shade down from #2b8055, which measured 4.36:1 on the mint plate -
     close enough to pass by eye, under the bar by measurement. */
  color: #256f4a;
}
.win-hero-value { display: inline-flex; align-items: center; gap: 8px; }
.win-hero-value b {
  font-size: clamp(26px, 5.4vw, 38px);
  font-weight: 900;
  color: #128f4f;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.cash-ico.big { width: 28px; height: 18px; border-width: 2.5px; }

.win-best {
  margin: 0 2px 12px;
  display: flex; justify-content: space-between;
  font-size: 12px; font-weight: 700;
  color: var(--ink-soft); opacity: 0.72;
}

@media (max-width: 720px) {
  .win-row { flex-direction: column; gap: 4px; }
  .win-doll { flex: 0 0 auto; width: 42%; max-width: 190px; }
  .win-side { width: 100%; }
}
@media (max-height: 560px) {
  .win-doll { display: none; }
  .win-banner { padding: 6px 22px 8px; }
  .screen-win .stars { margin-top: 6px; }
}

/* --- "how far you got" bar, in the game-over band ---------------------- */
.run-bar {
  height: 9px;
  margin: 14px auto 6px;
  width: 76%;
  border-radius: 99px;
  background: rgba(52,12,20,0.30);
  box-shadow: 0 1px 0 rgba(255,255,255,0.20);
  overflow: hidden;
}
.run-bar > i {
  display: block; height: 100%; width: 0;
  border-radius: 99px;
  background: linear-gradient(90deg, #ffd98a, #ffe9c2);
  box-shadow: 0 0 8px rgba(255,220,150,0.6);
  transition: width 0.65s cubic-bezier(.2,.9,.3,1) 0.15s;
}
.run-bar-cap {
  margin: 0;
  font-weight: 800; font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255,255,255,0.9);
}
.run-bar-cap span:first-child { font-size: 15px; }

/* Rows on hairline dividers rather than inside a boxed sub-panel: one less
   frame, and the numbers carry the hierarchy instead of the border. */
.score-list {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin: 0 2px 18px;
  text-align: left;
}
.score-line {
  display: flex; justify-content: space-between; align-items: center;
  font-weight: 600; font-size: 13.5px;
  padding: 9px 2px;
  color: var(--ink-soft);
  border-bottom: 1px solid rgba(43,58,103,0.09);
}
.score-line:last-child { border-bottom: 0; }
.score-line b {
  color: var(--ink); font-size: 18px; font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.score-line.total { padding-top: 4px; }
.score-line.total b { color: var(--blue); font-size: 30px; font-weight: 800; line-height: 1; }
.score-line.cash b { color: #1aa35c; }
.score-line.small { font-size: 12px; opacity: 0.6; padding: 7px 2px 2px; }
.score-line.small b { font-size: 14px; font-weight: 700; }

/* ============================ BUTTONS ================================ */
/* --- buttons -----------------------------------------------------------
   Solid fills with a top-light gradient and a tight coloured rim, pressing
   1-2px rather than 5. Thick black outlines and a 5px hard drop shadow are
   the tell of the generic cartoon UI kit; this keeps the weight but not the
   costume. */
.btn {
  pointer-events: auto;
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%;
  font-family: inherit;
  font-weight: 800;
  font-size: 15.5px;
  letter-spacing: 0.07em;
  color: #fff;
  background-image: linear-gradient(180deg, rgba(255,255,255,0.22), rgba(255,255,255,0) 55%);
  background-color: var(--blue);
  border: 0;
  border-radius: 16px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.35) inset,
    0 -2px 0 rgba(0,0,0,0.14) inset,
    0 4px 12px -2px rgba(20,32,66,0.30);
  padding: 15px 16px;
  cursor: pointer;
  margin-bottom: 10px;
  transition: transform .08s ease, box-shadow .08s ease, filter .08s ease;
}
.btn:hover:not(:disabled) { filter: brightness(1.06); }
.btn:active:not(:disabled) {
  transform: translateY(2px);
  box-shadow: 0 -2px 0 rgba(0,0,0,0.14) inset, 0 1px 4px -1px rgba(20,32,66,0.35);
}
.btn:disabled { opacity: 0.45; cursor: default; }
.btn:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }

/* --- press feedback ---------------------------------------------------
   Driven from JS on pointerdown/up rather than :active, so it fires on the
   frame the finger lands (touch delays :active by up to 100ms) and so the
   release can overshoot on the way back instead of snapping. */
.pressable { transition: transform .07s ease, box-shadow .07s ease, filter .07s ease; }
.pressable.pressing {
  transform: translateY(2px) scale(0.972);
  filter: brightness(0.96);
}
.pressable.popped { animation: btnPop 0.24s cubic-bezier(.2,1.8,.4,1); }
@keyframes btnPop {
  0%   { transform: translateY(2px) scale(0.972); }
  55%  { transform: translateY(-1px) scale(1.035); }
  100% { transform: none; }
}
/* Refused: a short horizontal shake, the standard "no" in a game UI. */
.pressable.denied { animation: btnDeny 0.34s ease; }
@keyframes btnDeny {
  0%, 100% { transform: none; }
  15% { transform: translateX(-6px) rotate(-1deg); }
  38% { transform: translateX(5px) rotate(1deg); }
  62% { transform: translateX(-3px); }
  84% { transform: translateX(2px); }
}

.btn-big { font-size: clamp(17px, 4.2vw, 20px); padding: 18px 16px; }

/* Try Again while it counts down. Kept full-opacity rather than greyed: it is
   arriving, not unavailable, and the digit says so. */
.btn.counting {
  opacity: 1;
  background-color: #9fb3c9;
  font-size: clamp(22px, 5.5vw, 30px);
  font-variant-numeric: tabular-nums;
  cursor: default;
}
/* ...and the rewarded continue is the live option in the meantime. */
.btn-reward.urging { animation: urge 1.05s ease-in-out infinite; }
@keyframes urge {
  0%, 100% { transform: none; box-shadow: 0 1px 0 rgba(255,255,255,0.35) inset,
                                          0 -2px 0 rgba(0,0,0,0.14) inset,
                                          0 4px 12px -2px rgba(20,32,66,0.30); }
  50%      { transform: scale(1.028); box-shadow: 0 1px 0 rgba(255,255,255,0.35) inset,
                                          0 -2px 0 rgba(0,0,0,0.14) inset,
                                          0 6px 20px -2px rgba(139,108,240,0.65); }
}

.btn-primary   { background-color: #2ec46f; }
/* The clear card's free continue. min-height, not padding: the rewarded
   button below it carries a video glyph and an AD badge that set its own
   height, so matching by padding alone left the free one 5px shorter than
   the rule allows. */
.cf-next {
  margin-bottom: 7px;
  padding: 13px 16px;
  font-size: 15.5px;
  min-height: 52px;
}
.btn-secondary {
  background-color: #eef2f8; color: var(--ink);
  box-shadow: 0 0 0 1px rgba(43,58,103,0.10) inset, 0 3px 8px -3px rgba(20,32,66,0.25);
}
.btn-ghost { background-color: var(--panel-2); color: var(--ink); }

.btn-buy { background-color: var(--cash); color: #fff; }
/* Out of reach. A flat grayscale filter made these read as broken controls;
   they are not broken, they are priced. A quiet outlined pill says "later"
   where a grey slab says "disabled". */
.btn-buy.too-poor {
  filter: none;
  background: rgba(255,255,255,0.05);
  border-color: rgba(255,255,255,0.16);
  box-shadow: none;
  color: #9fb0d8;
}
.btn-buy.too-poor .cash-ico { opacity: 0.6; }

/* Equipped is a STATE, not an action, and it was wearing the same solid green
   as five buy buttons beside it - so the one card you are actually using read
   as just another price. It now borrows the card's own tier colour and sits
   flat, which also stops the grid reading as a wall of green. */
.btn-equipped {
  background: rgba(255,255,255,0.07) !important;
  border-color: var(--tier, #2ec46f) !important;
  color: var(--tier, #fff) !important;
  box-shadow: none !important;
  opacity: 1 !important;
  letter-spacing: 0.1em;
}

.btn-row { display: flex; gap: 9px; }
.icon-only { width: 56px; flex: 0 0 56px; padding: 12px 16px; }
.icon-only .ico-sound { fill: var(--ink); stroke: var(--ink); }

/* --- rewarded-video buttons -----------------------------------------
   Every one of these opens an ad, so every one carries the video glyph and
   an AD badge. A rewarded button that looks like a normal button is the
   fastest way to lose a player's trust. */
/* A shade deeper than the old #8b6cf0. Its label is 15px, below the size where
   the relaxed large-text contrast rule applies, and white measured 3.8:1 on
   the lighter purple. This clears 4.5:1 and still reads as the same colour. */
.btn-reward {
  background-color: #7a57e8;
  padding-right: 12px;
}
.rv-ico {
  width: 30px; height: 22px; flex: 0 0 auto;
  fill: #fff; stroke: #fff;
}
.rv-text { flex: 1 1 auto; text-align: center; }

/* Continues left, as pips rather than a number: nothing to translate, and a
   depleting row reads as a limited resource at a glance. Empty while the cap
   does not apply, so the early levels show no meter at all. */
.rv-pips { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.rv-pips i {
  width: 7px; height: 7px; border-radius: 50%;
  background: rgba(255,255,255,0.92);
  box-shadow: 0 0 0 1px rgba(0,0,0,0.12);
}
.rv-pips i.used { background: rgba(255,255,255,0.20); box-shadow: none; }
/* Dark plate rather than a white wash. At 10px the old translucent-white badge
   measured 2.5:1 against the button under it; the same white letters on a dark
   plate clear 4.5:1 and the badge reads as a stamp rather than a smudge. */
.ad-badge {
  flex: 0 0 auto;
  font-size: 10px; font-weight: 900; letter-spacing: 0.08em;
  color: #fff;
  background: rgba(20,14,48,0.42);
  border: 2px solid rgba(255,255,255,0.42);
  border-radius: 6px;
  padding: 2px 6px;
  line-height: 1.2;
}

/* ============================== SHOP =================================
   The one screen in the game that is deliberately DARK.

   Everywhere else a white panel is right: it is a cartoon game and the UI
   should feel like paper. The shop is the exception, and the reason is the
   merchandise. Two thirds of this catalogue is made of light - auras, embers,
   arcs, orbits, prisms - and light is only light against something dark. On a
   white card the flame aura was a pale smudge and the 34,000 legendary looked
   like the same object as the free starter, which threw away the entire
   difference the price was supposed to be buying.

   So: a deep panel, each skin in its own lit display case, and a rarity plate
   under the art. Everything else about the screen - the two sections, the
   goal bar, the two-currency wallet, the pinned actions - is unchanged. The
   scoping is on #screen-shop so the dark treatment cannot leak into any other
   panel; the id also outranks the .panel rules further up this file, which is
   what makes the override work without a single !important. */
.shop-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* min-width:0 because a flex item will not shrink below its own text without
   it: on a 320px phone the Russian title "СКИНЫ" is 8px wider than the English
   one and that was enough to shove the cash pill out through the side of the
   panel, where overflow:hidden quietly sliced the last digit off the player's
   balance. The title gives way now; the ellipsis is the backstop. */
.shop-head h2 {
  margin: 0; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shop-wallet { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; }
/* ...and rather than truncate a five-letter word, the smallest phones simply
   set it smaller. Both currencies stay whole, which is the point of the row. */
@media (max-width: 340px) {
  #screen-shop .shop-head h2 { font-size: 18px; letter-spacing: 0.04em; }
  #screen-shop .shop-wallet { gap: 5px; }
  #screen-shop .shop-wallet .pill { padding: 4px 9px; gap: 5px; }
}
.pill-stars { color: #8a5e0f; }   /* #a9761a measured 3.9:1 on the pill */

/* ---- the case ---------------------------------------------------------- */
#screen-shop .panel-wide {
  background:
    /* a soft violet wash spilling in from above the title, so the panel has a
       light source rather than being a flat dark rectangle */
    radial-gradient(118% 62% at 50% -14%, rgba(126,92,236,0.38) 0%, rgba(126,92,236,0) 64%),
    linear-gradient(180deg, #26325e 0%, #171f45 52%, #0f1431 100%);
  /* Redundant to the eye - the gradient above is opaque and covers it - but
     it is what stops the panel computing as background-color: transparent.
     Anything measuring contrast against this surface (the audit tool, a
     browser's own accessibility inspector) walks up looking for a real colour
     and would otherwise sail past the shop and land on the page behind it. */
  background-color: #171f45;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.16) inset,
    0 0 0 1px rgba(150,175,255,0.18),
    0 26px 60px -18px rgba(0,0,0,0.72),
    0 6px 18px -6px rgba(0,0,0,0.45);
}
#screen-shop .panel-wide h2 {
  color: #fff;
  text-shadow: 0 2px 10px rgba(0,0,0,0.45);
}
/* The ink-on-white pill outline vanishes against the case, so the wallet gets
   a light rim instead and reads as two chips sitting on the glass. */
#screen-shop .shop-wallet .pill {
  border-color: rgba(255,255,255,0.30);
  box-shadow: 0 2px 8px -2px rgba(0,0,0,0.55);
}
/* The faint ink track is invisible on the case; the grid is the one thing in
   here that scrolls, so it gets the light track. */
#screen-shop ::-webkit-scrollbar-track { background: rgba(255,255,255,0.10); }

/* The star mark, matching the ones on the clear card so the currency the shop
   asks for is visibly the same thing the player is scoring. */
.star-ico {
  width: 15px; height: 15px; flex: 0 0 auto;
  display: inline-block;
  background: linear-gradient(180deg, #ffe27a 0%, #ffcb2e 55%, #f5a623 100%);
  box-shadow: 0 0 0 1.5px rgba(120,80,0,0.35);
  clip-path: polygon(50% 0%, 62% 36%, 100% 38%, 70% 60%, 81% 98%, 50% 75%, 19% 98%, 30% 60%, 0% 38%, 38% 36%);
  margin-right: 4px;
}

/* ---- the one stated goal ------------------------------------------------
   A shop with 24 items and no target is a list. This is the only line in here
   that answers "what am I saving for, and how close am I". */
.shop-goal {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 4px 8px;
  padding: 8px 12px 10px;
  margin-bottom: 12px;
  border-radius: var(--r-sm);
  background: var(--panel-2);
  box-shadow: 0 0 0 1px rgba(43,58,103,0.08) inset;
}
.shop-goal-label {
  font-size: 10px; font-weight: 900; letter-spacing: 0.14em;
  color: var(--ink-soft);
}
.shop-goal-name {
  font-size: 14px; font-weight: 900; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shop-goal-num {
  display: inline-flex; align-items: center;
  font-size: 12px; font-weight: 800; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.shop-goal-bar {
  grid-column: 1 / -1;
  height: 8px; margin-top: 3px;
  border-radius: 999px;
  background: rgba(43,58,103,0.14);
  overflow: hidden;
}
.shop-goal-bar > i {
  display: block; height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #58d97a, #2ec46f);
  transition: width 0.5s cubic-bezier(.2,.9,.3,1);
}
.shop-goal.by-stars .shop-goal-bar > i { background: linear-gradient(90deg, #ffe27a, #f5a623); }
.shop-goal.done { grid-template-columns: 1fr; justify-items: center; }

/* ...the same line, inverted for the case. */
#screen-shop .shop-goal {
  background: rgba(255,255,255,0.055);
  box-shadow: 0 0 0 1px rgba(160,185,255,0.16) inset;
}
#screen-shop .shop-goal-label { color: #a9b9e4; }
#screen-shop .shop-goal-name  { color: #ffffff; }
#screen-shop .shop-goal-num   { color: #c6d1f0; }
#screen-shop .shop-goal-bar   { background: rgba(0,0,0,0.38); }

/* On a narrow phone the three-column row cannot hold a label, a skin name and
   a fraction - the name is what got ellipsised, and the name is the one word
   in the line that matters. The label goes instead: a green bar under the word
   "NEXT UNLOCK" is not carrying much. */
@media (max-width: 360px) {
  .shop-goal { grid-template-columns: 1fr auto; }
  .shop-goal-label { display: none; }
}

/* ---- section headings --------------------------------------------------
   The split is the point: one half is bought, the other is earned, and saying
   so turns a wall of cards into two ladders. */
.shop-sec {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0 8px;
  padding: 4px 2px 0;
}
.shop-sec:not(:first-child) {
  margin-top: 8px;
  padding-top: 12px;
  border-top: 2px solid rgba(43,58,103,0.10);
}
.shop-sec-title {
  font-size: 11px; font-weight: 900; letter-spacing: 0.14em;
  color: var(--ink);
}
/* "6/18". The whole reason a wall of cards reads as a collection rather than
   a price list. Tabular figures so it does not jitter as it fills. */
.shop-sec-count {
  font-size: 11px; font-weight: 800;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}
.shop-sec-count b { font-weight: 900; }
.shop-sec-hint {
  /* 11px and darker: at 10px in --ink-soft this measured 3.7:1, and it is the
     line that explains what the whole second tier is. */
  grid-column: 1 / -1;
  font-size: 11px; font-weight: 700; letter-spacing: 0.02em;
  color: #46567f;
  margin-top: 2px;
}
#screen-shop .shop-sec:not(:first-child) { border-top-color: rgba(160,185,255,0.20); }
#screen-shop .shop-sec-title { color: #ffffff; }
#screen-shop .shop-sec-hint  { color: #b6c3ea; }
#screen-shop .shop-sec-count { color: #93a4d0; }
/* Campaign star total, shown against what the section needs. A progress line,
   which is what stars are - deliberately not styled as a wallet pill. */
.shop-sec-total {
  grid-column: 1 / -1;
  display: inline-flex; align-items: center; justify-self: start;
  margin-top: 3px;
  font-size: 11px; font-weight: 800; letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.shop-sec-total b { font-weight: 900; }
#screen-shop .shop-sec-total { color: #a9b9e4; }
#screen-shop .shop-sec-total b { color: #ffd166; }
#screen-shop .shop-sec-count b { color: #ffffff; }
/* A finished set says so in gold, matching the star currency. */
#screen-shop .shop-sec-count.full,
#screen-shop .shop-sec-count.full b { color: #ffd166; }

.btn-star {
  background-color: #f0a91e; color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
}

/* Sized so a phone gets three columns, not two. The catalogue is 24 deep now,
   and at two columns that is twelve rows of scrolling to see what exists -
   which defeats the point of showing the player what they are working toward. */
.skin-grid {
  display: grid;
  /* 90, not 96: inside the panel's padding a 380px phone leaves about 304px of
     grid, and three 96px columns plus gaps wanted 306. Four pixels was the
     difference between three columns and twelve rows of scrolling. */
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
  gap: 9px;
  margin-bottom: 15px;
}

/* ---- rarity ------------------------------------------------------------
   One accent per tier, spent on the card rim, the light behind the doll and
   the plate under it. The ladder is steel -> blue -> violet -> rose -> gold,
   which is the order players already read as ascending.

   Gold is the top of the ladder AND the colour of a star, which is not a
   coincidence: the star-earned set is the only one money cannot buy, so it
   wears the currency that buys it. Tiers come from Skins.rarity(), which
   derives them from price - re-pricing a skin re-frames its card for free.

   Written as literal rgba rather than color-mix(): this ships to phone
   browsers a couple of years behind, and a tier that silently loses its
   colour is worse than one that never had it. */
.rar-common {
  --tier: #a9bbdb;
  --tier-edge: rgba(169,187,219,0.30);
  --tier-glow: rgba(118,144,196,0.32);
}
.rar-rare {
  --tier: #55c8ff;
  --tier-edge: rgba(85,200,255,0.42);
  --tier-glow: rgba(28,128,210,0.48);
}
.rar-epic {
  --tier: #c39bff;
  --tier-edge: rgba(195,155,255,0.46);
  --tier-glow: rgba(120,70,224,0.54);
}
.rar-legendary {
  --tier: #ff86bd;
  --tier-edge: rgba(255,134,189,0.52);
  --tier-glow: rgba(214,47,127,0.56);
}
.rar-mastery {
  --tier: #ffd166;
  --tier-edge: rgba(255,209,102,0.52);
  --tier-glow: rgba(224,138,18,0.54);
}

.skin-card {
  position: relative;
  background: linear-gradient(180deg, rgba(255,255,255,0.11) 0%, rgba(255,255,255,0.028) 100%);
  border: 0;
  box-shadow: 0 0 0 1px var(--tier-edge), 0 8px 18px -10px rgba(0,0,0,0.85);
  border-radius: 16px;
  padding: 6px 6px 8px;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
}
/* The top two tiers throw light onto the panel around them. It is the only
   thing on the screen that says "this one is worth the grind" before you have
   read a single price. */
.rar-legendary, .rar-mastery {
  box-shadow: 0 0 0 1px var(--tier-edge),
              0 0 20px -8px var(--tier),
              0 8px 18px -10px rgba(0,0,0,0.85);
}

/* The display case: the doll lit from behind in its own tier's colour. */
.skin-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 132 / 168;
  border-radius: 11px;
  overflow: hidden;
  background:
    radial-gradient(72% 50% at 50% 40%, var(--tier-glow) 0%, rgba(0,0,0,0) 74%),
    linear-gradient(180deg, #151d3e 0%, #0a0f26 100%);
  background-color: #0d1330;      /* see the note on .panel-wide */
  box-shadow: 0 0 0 1px rgba(255,255,255,0.07) inset;
}
/* A pool of light on the floor, so the doll stands in the case instead of
   floating in it. */
.skin-frame::before {
  content: '';
  position: absolute; left: 14%; right: 14%; bottom: 17%;
  height: 11%;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, var(--tier-glow) 0%, rgba(0,0,0,0) 70%);
  pointer-events: none;
}
.skin-prev { position: relative; width: 100%; height: auto; aspect-ratio: 132 / 168; display: block; }

/* The plate. Inside the frame on a scrim rather than on its own row: at
   twenty-seven cards a third text line costs half a screen of scrolling. */
.skin-tier {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2px 3px 3px;
  font-size: 9px; line-height: 1.15; font-weight: 900; letter-spacing: 0.08em;
  text-align: center;
  color: var(--tier);
  background: linear-gradient(180deg, rgba(6,9,22,0) 0%, rgba(6,9,22,0.82) 44%, rgba(6,9,22,0.94) 100%);
  text-shadow: 0 1px 2px rgba(0,0,0,0.85);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}

/* A shine crossing the glass, top two tiers only. Staggered by column so four
   legendaries in a row do not flash in unison like a fault. */
.rar-legendary .skin-frame::after,
.rar-mastery .skin-frame::after {
  content: '';
  position: absolute; inset: -40% -60%;
  background: linear-gradient(100deg,
              rgba(255,255,255,0) 42%,
              rgba(255,255,255,0.26) 50%,
              rgba(255,255,255,0) 58%);
  transform: translateX(-120%);
  animation: skinShine 4.6s ease-in-out infinite;
  pointer-events: none;
}
.skin-card:nth-child(3n) .skin-frame::after   { animation-delay: 1.5s; }
.skin-card:nth-child(3n+1) .skin-frame::after { animation-delay: 3.0s; }
@keyframes skinShine {
  0%, 64%   { transform: translateX(-120%); }
  88%, 100% { transform: translateX(120%); }
}

/* The worth badge. Deliberately quiet - a stamped plate on the corner of the
   glass, not a price tag. It has to survive on top of an aura, so it carries
   its own dark ground rather than relying on the art behind it. */
.skin-worth {
  position: absolute; top: 5px; left: 5px;
  display: inline-flex; align-items: center;
  padding: 2px 5px 2px 4px;
  border-radius: 7px;
  background: rgba(8,12,28,0.76);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.17);
  font-size: 9.5px; font-weight: 900;
  color: #fff;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  pointer-events: none;
}
.skin-worth .cash-ico.small {
  width: 13px; height: 9px; margin-right: 3px;
  border-width: 1.5px; border-radius: 2px;
}
.skin-worth .cash-ico::after { width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; box-shadow: 0 0 0 1px rgba(255,255,255,0.7); }
.skin-worth .star-ico { width: 11px; height: 11px; margin-right: 3px; }

/* Locked: a padlock on the glass. The button already states the price, but
   the art is what the eye reads first and it should answer on its own. */
.skin-lock {
  position: absolute; top: 5px; right: 5px;
  width: 17px; height: 17px;
  border-radius: 6px;
  background: rgba(8,12,28,0.74);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.18);
}
.skin-lock::before {
  content: ''; position: absolute; left: 50%; top: 3px;
  width: 7px; height: 6px; margin-left: -3.5px;
  border: 1.6px solid rgba(255,255,255,0.75);
  border-bottom: 0;
  border-radius: 4px 4px 0 0;
}
.skin-lock::after {
  content: ''; position: absolute; left: 50%; top: 8px;
  width: 11px; height: 7px; margin-left: -5.5px;
  border-radius: 2px;
  background: rgba(255,255,255,0.85);
}

/* Worn: a green tick on the glass and a lit rim, so the one you are wearing
   is findable in a scroll of twenty-seven without reading any buttons. */
.skin-check {
  position: absolute; top: 5px; right: 5px;
  width: 18px; height: 18px; border-radius: 50%;
  background: linear-gradient(180deg, #58e08a 0%, #24a95c 100%);
  box-shadow: 0 0 0 2px rgba(8,16,12,0.45), 0 2px 6px rgba(0,0,0,0.5);
}
.skin-check::after {
  content: ''; position: absolute;
  left: 6px; top: 3.5px; width: 4px; height: 9px;
  border: solid #fff; border-width: 0 2.2px 2.2px 0;
  border-radius: 1px;
  transform: rotate(42deg);
}
.skin-card.equipped {
  background: linear-gradient(180deg, rgba(72,226,142,0.20) 0%, rgba(46,196,111,0.05) 100%);
  box-shadow: 0 0 0 2px #3fdc86,
              0 0 22px -8px rgba(63,220,134,0.85),
              0 8px 18px -10px rgba(0,0,0,0.85);
}

.skin-name { font-size: 11px; font-weight: 800; color: var(--ink-soft); }
/* A shade brighter and a touch wider than it was. It is the only place the
   skin is actually named, and it was the quietest text on a card that has a
   rarity plate, a price and a worth badge all shouting over it. */
#screen-shop .skin-name {
  color: #f2f5ff;
  font-size: 11.5px;
  letter-spacing: 0.015em;
  line-height: 1.2;
}

/* The shop's two actions, in one row. `shop-back` is the quieter of the two:
   it is the way out, not the thing to do. */
.shop-foot { display: flex; gap: 9px; align-items: stretch; }
.shop-foot .btn { margin-bottom: 0; flex: 1 1 0; min-width: 0; }
.shop-foot .shop-back { flex: 0 0 34%; }
/* Under 420px the reward label and the word Back cannot share a line without
   one of them wrapping to two, so they go back to stacking. */
@media (max-width: 420px) {
  .shop-foot { flex-direction: column; }
  .shop-foot .shop-back { flex: 1 1 auto; }
}
/* min-height, not just padding: these are the buy buttons, and on a narrow
   phone the grid squeezed them to 32px tall - under a fingertip, on the one
   control in the game that spends the player's money. */
.btn-small {
  font-size: 12px; padding: 8px 6px; margin-bottom: 0;
  border-width: 3px; border-radius: var(--r-sm); gap: 4px;
  min-height: 40px;
}

/* ============================ AD CARD ================================ */
.ad-box { text-align: center; color: #fff; width: min(100%, 340px); }
.ad-label { font-size: 13px; letter-spacing: 0.24em; font-weight: 800; margin: 0 0 14px; opacity: 0.85; }
.ad-bar { height: 8px; background: rgba(255,255,255,0.18); border-radius: 999px; overflow: hidden; }
.ad-bar-fill { height: 100%; width: 0; background: var(--yellow); border-radius: 999px; }
.ad-note { margin: 14px 0 0; font-size: 11px; opacity: 0.45; }

/* ============================== TOAST ================================ */
.toast {
  position: absolute;
  left: 50%; bottom: calc(26px + var(--safe-b));
  transform: translate(-50%, 20px);
  background: var(--ink); color: #fff;
  font-weight: 800; font-size: 14px;
  padding: 10px 18px; border-radius: 999px;
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  max-width: 88vw; text-align: center;
  z-index: 5;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ====================== short-viewport tuning ======================== */
@media (max-height: 620px) {
  .brand-art { height: 66px; }
  .brand-doll { width: 56px; height: 64px; }
  .brand-doll-b { bottom: -34px; }
  .wordmark, .brand-lg .wordmark { font-size: clamp(30px, 8vw, 46px); -webkit-text-stroke-width: 7px; }
  .brand { margin-bottom: 10px; }
  .play-btn { height: 78px; width: 98px; padding: 18px; }
  .play-plate { padding: 4px 15px; margin-top: -11px; }
  .best-line { margin: 9px 0 11px; }
  .tile-btn { width: 50px; height: 50px; padding: 12px; }
  .load-box { margin-top: 20px; }

  .panel { padding: 15px 15px 12px; }
  /* ...but a headed panel's band must still run edge to edge. */
  .panel.panel-headed { padding: 0; }
  .panel-head { padding: 16px 18px 20px; }
  .panel-body { padding: 12px 16px 14px; }
  .star { width: 26px; height: 26px; }
  .star:nth-child(2) { width: 33px; height: 33px; }
  .subtitle { margin-bottom: 9px; }
  .btn { padding: 11px 12px; margin-bottom: 7px; font-size: 15px; }
  .btn-big { padding: 13px; }
  .score-line { padding: 2px 0; font-size: 13px; }
  .skin-grid { grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); }
}

@media (prefers-reduced-motion: reduce) {
  .panel, .tap-hint.show, .spark, .bump,
  .btn-reward.urging, .pressable.popped, .progress-head.hop,
  .brand-doll-a, .brand-doll-b { animation: none; }
  /* The shine has to be removed, not stopped: paused at 0% it parks a bright
     band across the middle of every top-tier card. */
  .rar-legendary .skin-frame::after,
  .rar-mastery .skin-frame::after { content: none; }
  /* starPop starts from opacity 0 with fill-mode both, so it has to be replaced
     rather than merely stopped - otherwise the earned stars never appear. */
  .star.on { animation: none; transform: scale(1); opacity: 1; }
  .brand-doll-a { transform: rotate(34deg); }
  .brand-doll-b { transform: rotate(-18deg); }
}
