/* ==========================================================================
   2048 Voxel Worlds — styled after brickbreaker3d.html.

   The defining move of that game's look is that the 3D scene IS the page: the
   canvas is fixed at inset 0, the scene's own background and fog are the
   backdrop, and every piece of UI floats on top in `pointer-events: none`
   bands. Nothing is boxed in a CSS panel. This sheet follows that.

   Its surface language, also taken from there:
     · panels are a vertical gradient with a SOLID 2px edge (--vx-edge), not a
       translucent hairline
     · buttons are extruded — `0 Npx 0 <darker>` plus a soft ambient shadow —
       and sink on :active
     · type is heavy, letter-spaced, with hard `0 Npx 0` text shadows

   Mobile-first: base rules target a phone, the media queries only widen.
   Colours here are DEFAULTS; js/voxel/themes.js owns the real values and
   rewrites these custom properties per theme.
   ========================================================================== */

/* ---- fonts, self-hosted ----
   Both faces are served from css/fonts/ rather than the Google CDN. A game
   that fetches its icons at runtime loses them offline, behind a captive
   portal, or in a region that blocks Google — a blank back button is a broken
   game, not a cosmetic downgrade — and most HTML5 portals require a build with
   no external requests. Self-hosting also keeps player IPs away from a third
   party, which is what the German Google Fonts rulings were about.

   Outfit is the variable font, one file for the whole 400–900 range that this
   sheet uses. Material Symbols is subset to the 17 ligatures actually on
   screen: 4 KB rather than the full face. Both are the files Google serves, so
   rendering is identical to the CDN version.

   Adding an icon means re-subsetting. Fetch a new file with the icon list:
     https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded
       :opsz,wght,FILL,GRAD@24,600,0,0&icon_names=arrow_back,bolt,...
   and replace material-symbols-rounded-subset.woff2 with what it points at.
   An icon missing from the subset renders as nothing, so the list below is the
   record of what the build contains:
     arrow_back  bar_chart  bolt      calendar_month  close     help
     lightbulb   person     public       rotate_left     rotate_right  settings
     swap_horiz  trophy     undo         volume_off      volume_up

   Licences: Outfit is SIL OFL 1.1, Material Symbols is Apache 2.0 — both
   cleared for commercial use. See css/fonts/LICENSE.md. */

@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('fonts/outfit-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('fonts/outfit-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Material Symbols Rounded';
  font-style: normal;
  font-weight: 600;
  /* `block`, not `swap`: these are ligatures. A swap period would render the
     word "volume_up" in the button until the font lands. */
  font-display: block;
  src: url('fonts/material-symbols-rounded-subset.woff2') format('woff2');
}

/* ---- interface icons ----
   Material Symbols Rounded, linked from index.html. Controls are the whole
   scope: back, mute, the dock, the rotate pair, and every close button. The
   emoji that remain — the result panel's rewards, achievements, the mascot —
   are content rather than controls, and are meant to carry their own colour.

   `font-size` drives the glyph's box, so each control sizes its own icon and
   the ligature name inherits nothing from the surrounding heavy UI type. The
   `font-variation-settings` line is required: without it the variable font
   falls back to axis defaults and the weight drifts away from the link's. */
.ms {
  font-family: 'Material Symbols Rounded';
  /* The subset was cut as a static instance at FILL 0, wght 600, GRAD 0,
     opsz 24 — the axes are baked in, so there is nothing to vary here. */
  font-weight: 600;
  font-style: normal;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
  direction: ltr;
  display: inline-block;
  font-feature-settings: 'liga';
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
  /* Belt and braces with font-display: block — the ligature name must never be
     seen, even if the file is missing entirely. */
  overflow: hidden;
  max-width: 1em;
}

:root {
  --vx-bg: #131a28;
  --vx-bg-2: #233040;
  --vx-panel: linear-gradient(180deg, #233040 0%, #141b25 100%);
  --vx-panel-solid: #1b2430;
  --vx-edge: #3a4a5e;
  --vx-ink: #ffffff;
  --vx-ink-soft: #9fb3c8;
  --vx-accent: #ffd94a;
  --vx-accent-2: #6c9af5;
  --vx-good: #5cb531;
  --vx-good-lo: #3c7d1e;
  --vx-sink: #10161f;
  --vx-bad: #e23b2e;

  --radius: 14px;
  --radius-sm: 10px;
  --gap: 0.5rem;

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* ===== CANVAS SCALER (from cricket-smash-3d) =====
   One continuous root font-size so the UI scales as a unit rather than
   jumping at stepped breakpoints. Every touch target additionally carries a
   px floor inside clamp(), because a pure-rem button would otherwise land
   around 27px on a small phone.

   The reference divisors are tighter than cricket's (64/38 landscape rather
   than 80/45) because the HUD here floats over a full-screen 3D scene instead
   of sitting in a laid-out page — against all that depth it read too small at
   cricket's scale. */
html {
  font-size: clamp(13px, min(100vw / 36, 100vh / 64), 22px);
}

@media (orientation: landscape) {
  html {
    font-size: clamp(12px, min(100vw / 64, 100vh / 38), 22px);
  }
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--vx-bg);
  font-family: 'Outfit', 'Segoe UI', system-ui, sans-serif;
  color: var(--vx-ink);
  /* `manipulation`, not `none` — per the cricket sheet's note, `none` on the
     root intersects with every descendant and kills modal scrolling. The play
     surface re-declares `none` for itself. */
  touch-action: manipulation;
}

.vx-app {
  position: relative;
  width: 100%;
  height: 100%;
  height: 100dvh;
  overflow: hidden;
}

/* ==========================================================================
   THE WORLD
   Full-bleed 3D behind everything, on the title screen as well as in play.
   ========================================================================== */

.world {
  position: fixed;
  inset: 0;
  z-index: 0;
  /* Drags here are swipes, never page pans. */
  touch-action: none;
}

.voxel-canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
}

.board-fallback {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: grid;
  place-content: center;
  gap: 0.3rem;
  text-align: center;
  padding: 1rem;
  background: var(--vx-bg);
}
.board-fallback[hidden] { display: none; }
.fallback-title { font-weight: 900; letter-spacing: 0.06em; }
.fallback-desc { font-size: clamp(11px, 0.7rem, 14px); color: var(--vx-ink-soft); }

#confetti-canvas,
.trail-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 60;
}

/* Above the modal scrim, so a burst fired with the result panel open falls in
   front of it rather than behind the dim layer. Under the toasts. */
#confetti-canvas { z-index: 85; }

/* ==========================================================================
   SCREENS
   ========================================================================== */

/* The screen layer covers the whole viewport on top of the canvas, so it MUST
   NOT take pointer input — otherwise it swallows every tap and drag meant for
   the board, which kills tap-to-target for Swap/Blast and swipe-to-move
   entirely (keyboard still worked, which is what hid it). Only the things
   that are actually controls take input back. */
.screens {
  position: fixed;
  inset: 0;
  z-index: 10;
  pointer-events: none;
}

.screen { display: none; }
.screen.active { display: block; }

/* The title screen is a full-screen scrim with buttons on it, so it does want
   input — unlike the game screen, which is only floating HUD bands. */
.screen-title { pointer-events: auto; }

/* ==========================================================================
   SURFACE LANGUAGE
   ========================================================================== */

.panel,
.scoreboard,
.rail-card,
.dock-btn,
.btn,
.icon-btn,
.modal {
  background: var(--vx-panel);
  border: 2px solid var(--vx-edge);
  border-radius: var(--radius-sm);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 10px 24px rgba(0, 0, 0, 0.55);
}

.section-label {
  font-size: clamp(10px, 0.62rem, 13px);
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--vx-ink-soft);
  text-transform: uppercase;
  margin-top: 0.3rem;
}

/* ---- buttons: extruded slabs that sink when pressed ---- */

.btn,
.tile-btn,
.dock-btn,
.icon-btn,
.rotate-side-btn {
  position: relative;
  color: var(--vx-ink);
  font-family: inherit;
  font-weight: 800;
  letter-spacing: 0.04em;
  cursor: pointer;
  border: 2px solid var(--vx-edge);
  border-radius: var(--radius-sm);
  background: var(--vx-panel);
  box-shadow: 0 4px 0 var(--vx-sink), 0 8px 16px rgba(0, 0, 0, 0.35);
  transition: transform 0.08s, box-shadow 0.08s, color 0.08s;
}

.btn:hover,
.tile-btn:hover,
.dock-btn:hover,
.icon-btn:hover { filter: brightness(1.18); }

.btn:active,
.tile-btn:active,
.dock-btn:active,
.icon-btn:active,
.rotate-side-btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 var(--vx-sink), 0 4px 10px rgba(0, 0, 0, 0.4);
}

.btn:disabled,
.dock-btn:disabled,
.tile-btn:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  transform: none;
}

.btn:focus-visible,
.tile-btn:focus-visible,
.dock-btn:focus-visible,
.icon-btn:focus-visible {
  outline: 3px solid var(--vx-accent);
  outline-offset: 3px;
}

.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  min-height: clamp(44px, 2.75rem, 54px);
  padding: 0.6rem 0.9rem;
  font-size: clamp(13px, 0.82rem, 17px);
}

.btn-big {
  min-height: clamp(56px, 3.5rem, 68px);
  font-size: clamp(18px, 1.15rem, 23px);
  letter-spacing: 0.12em;
}
.btn-small {
  min-height: clamp(42px, 2.4rem, 56px);
  font-size: clamp(12px, 0.72rem, 16px);
  padding-inline: 0.7rem;
}

.btn-primary {
  background: linear-gradient(180deg, #8fdc4d 0%, var(--vx-good) 100%);
  border-color: rgba(255, 255, 255, 0.28);
  color: #1c2a17;
  box-shadow: 0 5px 0 var(--vx-good-lo), 0 10px 20px rgba(0, 0, 0, 0.4);
}
.btn-primary:active {
  box-shadow: 0 1px 0 var(--vx-good-lo), 0 4px 10px rgba(0, 0, 0, 0.4);
}

.btn-danger {
  background: linear-gradient(180deg, #ef6b6b 0%, #d34747 100%);
  border-color: rgba(255, 255, 255, 0.22);
  color: #2b0808;
  box-shadow: 0 5px 0 #8f2b2b, 0 10px 20px rgba(0, 0, 0, 0.4);
}

.icon-btn {
  display: grid;
  place-items: center;
  width: clamp(44px, 2.6rem, 60px);
  height: clamp(44px, 2.6rem, 60px);
  flex: none;
  font-size: clamp(16px, 1rem, 20px);
  color: var(--vx-ink-soft);
}
.icon-btn:hover { color: var(--vx-ink); }

/* ==========================================================================
   TITLE SCREEN
   A scrim over the live world, so the 3D reads through the menu.
   ========================================================================== */

.screen-title {
  position: fixed;
  inset: 0;
  background: rgba(10, 14, 20, 0.55);
  backdrop-filter: blur(2px);
}
.screen-title.active {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: calc(1rem + var(--safe-top)) 1rem calc(1rem + var(--safe-bottom));
  text-align: center;
}

.title-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  width: 100%;
  max-width: 26rem;
  margin: auto 0;
}

.brand-logo { display: flex; gap: 0.28rem; }

/* Each digit is an extruded cube — the same trick the buttons use. */
.brand-cube {
  display: grid;
  place-items: center;
  width: clamp(50px, 3.2rem, 66px);
  height: clamp(50px, 3.2rem, 66px);
  border-radius: 10px;
  font-size: clamp(29px, 1.9rem, 40px);
  font-weight: 900;
  color: #2c2418;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 55%, #d9a91f 100%);
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, 0.6),
    0 6px 0 #9d7f1e,
    0 12px 20px rgba(0, 0, 0, 0.5);
  animation: cube-bob 2.4s ease-in-out infinite;
}
.brand-cube:nth-child(2) { animation-delay: 0.12s; }
.brand-cube:nth-child(3) { animation-delay: 0.24s; }
.brand-cube:nth-child(4) { animation-delay: 0.36s; }

@keyframes cube-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-0.3rem); }
}

.brand-sub {
  font-size: clamp(12px, 0.75rem, 16px);
  font-weight: 800;
  letter-spacing: 0.32em;
  color: var(--vx-ink-soft);
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}

.best-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  background: var(--vx-panel);
  border: 2px solid var(--vx-edge);
}
.best-pill-label {
  font-size: clamp(10px, 0.6rem, 13px);
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--vx-ink-soft);
}
.best-pill-value {
  font-size: clamp(15px, 0.95rem, 20px);
  font-weight: 900;
  color: var(--vx-accent);
}

.title-actions { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; }
.title-actions .btn[hidden] { display: none; }

/* Six chunky squares — the whole menu reachable with a thumb. */
.title-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  width: 100%;
}

.tile-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.2rem;
  padding: 0.6rem 0.3rem;
  min-height: clamp(64px, 4.1rem, 80px);
}
/* The emoji these replaced brought their own colour; a monochrome glyph has to
   be given one, or the button reads as label-only. */
.tile-btn-ico {
  font-size: clamp(22px, 1.4rem, 30px);
  line-height: 1;
  color: var(--vx-accent);
}
.tile-btn-label {
  font-size: clamp(9px, 0.58rem, 12px);
  letter-spacing: 0.08em;
  color: var(--vx-ink-soft);
}

.nav-dot {
  position: absolute;
  top: 0.4rem;
  inset-inline-end: 0.4rem;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--vx-accent);
  box-shadow: 0 0 0.5rem var(--vx-accent);
}
.nav-dot[hidden] { display: none; }

/* The same corner as the dot, but a number: how many worlds are affordable
   now. Red rather than the accent gold, matching the counts on the dock, so
   a badge with a figure in it always reads the same way across the game. */
.nav-count {
  position: absolute;
  top: -8px;
  inset-inline-end: -8px;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--vx-bad);
  border: 2px solid rgba(255, 255, 255, 0.85);
  color: #fff;
  font-size: clamp(11px, 0.68rem, 14px);
  font-weight: 900;
  display: grid;
  place-items: center;
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.45);
  animation: video-pulse 1.6s ease-in-out infinite;
}
.nav-count[hidden] { display: none; }

body.reduced-motion .nav-count { animation: none; }

/* ---- the player chip ----
   Top left, opposite the mute button, on the title screen only. It carries the
   wallet because coins belong to the player, not to the menu they were sitting
   under, and it gives the profile panel something to open from. */

.profile-chip {
  position: absolute;
  z-index: 12;
  top: calc(0.7rem + var(--safe-top));
  inset-inline-start: 0.7rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  max-width: min(62vw, 15rem);
  /* Asymmetric: tighter on the avatar side. Logical, so the tight edge
     follows the avatar when the layout mirrors. */
  padding-block: 0.4rem;
  padding-inline: 0.4rem 0.7rem;
  border-radius: 999px;
  border: 2px solid var(--vx-edge);
  background: var(--vx-panel);
  box-shadow: 0 4px 0 var(--vx-sink), 0 8px 16px rgba(0, 0, 0, 0.35);
  color: var(--vx-ink);
  font-family: inherit;
  cursor: pointer;
  /* The screen layer lets drags through to the board; a control on it has to
     claim its own taps back. */
  pointer-events: auto;
}
.profile-chip:hover { filter: brightness(1.12); }
.profile-chip:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--vx-sink); }

.profile-av {
  display: grid;
  place-items: center;
  flex: none;
  width: clamp(30px, 1.9rem, 42px);
  height: clamp(30px, 1.9rem, 42px);
  border-radius: 50%;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  font-size: clamp(15px, 0.95rem, 21px);
  color: #3a2d05;
}

.profile-meta { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }

.profile-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: clamp(12px, 0.74rem, 16px);
  font-weight: 900;
  letter-spacing: 0.02em;
}

.profile-coins {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: clamp(11px, 0.66rem, 14px);
  font-weight: 800;
  color: var(--vx-accent);
}

/* ---- the profile panel ---- */

.profile-hero {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.28);
  border: 2px solid var(--vx-edge);
}

.profile-hero-av {
  display: grid;
  place-items: center;
  flex: none;
  width: clamp(44px, 2.8rem, 60px);
  height: clamp(44px, 2.8rem, 60px);
  border-radius: 50%;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  font-size: clamp(22px, 1.4rem, 30px);
  color: #3a2d05;
  box-shadow: 0 3px 0 #9d7f1e;
}

.profile-hero-meta { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }

.profile-hero-name {
  font-size: clamp(15px, 0.95rem, 20px);
  font-weight: 900;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-hero-sub { font-size: clamp(11px, 0.66rem, 14px); color: var(--vx-ink-soft); }
.profile-hero-sub b { color: var(--vx-accent); }

/* The panel is opened to do one thing, so its button spans the width and sits
   at the bottom where a form is finished. */
#modal-profile .modal-actions { margin-top: 0.2rem; }
#modal-profile .modal-actions .btn { flex: 1; min-height: clamp(44px, 2.8rem, 56px); }

/* ---- the welcome panel -------------------------------------------------

   The first-run dressing on #modal-profile. Everything here is additive: with
   .is-welcome off, the panel falls back to the plain settings row it was. */

/* The heading centres and stacks with its subtitle, so the close button has to
   come out of the flow — otherwise it steals half the row and pushes the title
   off centre, which is exactly what a centred title cannot survive. */
/* .modal-head.profile-head, not .profile-head alone: the base .modal-head rule
   is declared further down this file with justify-content:space-between, and at
   equal specificity the later rule wins — the title stayed left-aligned with a
   silent no-op sitting up here. Two classes settles it wherever either moves. */
.modal-head.profile-head { position: relative; justify-content: center; }
.profile-head-inner { text-align: center; }
.profile-head .modal-close {
  position: absolute;
  top: 50%;
  inset-inline-end: 0.85rem;
  transform: translateY(-50%);
  border-radius: 50%;
}

#profile-title {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

/* Sparkles only on the greeting. They are drawn at the title's own font-size so
   they scale with it rather than needing their own clamp. */
.title-spark { display: none; flex: none; width: 0.85em; color: var(--vx-accent-2); }
.title-spark svg { display: block; width: 100%; height: auto; fill: currentColor; }
.modal-welcome.is-welcome .title-spark { display: block; }

.profile-subtitle {
  margin: 0.2rem 0 0;
  font-size: clamp(11px, 0.7rem, 14px);
  font-weight: 700;
  color: var(--vx-ink-soft);
}
.profile-subtitle[hidden] { display: none; }

/* ---- the name field ----
   A row that OWNS the border, with a borderless input inside it, so the focus
   ring surrounds the icons too instead of cutting between them and the text. */
.name-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-inline: 0.65rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.35);
}
.name-field:focus-within { border-color: var(--vx-accent); }

.name-field .text-input {
  flex: 1;
  min-width: 0;
  padding-inline: 0;
  border: 0;
  background: none;
  min-height: clamp(44px, 2.8rem, 56px);
}
.name-field .text-input:focus { outline: none; }

.name-field-ico { flex: none; font-size: 1.15rem; color: var(--vx-ink-soft); }
.name-field-pen { flex: none; width: 1.05rem; color: var(--vx-ink-soft); }
.name-field-pen svg { display: block; width: 100%; height: auto; }

/* ---- the note, with its globe ----
   align-items:start, not center: the note wraps to two lines on most languages
   and a centred icon would float in the gap between them. */
.note-iconed {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
}
.note-ico { flex: none; font-size: 1rem; line-height: 1.35; color: var(--vx-accent-2); }

/* The ▶ on the welcome CTA. Sized off the button's own text so it never drifts
   out of step with it. */
#btn-profile-save { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; }
.cta-play { font-size: 0.8em; line-height: 1; }
.cta-play[hidden] { display: none; }

.wallet-row { display: flex; gap: 0.5rem; }

.wallet-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.32rem 0.7rem;
  border-radius: 999px;
  background: var(--vx-panel);
  border: 2px solid var(--vx-edge);
  font-size: clamp(12px, 0.78rem, 16px);
  font-weight: 800;
}
.wallet-ico.coin { color: var(--vx-accent); }

.site-footer {
  font-size: clamp(9px, 0.55rem, 11px);
  color: var(--vx-ink-soft);
  opacity: 0.6;
  text-align: center;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}

/* ==========================================================================
   GAME HUD
   Bands float over the world. Each band ignores pointer input so drags reach
   the canvas; only the controls inside accept it.
   ========================================================================== */

.hud {
  position: fixed;
  z-index: 12;
  pointer-events: none;
}
.hud > * { pointer-events: auto; }

.hud-top {
  top: calc(0.7rem + var(--safe-top));
  inset-inline: 0.7rem;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 0.5rem;
}

/* Corner controls are absolutely positioned rather than sharing the row, so
   the stat panel sits on the true centre of the viewport whatever flanks it. */
.hud-back {
  position: absolute;
  top: 0;
  inset-inline-start: 0;
}

.mute-btn {
  position: fixed;
  z-index: 30;
  top: calc(0.7rem + var(--safe-top));
  inset-inline-end: 0.7rem;
}
.mute-btn.muted { color: var(--vx-bad); }

/* ---- RTL: the two icons that mean a DIRECTION rather than a shape ----

   Logical inset properties move the chrome to the right corners on their own,
   but an icon font is just glyphs — `arrow_back` points left in every language,
   so on a mirrored layout the "back" button sat in the top-right pointing away
   from where back now is. Same for `undo`, whose curl reads as "leftwards".

   Deliberately NOT in this list: rotate_left / rotate_right, which name real
   directions of spin. Counter-clockwise is counter-clockwise in Arabic too, and
   flipping those would make the buttons lie about what they do. */
[dir="rtl"] .hud-back .ms,
[dir="rtl"] #btn-undo .dock-ico .ms { transform: scaleX(-1); }

/* ---- the one stat panel ----
   Target and Next, a centred Score with Best under it, and the run's coins, on
   a single row. These used to be five separate chips spread over two bands,
   which read as clutter and split attention; grouped, they scan as a single
   instrument. */

.stat-panel {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.5rem 0.85rem 0.6rem;
  /* Three short cells do not need 30rem. At that width the row spread until
     the numbers looked stranded in their own panel; capping it keeps them
     grouped and leaves more of the world visible behind. */
  width: min(24rem, calc(100vw - 7rem));
  max-width: 21rem;
  background: var(--vx-panel);
  border: 2px solid var(--vx-edge);
  border-radius: var(--radius);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 10px 24px rgba(0, 0, 0, 0.55);
}

/* 1fr auto 1fr keeps the middle zone on the panel's exact centre no matter
   how wide the flanking cells get — space-between would let SCORE/BEST drift
   as the numbers grow. */
.stat-row-main {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a plain 1fr will not shrink below its content, so
     a very long score burst the row on a narrow phone. This lets the flanking
     zones give way first and keeps SCORE/BEST intact and centred. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 0.4rem;
  overflow: hidden;
}

.stat-side { display: flex; gap: 0.55rem; min-width: 0; overflow: hidden; }
.stat-side-end { justify-content: flex-end; }

.stat-core {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.stat-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  min-width: 0;
}

/* Size carries the reading order: the score is what the player is watching,
   best is the thing being chased, target and next are glanced at. */
.stat-primary .stat-value { font-size: clamp(29px, 1.92rem, 53px); }

.stat-primary .stat-label { font-size: clamp(12px, 0.72rem, 17px); color: var(--vx-accent); }
.stat-minor .stat-label   { font-size: clamp(10px, 0.58rem, 13px); }

/* BEST rides under the score as a caption rather than as a second number of
   its own — one figure to read, with its context attached. */
.stat-caption {
  font-size: clamp(11px, 0.65rem, 14px);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--vx-ink-soft);
  white-space: nowrap;
}
.stat-caption b { color: var(--vx-ink); font-weight: 900; }

/* ---- the coin cell ---- */

.coin-cell { gap: 0.15rem; }
.coin-cell .stat-label { color: var(--vx-accent); }
.coin-amount {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: clamp(17px, 1.14rem, 29px);
  font-weight: 900;
  line-height: 1.1;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}
.coin-cell-ico { color: var(--vx-accent); }
.coin-slot.pop { animation: coin-slot-pop 260ms ease-out; }

/* One coin in flight. Positioned in the fixed layer at the merged tile and
   animated to the slot by app.js, which knows both screen points. */
.flying-coin {
  position: fixed;
  z-index: 16;
  width: clamp(14px, 0.9rem, 20px);
  height: clamp(14px, 0.9rem, 20px);
  margin: calc(clamp(14px, 0.9rem, 20px) / -2) 0 0 calc(clamp(14px, 0.9rem, 20px) / -2);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffe98a 0%, var(--vx-accent) 60%, #b98d12 100%);
  border: 1px solid rgba(0, 0, 0, 0.25);
  box-shadow: 0 0 8px rgba(255, 209, 61, 0.65);
  pointer-events: none;
  will-change: transform, opacity;
}

@keyframes coin-slot-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.22); }
  100% { transform: scale(1); }
}

.stat-label {
  font-size: clamp(11px, 0.66rem, 16px);
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--vx-ink-soft);
  white-space: nowrap;
}
.stat-label b { color: var(--vx-accent); }

.stat-value {
  font-weight: 900;
  line-height: 1.1;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}

/* TARGET holds a block rather than a number, so it sits tighter than a value
   cell would. */
.stat-minor { gap: 0.15rem; }

.score-bump { animation: chip-bump 170ms ease; }
@keyframes chip-bump { 50% { transform: scale(1.09); } }

/* The run passing the old best: the score cell flares gold once, so the news
   is on the chip that carries it and not only in the toast. */
.best-flash { animation: best-flash 950ms ease; }
@keyframes best-flash {
  0%   { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 217, 74, 0.55); }
  18%  { transform: scale(1.12); box-shadow: 0 0 0 8px rgba(255, 217, 74, 0.28); }
  60%  { transform: scale(1); box-shadow: 0 0 0 14px rgba(255, 217, 74, 0); }
  100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 217, 74, 0); }
}
.best-flash .stat-caption b { color: var(--vx-accent); }

/* ---- target cube ----
   The goal used to be a caption and a progress bar: "GOAL 2048 TILE" over a
   log-scaled fill. It read as text to be parsed, and one bar inching along for
   a whole game is not a thing anyone watches. The cube is the goal made
   literal — build that tile, watch it get hit, and the next one is set. */

.target-cube {
  display: grid;
  place-items: center;
  width: clamp(31px, 2rem, 46px);
  height: clamp(31px, 2rem, 46px);
  border-radius: 7px;
  font-size: clamp(12px, 0.74rem, 18px);
  font-weight: 900;
  color: #3a2d05;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, 0.55),
    0 3px 0 #9d7f1e,
    0 0 12px rgba(255, 209, 61, 0.35);
}

/* The landing: struck, then the new value snaps in at the top of the bounce. */
.target-cube.hit { animation: target-hit 620ms ease-out; }

@keyframes target-hit {
  0%   { transform: scale(1) rotate(0deg); }
  18%  { transform: scale(0.72) rotate(-8deg); }
  45%  { transform: scale(1.35) rotate(5deg); box-shadow: 0 0 26px rgba(255, 233, 138, 0.95); }
  70%  { transform: scale(0.94) rotate(-2deg); }
  100% { transform: scale(1) rotate(0deg); }
}

/* The projectile the merged tile throws at the cube. Same flight machinery as
   a coin, sized and coloured as a block so it reads as the tile itself. */
.target-shot {
  position: fixed;
  z-index: 16;
  display: grid;
  place-items: center;
  width: clamp(26px, 1.7rem, 38px);
  height: clamp(26px, 1.7rem, 38px);
  margin: calc(clamp(26px, 1.7rem, 38px) / -2) 0 0 calc(clamp(26px, 1.7rem, 38px) / -2);
  border-radius: 6px;
  font-size: clamp(10px, 0.62rem, 15px);
  font-weight: 900;
  color: #3a2d05;
  background: linear-gradient(180deg, #fff6cf 0%, var(--vx-accent) 100%);
  box-shadow: 0 0 18px rgba(255, 209, 61, 0.85);
  pointer-events: none;
  will-change: transform, opacity;
}

/* ---- bottom band ---- */

.hud-bottom {
  left: 0.7rem;
  right: 0.7rem;
  bottom: calc(0.7rem + var(--safe-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45rem;
}

.dock {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0.35rem;
  width: 100%;
  max-width: 30rem;
}

.dock-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  min-height: clamp(56px, 3.1rem, 74px);
  padding: 0.4rem 0.2rem;
}
.dock-ico { font-size: clamp(18px, 1.05rem, 25px); line-height: 1; color: var(--vx-accent); }
/* The names sit over a lit 3D world and were the softest grey on the screen,
   which left the row reading as icons with a smudge under them. Near-white at
   full weight, over a shadow that holds them off whatever is behind. */
.dock-name {
  font-size: clamp(10px, 0.56rem, 14px);
  letter-spacing: 0.07em;
  font-weight: 900;
  color: #eaf3fd;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

.dock-btn.active {
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  color: #241d02;
  box-shadow: 0 4px 0 #9d7f1e, 0 8px 16px rgba(0, 0, 0, 0.4);
}
.dock-btn.active .dock-ico,
.dock-btn.active .dock-name { color: inherit; }

.dock-badge {
  position: absolute;
  top: -8px;
  inset-inline-end: -8px;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--vx-bad);
  color: #fff;
  font-size: clamp(11px, 0.68rem, 14px);
  font-weight: 900;
  display: grid;
  place-items: center;
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.45);
}

/* ==========================================================================
   LANDSCAPE
   A wide, short window has width to spare and height to save, so the dock
   leaves the bottom band and becomes a column down the right edge. The bottom
   band is where a phone wants it and a laptop cannot afford it.
   ========================================================================== */

@media (orientation: landscape) and (min-width: 48rem) {
  .hud-bottom {
    left: auto;
    right: 0.7rem;
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    width: auto;
  }

  .dock {
    grid-template-columns: 1fr;
    width: clamp(72px, 4.6rem, 104px);
    max-width: none;
    gap: 0.3rem;
  }

  .dock-btn { min-height: clamp(48px, 2.9rem, 64px); }

  /* Leave room for the dock column and both corner buttons. */
  .stat-panel { width: min(30rem, calc(100vw - 14rem)); }

  /* Mute stays top-right: the dock column is vertically centred, so the top of
     that edge is free and the Back/Mute pairing stays consistent with portrait. */
}

/* ---- floating banners and prompts ---- */

.power-banner {
  position: fixed;
  z-index: 14;
  left: 50%;
  top: clamp(128px, 8rem, 172px);
  transform: translate(-50%, -160%);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  border: 2px solid rgba(255, 255, 255, 0.3);
  color: #241d02;
  font-size: clamp(12px, 0.72rem, 15px);
  font-weight: 800;
  box-shadow: 0 4px 0 #9d7f1e, 0 8px 18px rgba(0, 0, 0, 0.45);
  opacity: 0;
  pointer-events: none;
  transition: transform 200ms ease, opacity 200ms ease;
}
.power-banner.show { transform: translate(-50%, 0); opacity: 1; pointer-events: auto; }

.btn-banner-cancel {
  border: 0;
  background: rgba(0, 0, 0, 0.25);
  color: inherit;
  border-radius: 50%;
  width: clamp(28px, 1.7rem, 34px);
  height: clamp(28px, 1.7rem, 34px);
  font-family: inherit;
  cursor: pointer;
}

.win-prompt-overlay {
  position: fixed;
  inset: 0;
  z-index: 15;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  padding: 1rem;
  text-align: center;
  background: rgba(10, 14, 20, 0.55);
  backdrop-filter: blur(2px);
  /* The screen layer is pointer-events:none so drags reach the board; an
     overlay that wants taps has to claim them back or its buttons are dead. */
  pointer-events: auto;
}
.win-prompt-overlay.show { display: flex; }

/* ---- ring rotation: a control on each side of the board ---- */

.rotate-side {
  position: fixed;
  z-index: 14;
  top: 50%;
  transform: translateY(-50%) scale(0.85);
  display: none;
  pointer-events: auto;
  opacity: 0;
  transition: transform 160ms ease, opacity 160ms ease;
}
.rotate-side.show { display: block; opacity: 1; transform: translateY(-50%) scale(1); }

.rotate-side-left { left: clamp(0.5rem, 2vw, 1.4rem); }
.rotate-side-right { right: clamp(0.5rem, 2vw, 1.4rem); }

.rotate-side-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  width: clamp(52px, 3.2rem, 68px);
  height: clamp(52px, 3.2rem, 68px);
  padding: 0;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  color: #241d02;
  box-shadow: 0 4px 0 #9d7f1e, 0 8px 18px rgba(0, 0, 0, 0.45);
}
.rotate-side-btn:hover { filter: brightness(1.12); }
.rotate-side-btn:disabled { opacity: 0.42; cursor: not-allowed; }
.rotate-btn-icon { font-size: clamp(22px, 1.4rem, 30px); line-height: 1; }
.rotate-btn-label {
  font-size: clamp(9px, 0.52rem, 12px);
  letter-spacing: 0.08em;
}

/* The left/right values above are only where the controls wait: _placeRotateSides
   sets an inline left and top from the board's projected edges each time the mode
   opens, so they sit against the ring they turn instead of against the screen. */

.win-prompt-title {
  font-size: clamp(32px, 6vw, 54px);
  font-weight: 800;
  color: var(--vx-accent);
  text-shadow: 0 4px 0 rgba(0, 0, 0, 0.5);
}
.win-prompt-desc {
  font-size: clamp(13px, 0.8rem, 18px);
  color: var(--vx-ink-soft);
  max-width: 22rem;
}
.win-prompt-actions { display: flex; gap: 0.5rem; width: 100%; max-width: 20rem; }

/* ==========================================================================
   MODALS
   ========================================================================== */

.modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  padding: 0.75rem;
  background: rgba(10, 14, 20, 0.7);
  backdrop-filter: blur(3px);
  touch-action: pan-y;
}
.modal-scrim[hidden] { display: none; }

.modal {
  width: min(28rem, 100%);
  max-height: 92dvh;
  display: flex;
  flex-direction: column;
  border-radius: var(--radius);
}
.modal[hidden] { display: none; }

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.75rem 0.85rem;
  border-bottom: 2px solid var(--vx-edge);
}
.modal-head h2 {
  font-size: clamp(15px, 0.95rem, 20px);
  font-weight: 900;
  letter-spacing: 0.08em;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.4);
}

.modal-body {
  padding: 0.8rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* A slim translucent bar rather than the platform default, which was a wide
     opaque strip jammed against the rounded corner of the panel. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
}
.modal-body::-webkit-scrollbar { width: 6px; }
.modal-body::-webkit-scrollbar-track { background: transparent; }
.modal-body::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.22);
  border-radius: 999px;
}
.modal-body p { font-size: clamp(12px, 0.72rem, 15px); line-height: 1.55; color: var(--vx-ink-soft); }
.modal-body p b { color: var(--vx-ink); }
.modal-note { font-size: clamp(10px, 0.62rem, 13px) !important; font-style: italic; opacity: 0.8; }
.modal-actions { display: flex; gap: 0.45rem; }

/* ---- board size cards ---- */

.grid-size-selector { display: flex; gap: 0.4rem; }

.grid-card {
  flex: 1;
  padding: 0.55rem 0.3rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.25);
  color: var(--vx-ink);
  font-family: inherit;
  cursor: pointer;
  text-align: center;
}
.grid-card.selected { border-color: var(--vx-good); background: rgba(92, 181, 49, 0.18); }
.grid-card .name { font-size: clamp(12px, 0.78rem, 16px); font-weight: 900; margin-top: 0.3rem; }
.grid-card .goal { font-size: clamp(9px, 0.58rem, 12px); color: var(--vx-ink-soft); }
.grid-card .best { font-size: clamp(9px, 0.55rem, 11px); color: var(--vx-accent); margin-top: 0.1rem; }

.grid-preview {
  display: grid;
  gap: 2px;
  width: 2.2rem;
  margin: 0 auto;
  aspect-ratio: 1;
}
.preview-4x4 { grid-template-columns: repeat(4, 1fr); }
.preview-5x5 { grid-template-columns: repeat(5, 1fr); }
.preview-6x6 { grid-template-columns: repeat(6, 1fr); }
/* Dots are generated in JS so the markup stays clean. */
.preview-dot { border-radius: 2px; background: rgba(255, 255, 255, 0.22); }

/* ---- toggles ---- */

.toggle-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem 0.6rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.25);
  border: 2px solid var(--vx-edge);
  cursor: pointer;
}
.toggle-info { flex: 1; display: flex; flex-direction: column; gap: 0.08rem; }
.toggle-info .title { font-size: clamp(12px, 0.73rem, 15px); font-weight: 800; }
.toggle-info .sub { font-size: clamp(10px, 0.6rem, 12px); color: var(--vx-ink-soft); }

.switch { position: relative; width: 44px; height: 25px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.slider {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  transition: background-color 160ms ease;
  pointer-events: none;
}
.slider::before {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: #fff;
  transition: transform 160ms ease;
}
.switch input:checked + .slider { background: var(--vx-good); }
.switch input:checked + .slider::before { transform: translateX(19px); }

.text-input {
  padding: 0.5rem 0.65rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.35);
  color: var(--vx-ink);
  font-family: inherit;
  font-size: clamp(12px, 0.76rem, 15px);
  user-select: text;
}

/* ---- the SETTINGS language control -------------------------------------

   This replaced a native <select>, which could not be finished. Worth writing
   down so nobody puts one back: a <select>'s popup is drawn by the operating
   system, and CSS reaches into it only far enough to set a background and a
   colour on the <option>. The background offered here is --vx-panel, which is
   a GRADIENT — an <option> cannot take one — so the popup fell through to the
   system white, the select's own translucent black painted over it, and the
   list rendered mid-grey with white text sitting on it. Unreadable, and not
   fixable from this side.

   Everything below is ordinary markup instead, which also lets the rows carry
   the same flags as the first-run grid. */

.lang-select { position: relative; }

.lang-select-btn {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  min-height: 44px;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.35);
  color: var(--vx-ink);
  font-family: inherit;
  font-size: clamp(12px, 0.76rem, 15px);
  font-weight: 700;
  cursor: pointer;
  text-align: left;
}
.lang-select-btn:hover { background: rgba(255, 255, 255, 0.07); }
.lang-select-btn:focus-visible { outline: 3px solid var(--vx-accent); outline-offset: 2px; }

.lang-select-name { flex: 1; min-width: 0; }

/* Drawn in CSS rather than carried as a glyph: the icon font is subset to
   seventeen ligatures (see the top of this sheet) and a caret is not one of
   them, and adding one would mean re-subsetting for a triangle. */
.lang-select-caret {
  flex: none;
  width: 0;
  height: 0;
  border-left: 0.3rem solid transparent;
  border-right: 0.3rem solid transparent;
  border-top: 0.34rem solid var(--vx-ink-soft);
  transition: transform 0.15s ease;
}
.lang-select.open .lang-select-caret { transform: rotate(180deg); }

/* The flag on the closed button, and on every row. Fixed 3:2 to match the SVG
   viewBox so nothing jitters as the selection changes. */
.lang-select-flag,
.lang-option-flag {
  flex: none;
  display: block;
  width: 1.6rem;
  aspect-ratio: 3 / 2;
  border-radius: 3px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.25);
}
.lang-select-flag svg,
.lang-option-flag svg { display: block; width: 100%; height: 100%; }

/* Expands INLINE, pushing the panel down, rather than floating over it: the
   settings body is its own scroll container, and an absolutely positioned
   popup inside one is clipped by it at the first row past the fold.

   Its own scroller, capped, because 36 rows would otherwise bury the rest of
   the settings under a list nobody asked to be permanent. */
.lang-select-list {
  margin-top: 0.35rem;
  max-height: 15rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  /* -solid, not the gradient: a gradient on a scrolling box slides under the
     content as it moves. */
  background: var(--vx-panel-solid);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.lang-select-list[hidden] { display: none; }

.lang-option {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  /* min-height in PX, not rem, and this is the point: the root size here is
     clamped down on small screens, so a rem-based row measured 29px on a
     375px phone — a third under the 44px everyone agrees is the floor for a
     finger. Padding still scales; the floor does not. This has to stand on its
     own because a <select> would have handed us the OS picker wheel and we
     have given that up. */
  min-height: 44px;
  padding: 0.5rem 0.6rem;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  background: transparent;
  color: var(--vx-ink);
  font-family: inherit;
  font-size: clamp(12px, 0.74rem, 14px);
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}
.lang-option:last-child { border-bottom: 0; }
.lang-option:hover { background: rgba(255, 255, 255, 0.08); }
.lang-option:focus-visible {
  outline: 2px solid var(--vx-accent);
  outline-offset: -2px;
}

/* Marked by colour AND a tick, so it survives a colour-blind reading. */
.lang-option.selected {
  background: rgba(92, 181, 49, 0.18);
  color: var(--vx-good);
}
.lang-option.selected::after {
  content: '✓';
  margin-left: auto;
  font-weight: 900;
}

.lang-option-name { min-width: 0; overflow-wrap: anywhere; }
/* Same reasoning as .lang-name above — these are the same words, and the row
   inherits .lang-option's 700. Lightened so the two controls agree. */
.lang-option-name, .lang-select-name { font-weight: 600; }

/* ---- themes ---- */

.theme-list { display: flex; flex-direction: column; gap: 0.35rem; }

.theme-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.25);
  color: var(--vx-ink);
  font-family: inherit;
  font-size: clamp(12px, 0.75rem, 15px);
  font-weight: 700;
  cursor: pointer;
  text-align: left;
  width: 100%;
}
.theme-item:hover { filter: brightness(1.2); }
.theme-item.selected { border-color: var(--vx-good); background: rgba(92, 181, 49, 0.18); }

/* Swatch previews the world's own floor + sky colours, with its icon on top. */
.theme-swatch {
  display: grid;
  place-items: center;
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 6px;
  flex: none;
  font-size: clamp(14px, 0.95rem, 19px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
  text-shadow: 0 2px 3px rgba(0, 0, 0, 0.6);
}
.theme-meta { flex: 1; min-width: 0; }
.theme-name { display: block; }
.theme-sub {
  display: block;
  font-size: clamp(9px, 0.6rem, 12px);
  font-weight: 600;
  color: var(--vx-ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.theme-flag { font-size: clamp(14px, 0.9rem, 18px); }

/* Price tag on a world that is not owned yet. Dimmed when it is out of reach,
   gold and outlined when it is affordable, so the list scans for what you can
   actually buy right now. */
.theme-price {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: none;
  padding: 3px 8px;
  border-radius: 999px;
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.35);
  color: var(--vx-ink-soft);
  font-size: clamp(10px, 0.62rem, 13px);
  font-weight: 900;
  white-space: nowrap;
}
.theme-price .coin-ico { color: #f1c40f; }
.theme-price.can-buy {
  border-color: var(--vx-accent);
  color: var(--vx-accent);
  background: rgba(255, 217, 74, 0.14);
}

/* An affordable world reads as live rather than greyed out. */
.theme-item.locked { opacity: 0.62; }
.theme-item.locked.affordable { opacity: 1; }

/* ---- statistics ---- */

.stat-list { display: flex; flex-direction: column; gap: 0.35rem; }
.stat-row { display: flex; align-items: center; gap: 0.5rem; font-size: clamp(12px, 0.73rem, 15px); font-weight: 700; }
.stat-row .s-ico { width: 1.2rem; text-align: center; }
.stat-row .s-name { flex: 1; color: var(--vx-ink-soft); font-weight: 600; }
.stat-row .s-val { font-weight: 900; }

/* ---- achievements ---- */

.ach-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0.4rem;
}

.ach-card {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--vx-edge);
  background: rgba(0, 0, 0, 0.25);
}
.ach-card.done { border-color: var(--vx-good); background: rgba(92, 181, 49, 0.14); }

.ach-badge {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  flex: none;
  border-radius: 6px;
  font-size: clamp(9px, 0.58rem, 12px);
  font-weight: 900;
  color: #2c2418;
  background: linear-gradient(180deg, #d9cdb8, #b9ab93);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 2px 0 rgba(0, 0, 0, 0.4);
}
.ach-card.done .ach-badge { background: linear-gradient(180deg, #ffe98a, var(--vx-accent)); }

.ach-meta { min-width: 0; flex: 1; }
.ach-name { font-size: clamp(11px, 0.66rem, 14px); font-weight: 800; }
.ach-desc { font-size: clamp(9px, 0.56rem, 11px); color: var(--vx-ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ach-track { margin-top: 0.22rem; height: 4px; border-radius: 999px; background: rgba(0, 0, 0, 0.45); overflow: hidden; }
.ach-fill { height: 100%; background: var(--vx-good); border-radius: 999px; }
.ach-tick { color: var(--vx-good); font-weight: 900; }

/* ---- leaderboard ---- */

.tab-row {
  display: flex;
  gap: 0.25rem;
  padding: 3px;
  background: rgba(0, 0, 0, 0.4);
  border-radius: 999px;
}
.tab-btn {
  flex: 1;
  padding: 0.35rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vx-ink-soft);
  font-family: inherit;
  font-size: clamp(10px, 0.64rem, 13px);
  font-weight: 800;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.tab-btn.active { background: var(--vx-accent-2); color: #06152e; }

.lb-list { display: flex; flex-direction: column; gap: 0.25rem; }
.lb-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.4rem;
  border-radius: 6px;
  font-size: clamp(11px, 0.72rem, 15px);
  font-weight: 700;
}
.lb-row.is-player { background: rgba(92, 181, 49, 0.2); border: 2px solid var(--vx-good); }
.lb-rank { width: 1.4rem; text-align: center; font-weight: 900; color: var(--vx-ink-soft); }
.lb-av { font-size: clamp(14px, 0.95rem, 19px); }
.lb-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-score { font-weight: 900; color: var(--vx-accent); }

/* ---- how to play ---- */

.howto-step { display: flex; flex-direction: column; gap: 0.3rem; }
.howto-art { display: flex; align-items: center; gap: 0.35rem; }
.howto-arrow, .howto-plus { color: var(--vx-ink-soft); font-weight: 900; }

.mini-tile {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 5px;
  font-size: clamp(10px, 0.68rem, 14px);
  font-weight: 900;
  color: #5b5348;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 2px 0 rgba(0, 0, 0, 0.4);
}
.mini-tile.t2    { background: #ece2d1; }
.mini-tile.t4    { background: #eddcbb; }
.mini-tile.t8    { background: #f2ac70; color: #fff; }
.mini-tile.t2048 { background: #edbe38; color: #fff; width: 2.9rem; }

/* ---- daily ---- */

.daily-date { font-size: clamp(14px, 0.9rem, 19px); font-weight: 900; letter-spacing: 0.05em; }
.daily-facts { display: flex; gap: 0.4rem; }
.fact {
  flex: 1;
  padding: 0.5rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.25);
  border: 2px solid var(--vx-edge);
  text-align: center;
}
.fact-label { display: block; font-size: clamp(9px, 0.52rem, 11px); font-weight: 800; letter-spacing: 0.1em; color: var(--vx-ink-soft); }
.fact-value { display: block; font-size: clamp(13px, 0.85rem, 18px); font-weight: 900; margin-top: 0.15rem; }
.daily-status { font-size: clamp(11px, 0.7rem, 14px); font-weight: 700; color: var(--vx-good); }
.daily-status.done { color: var(--vx-accent); }

/* ---- result panel ----
   Modelled on the reference art: ribbon header, dead-eyed voxel mascot over a
   radial burst, an extruded score, and stat cards that carry their own tint. */

.modal-narrow { width: min(22rem, 100%); }

/* ---- first-run language picker ----------------------------------------

   Wider than the other panels because its content is a grid of tiles rather
   than a column of rows: 36 languages in a 22rem box would be a very long
   scroll, and the whole idea is that a player SEES their language rather than
   hunting for it. Still capped against the viewport, so it never overflows.  */

.modal-wide { width: min(46rem, 100%); }

/* The heading centres because this panel has no close button beside it. Every
   other .modal-head is a title pushed left by a × on the right; with nothing on
   the right, left-aligned just reads as off-centre. */
.lang-pick-head { justify-content: center; }
.lang-pick-head h2 { text-align: center; }

.lang-pick-note {
  margin: 0 0 0.75rem;
  text-align: center;
}

/* The confirm bar. Sits outside .modal-body so the grid scrolls under it rather
   than carrying it off the bottom of a long list. */
.lang-pick-actions {
  padding: 0.75rem 0.85rem;
  border-top: 2px solid var(--vx-edge);
}
.lang-pick-actions[hidden] { display: none; }
.lang-pick-actions .btn { width: 100%; }

/* auto-fill + minmax is what makes this responsive with no breakpoints and no
   column counts written down anywhere: the browser fits as many 5.5rem tracks
   as there is room for and shares the remainder out, so the grid goes from
   seven-across on a desktop panel to three on a small phone on its own. `1fr`
   as the max is what stops the last row of a short list from stretching into
   giant tiles. minmax(0, 1fr) rather than 1fr guards the long autonyms —
   "Bahasa Indonesia" — from forcing a track wider than its share and pushing
   the grid into a horizontal scroll. */
.lang-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
  gap: 0.5rem;
}

.lang-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  /* Generous, and deliberately so: this is the one control in the game a small
     child is expected to hit, and the tile is comfortably past the 44px touch
     target everyone recommends. */
  padding: 0.6rem 0.35rem;
  min-height: 5.25rem;
  border-radius: var(--radius-sm);
  /* TRANSPARENT, not none. An outline on every tile turned the grid into a
     sheet of boxes and left the selected one competing with 35 rivals; only the
     chosen language is outlined now. The border still occupies its 2px so
     nothing shifts by a pixel when the highlight moves. */
  border: 2px solid transparent;
  background: rgba(0, 0, 0, 0.25);
  color: var(--vx-ink);
  font-family: inherit;
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}

.lang-tile:hover { background: rgba(255, 255, 255, 0.07); }
.lang-tile:active { transform: scale(0.95); }
.lang-tile:focus-visible {
  outline: 3px solid var(--vx-accent);
  outline-offset: 2px;
}

/* The flag. Fixed 3:2 to match every SVG's viewBox, so a row of tiles lines up
   whatever shape the artwork inside is. The border keeps the white flags
   (Poland, Japan-style fields) from bleeding into the dark panel. */
.lang-flag {
  display: block;
  width: 100%;
  max-width: 3.25rem;
  aspect-ratio: 3 / 2;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.25);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.lang-flag svg { display: block; width: 100%; height: 100%; }

/* The name wraps rather than truncating: a language you cannot read is bad
   enough without it being cut off halfway. */
/* The autonym under each flag.

   WEIGHT 600, NOT 800, AND THIS IS THE LEGIBILITY FIX. The rest of this
   interface is set in 800–900 because it is short, wide, upper-case English —
   a label like SETTINGS carries heavy weight well. These do not: they are ~11px
   and, in more than a third of the list, scripts whose letterforms are built
   from thin strokes and small enclosed counters. At 800 the counters in 中文,
   हिन्दी, ไทย and العربية fill in and the word turns into a dark smudge, which is
   the exact opposite of what this panel is for — a reader who cannot navigate
   the game yet is here BECAUSE they can read one of these.

   Made worse by the fallback: only Latin is drawn in Outfit, so the other
   scripts get a system face at a weight the browser may synthesise rather than
   have a real cut of, and synthetic bold smears strokes together.

   More open too — line-height 1.3 rather than 1.15, because Devanagari stacks
   marks above the line and Thai both above and below, and tight leading crops
   them.

   The SIZE is unchanged, and that was a deliberate walk-back: bumping it a
   point alongside the weight pushed the longest single-word autonym in the list
   — Azərbaycan — over the tile width, and `overflow-wrap: anywhere` broke it
   mid-word into "Azərbayca / n". Lighter is what made these readable; larger
   just cost a line break. */
.lang-name {
  font-size: clamp(10px, 0.62rem, 12px);
  font-weight: 600;
  line-height: 1.3;
  text-align: center;
  overflow-wrap: anywhere;
}

/* The selected tile, marked three ways over — colour, a lift, and a tick.
   Colour alone would be invisible to a colour-blind player, and on this panel
   it also has to read at a glance to someone who cannot read the label. */
.lang-tile.selected {
  border-color: var(--vx-good);
  background: rgba(126, 217, 87, 0.16);
  box-shadow: 0 0 0 2px rgba(126, 217, 87, 0.35), 0 6px 14px rgba(0, 0, 0, 0.45);
  position: relative;
}
.lang-tile.selected::after {
  content: '✓';
  position: absolute;
  top: 0.15rem;
  inset-inline-end: 0.3rem;
  font-size: 0.7rem;
  font-weight: 900;
  color: var(--vx-good);
}

/* Phones. The floor goes UP here, not down, which looks backwards until you
   watch a 375px screen fit five tracks and then break "Nederlands" across two
   lines as "Nederland / s". Fewer, wider tiles read better than more, narrower
   ones when the label is a word nobody can hyphenate for you — the longest
   autonyms this list has to hold are "Bahasa Indonesia" and "Slovenščina".
   Four across on a phone still shows a dozen flags without scrolling. */
@media (max-width: 480px) {
  .lang-grid {
    grid-template-columns: repeat(auto-fill, minmax(5.25rem, 1fr));
    gap: 0.4rem;
  }
  .lang-tile { min-height: 4.9rem; padding: 0.5rem 0.3rem; }
  .lang-flag { max-width: 2.8rem; }
}

/* Very narrow phones (320px and the like): let it fall to three across rather
   than squeezing the tiles below a comfortable tap target. */
@media (max-width: 350px) {
  .lang-grid { grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr)); }
}

/* Someone who has asked for less motion gets no scale on tap. */
body.reduced-motion .lang-tile { transition: none; }
body.reduced-motion .lang-tile:active { transform: none; }

.modal-result { overflow: hidden; }

/* The decorative header bleeds past the padding of the body, so it sits
   outside it rather than inside. */
.result-top {
  position: relative;
  padding: 0.8rem 1rem 0.3rem;
  text-align: center;
  background:
    radial-gradient(ellipse at 50% 120%, rgba(108, 154, 245, 0.22), transparent 70%);
}

.ribbon {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 1.6rem;
  border-radius: 8px;
  background: linear-gradient(180deg, #a855f7 0%, #7c3aed 100%);
  border: 2px solid rgba(255, 255, 255, 0.25);
  box-shadow: 0 5px 0 #5b21b6, 0 12px 22px rgba(0, 0, 0, 0.5);
  font-size: clamp(18px, 1.2rem, 28px);
  font-weight: 900;
  letter-spacing: 0.08em;
  color: #fff;
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.4);
}

/* The folded tails either side of the banner. */
.ribbon::before,
.ribbon::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 1.1rem;
  height: 1.6rem;
  transform: translateY(-50%);
  background: #6d28d9;
  z-index: -1;
}
.ribbon::before { left: -0.9rem; clip-path: polygon(0 0, 100% 20%, 100% 80%, 0 100%, 28% 50%); }
.ribbon::after  { right: -0.9rem; clip-path: polygon(100% 0, 0 20%, 0 80%, 100% 100%, 72% 50%); }

.ribbon-star { color: var(--vx-accent); font-size: 0.85em; }

#gameover-title.win  { color: #d9ffd0; }
#gameover-title.lose { color: #ffffff; }

/* ---- mascot ---- */

.mascot {
  position: relative;
  display: grid;
  place-items: center;
  height: clamp(60px, 3.7rem, 92px);
  margin-top: 0.35rem;
}

.mascot-glow {
  position: absolute;
  width: 120%;
  height: 100%;
  background: radial-gradient(ellipse at center, rgba(255, 217, 74, 0.34), transparent 65%);
}

/* A cube drawn as three faces — top, front, right — so it belongs to the same
   family as the blocks on the board rather than looking like a flat sticker. */
.mascot-cube {
  position: relative;
  width: clamp(40px, 2.5rem, 60px);
  height: clamp(40px, 2.5rem, 60px);
  border-radius: 8px;
  background: linear-gradient(180deg, #ffd23f 0%, #e8a80f 100%);
  box-shadow: 0 6px 0 #a9760a, 0 14px 22px rgba(0, 0, 0, 0.5);
  animation: mascot-bob 2.6s ease-in-out infinite;
}

.mascot-top {
  position: absolute;
  left: 8%;
  right: 8%;
  top: -14%;
  height: 26%;
  border-radius: 6px;
  background: #ffe98a;
}

.mascot-side {
  position: absolute;
  right: -12%;
  top: 12%;
  width: 22%;
  height: 76%;
  border-radius: 5px;
  background: #c68f09;
}

.mascot-face {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 22%;
  color: #4a3105;
  font-size: clamp(13px, 0.85rem, 19px);
  font-weight: 900;
}
.mascot-face i { font-style: normal; }

@keyframes mascot-bob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-0.3rem) rotate(2deg); }
}

/* ---- confetti ---- */

.confetti-bits { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.confetti-bits span {
  position: absolute;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  opacity: 0.9;
  animation: confetti-drift 3.4s ease-in-out infinite;
}
.confetti-bits span:nth-child(1) { left: 6%;  top: 22%; background: #5cb531; }
.confetti-bits span:nth-child(2) { left: 16%; top: 58%; background: #ff5a7a; animation-delay: .3s; }
.confetti-bits span:nth-child(3) { left: 27%; top: 12%; background: #4b8ef0; animation-delay: .7s; }
.confetti-bits span:nth-child(4) { left: 38%; top: 68%; background: #ffd94a; animation-delay: 1.1s; }
.confetti-bits span:nth-child(5) { left: 62%; top: 16%; background: #c084fc; animation-delay: .5s; }
.confetti-bits span:nth-child(6) { left: 72%; top: 62%; background: #4fd1e0; animation-delay: .9s; }
.confetti-bits span:nth-child(7) { left: 83%; top: 26%; background: #ff8a3d; animation-delay: 1.4s; }
.confetti-bits span:nth-child(8) { left: 92%; top: 54%; background: #5cb531; animation-delay: .2s; }
.confetti-bits span:nth-child(9) { left: 48%; top: 8%;  background: #ff5a7a; animation-delay: 1.7s; }
.confetti-bits span:nth-child(10){ left: 57%; top: 72%; background: #4b8ef0; animation-delay: 2.1s; }

@keyframes confetti-drift {
  0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.85; }
  50%      { transform: translateY(-8px) rotate(180deg); opacity: 1; }
}

/* ---- score ---- */

.gameover-sub {
  font-size: clamp(12px, 0.76rem, 16px);
  color: var(--vx-ink-soft);
  text-align: center;
}
.gameover-sub b { color: var(--vx-ink); }

.score-display { text-align: center; padding: 0 0 0.15rem; }

.score-label {
  font-size: clamp(11px, 0.62rem, 14px);
  letter-spacing: 0.16em;
  color: var(--vx-accent-2);
  font-weight: 800;
}

/* Stacked shadows fake the extruded, glowing numerals of the reference. */
.final-score {
  font-size: clamp(38px, 2.6rem, 64px);
  font-weight: 900;
  line-height: 1.02;
  color: #ffd23f;
  text-shadow:
    0 3px 0 #c8920b,
    0 6px 0 #a9760a,
    0 10px 24px rgba(255, 190, 40, 0.45);
}

.best-badge {
  display: inline-block;
  margin-top: 0.3rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: var(--vx-good);
  color: #08210a;
  font-size: clamp(9px, 0.58rem, 12px);
  font-weight: 900;
  letter-spacing: 0.1em;
}
.best-badge[hidden] { display: none; }

/* ---- stat cards ---- */

.results-stats { display: flex; gap: 0.4rem; }

.result-card {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.3);
  border: 2px solid var(--vx-edge);
  min-width: 0;
}

.result-ico {
  display: grid;
  place-items: center;
  width: clamp(26px, 1.7rem, 36px);
  height: clamp(26px, 1.7rem, 36px);
  flex: none;
  border-radius: 8px;
  font-size: clamp(13px, 0.85rem, 18px);
}

.result-meta { display: flex; flex-direction: column; min-width: 0; }
.result-card .label {
  font-size: clamp(9px, 0.55rem, 12px);
  letter-spacing: 0.08em;
  font-weight: 800;
  white-space: nowrap;
}
.result-card .val { font-size: clamp(15px, 1rem, 22px); font-weight: 900; }

/* Each card carries its own tint, as in the reference. */
.tint-purple { border-color: rgba(168, 85, 247, 0.45); }
.tint-purple .result-ico { background: rgba(168, 85, 247, 0.22); }
.tint-purple .label { color: #c9a6fb; }

.tint-blue { border-color: rgba(75, 142, 240, 0.45); }
.tint-blue .result-ico { background: rgba(75, 142, 240, 0.22); }
.tint-blue .label { color: #9dc0f8; }

.tint-green { border-color: rgba(92, 181, 49, 0.45); }
.tint-green .result-ico { background: rgba(92, 181, 49, 0.22); }
.tint-green .label { color: #a8dd8c; }

/* ---- rewards ---- */

.earned-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.earned-pill {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.42rem 0.7rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.3);
  border: 2px solid var(--vx-edge);
  font-size: clamp(11px, 0.68rem, 14px);
  font-weight: 800;
  color: var(--vx-ink);
}

.earned-pill .pill-ico {
  display: grid;
  place-items: center;
  width: clamp(24px, 1.6rem, 34px);
  height: clamp(24px, 1.6rem, 34px);
  flex: none;
  border-radius: 999px;
  font-size: clamp(13px, 0.85rem, 18px);
  background: rgba(255, 217, 74, 0.2);
}

.earned-pill .pill-meta { display: flex; flex-direction: column; min-width: 0; }
.earned-pill .pill-title { color: var(--vx-accent); letter-spacing: 0.04em; }
.earned-pill .pill-sub {
  font-size: clamp(9px, 0.58rem, 12px);
  font-weight: 600;
  color: var(--vx-ink-soft);
}
/* The coins banked are the run's headline reward, so the pill takes a full row
   of its own and centres on it. Sized to content it sat as a stub against the
   left edge — beside a wide achievement pill when there was one, and alone in
   an empty row when there was not, either way out of line with the stat cards
   above and the buttons below, which both span the body. */
.earned-pill.coin {
  flex: 1 1 100%;
  justify-content: center;
  gap: 0.55rem;
}
.earned-pill.coin .pill-meta { text-align: center; }
.earned-pill.coin .pill-title {
  font-size: clamp(15px, 1rem, 22px);
  font-weight: 900;
}

/* Achievements and world unlocks take a full row each for the same reason —
   a shared row leaves two pills of different widths and a ragged edge. */
.earned-pill.wide { flex: 1 1 100%; }

/* ---- double coins ---- */

.btn-double {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  border-color: rgba(255, 255, 255, 0.3);
  color: #241d02;
  box-shadow: 0 5px 0 #9d7f1e, 0 10px 20px rgba(0, 0, 0, 0.4);
  font-size: clamp(12px, 0.78rem, 16px);
  letter-spacing: 0.06em;
}
.btn-double:active { box-shadow: 0 1px 0 #9d7f1e, 0 4px 10px rgba(0, 0, 0, 0.4); }
.btn-double[hidden] { display: none; }
.btn-double:disabled { opacity: 0.5; }

.double-ico {
  display: grid;
  place-items: center;
  width: clamp(22px, 1.4rem, 30px);
  height: clamp(22px, 1.4rem, 30px);
  flex: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.28);
  color: #ffe98a;
  font-size: clamp(10px, 0.62rem, 14px);
}
.double-text { flex: 1; text-align: left; font-weight: 900; }
.double-amount {
  flex: none;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.28);
  color: #fff3bd;
  font-weight: 900;
}

/* ---- actions ---- */

.modal-result .modal-actions { gap: 0.5rem; margin-top: 0.2rem; }
.btn-result { min-height: clamp(46px, 2.9rem, 58px); font-size: clamp(13px, 0.86rem, 18px); }

.btn-info {
  background: linear-gradient(180deg, #4b8ef0 0%, #2f6ed0 100%);
  border-color: rgba(255, 255, 255, 0.25);
  color: #06152e;
  box-shadow: 0 5px 0 #1f4d95, 0 10px 20px rgba(0, 0, 0, 0.4);
}
.btn-info:active { box-shadow: 0 1px 0 #1f4d95, 0 4px 10px rgba(0, 0, 0, 0.4); }
/* Short viewports: the decorative half of the panel gives up its room first, so
   the stats, the offer and the buttons all still land inside the panel rather
   than behind a scrollbar. */
@media (max-height: 41rem) {
  .result-top { padding: 0.55rem 1rem 0.2rem; }
  .mascot { height: clamp(46px, 2.8rem, 70px); margin-top: 0.2rem; }
  .mascot-cube { width: clamp(34px, 2.1rem, 50px); height: clamp(34px, 2.1rem, 50px); }
  .final-score { font-size: clamp(32px, 2.15rem, 52px); }
  .modal-result .modal-body { gap: 0.4rem; padding: 0.7rem; }
  .earned-pill { padding: 0.35rem 0.6rem; }
}

body.reduced-motion .mascot-cube,
body.reduced-motion .confetti-bits span { animation: none; }

/* ==========================================================================
   FLOATING SCORE POPUPS + TOASTS
   ========================================================================== */

.floating-points {
  position: fixed;
  left: 50%;
  top: 38%;
  transform: translateX(-50%);
  z-index: 13;
  font-size: clamp(20px, 1.3rem, 30px);
  font-weight: 900;
  color: var(--vx-accent);
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.5), 0 6px 14px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  animation: float-up 750ms ease-out forwards;
}

@keyframes float-up {
  0%   { opacity: 0; transform: translate(-50%, 0.6rem); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -2.2rem); }
}

.toast-stack {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: clamp(120px, 9rem, 180px);
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  pointer-events: none;
  width: max-content;
  max-width: 90vw;
}

.toast {
  padding: 0.5rem 0.85rem;
  border-radius: var(--radius-sm);
  background: var(--vx-panel);
  border: 2px solid var(--vx-edge);
  box-shadow: 0 4px 0 var(--vx-sink), 0 10px 22px rgba(0, 0, 0, 0.5);
  font-size: clamp(11px, 0.7rem, 14px);
  font-weight: 800;
  text-align: center;
  animation: toast-in 240ms ease, toast-out 300ms ease 2.5s forwards;
}
.toast.gold { border-color: var(--vx-accent); color: var(--vx-accent); }

/* Beating the record is the one toast worth reading mid-swipe, so it is a gold
   plate rather than another dark chip in the stack. */
.toast.toast-best {
  padding: 0.55rem 1.1rem;
  background: linear-gradient(180deg, #ffe98a 0%, var(--vx-accent) 100%);
  border-color: rgba(255, 255, 255, 0.35);
  color: #2a2103;
  font-size: clamp(13px, 0.86rem, 18px);
  font-weight: 900;
  letter-spacing: 0.08em;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
  box-shadow: 0 5px 0 #9d7f1e, 0 12px 26px rgba(0, 0, 0, 0.5);
  animation: best-toast-in 420ms cubic-bezier(0.2, 1.5, 0.4, 1), toast-out 300ms ease 2.5s forwards;
}

@keyframes best-toast-in {
  0%   { opacity: 0; transform: translateY(0.7rem) scale(0.8); }
  60%  { opacity: 1; transform: translateY(0) scale(1.08); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toast-in  { from { opacity: 0; transform: translateY(0.6rem); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(0.6rem); } }

/* ==========================================================================
   REDUCED MOTION
   The setting is a game rule (tiles snap), so it is honoured explicitly rather
   than only through the media query.
   ========================================================================== */

body.reduced-motion .brand-cube,
body.reduced-motion .toast,
body.reduced-motion .target-cube.hit,
body.reduced-motion .coin-slot.pop { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .brand-cube { animation: none; }
}

/* ==========================================================================
   NARROW SCREENS
   A left rail eats width the board needs on a phone, so below 30rem the three
   cards become a row tucked under the scoreboard.
   ========================================================================== */

@media (max-width: 30rem) {

  .dock { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .dock-name { display: none; }
  .dock-btn { min-height: clamp(48px, 2.9rem, 58px); }
}

/* ==========================================================================
   BLOCK COLOUR EDITOR
   ========================================================================== */

.swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.2rem, 1fr));
  gap: 0.4rem;
}

/* The swatch is a miniature of the block itself: face colour on top, the
   derived darker side below it, and the number in the ink the board will
   actually use — so what you pick is what you get. */
.swatch {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: clamp(52px, 3.2rem, 64px);
  border: 2px solid var(--vx-edge);
  border-radius: var(--radius-sm);
  overflow: hidden;
  cursor: pointer;
  font-family: inherit;
  font-weight: 900;
  font-size: clamp(13px, 0.85rem, 18px);
  box-shadow: 0 4px 0 var(--vx-sink), 0 8px 16px rgba(0, 0, 0, 0.35);
}
.swatch:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--vx-sink); }

.swatch-value { position: relative; z-index: 1; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.25); }

/* The real <input type="color"> sits invisibly over the swatch so a tap opens
   the OS picker — styling a colour input directly is not portable. */
.swatch input[type="color"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* A dock button that has run dry but can still be refilled by watching. The
   badge itself carries the offer: it used to read 0 with a second pip on the
   opposite corner, which put two marks on one small button and gave the more
   prominent one to a number nobody can spend.

   The play mark is drawn here rather than set as a glyph — the icon font is
   subset to the ligatures on screen and carries no play symbol, and a bare
   triangle character renders as a different shape on every platform. */
.dock-badge.video {
  min-width: 26px;
  width: 26px;
  height: 26px;
  padding: 0;
  top: -9px;
  right: -9px;
  background: var(--vx-bad);
  border: 2px solid rgba(255, 255, 255, 0.85);
  animation: video-pulse 1.6s ease-in-out infinite;
}

.dock-badge.video::before {
  content: '';
  width: 0;
  height: 0;
  /* Nudged right of centre: a triangle looks off-centre in a circle when its
     bounding box is centred, because its mass sits to the left. */
  margin-left: 2px;
  border-left: 9px solid #fff;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}

@keyframes video-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.14); }
}

body.reduced-motion .dock-badge.video { animation: none; }

/* Spent but refillable reads as available, not greyed out. */
.dock-btn.refillable { opacity: 1; }
.dock-btn.refillable .dock-ico { color: var(--vx-bad); }

/* ==========================================================================
   DEV BAR  (?dev=1 only — never present in a normal load)
   ========================================================================== */

.dev-bar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  top: calc(0.4rem + var(--safe-top));
  z-index: 95;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(6, 9, 14, 0.85);
  border: 1px solid var(--vx-edge);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6);
}

.dev-bar button {
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vx-ink-soft);
  font-family: inherit;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
  line-height: 1;
}
.dev-bar button:hover { background: rgba(255, 255, 255, 0.12); }
.dev-bar button.on { background: var(--vx-accent); color: #241d02; }

.dev-sep {
  width: 1px;
  height: 18px;
  margin: 0 4px;
  background: var(--vx-edge);
}

/* ==========================================================================
   DEV CAMERA OVERLAY  (?dev=1 only)
   ========================================================================== */

.dev-cam {
  position: fixed;
  left: 10px;
  bottom: 10px;
  z-index: 96;
  width: 250px;
  border-radius: 10px;
  background: rgba(6, 9, 14, 0.92);
  border: 1px solid var(--vx-edge);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.6);
  font-family: 'Outfit', system-ui, sans-serif;
  color: var(--vx-ink);
}

.dev-cam-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 8px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.12em;
  color: var(--vx-ink-soft);
  border-bottom: 1px solid var(--vx-edge);
}
.dev-cam-head button {
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.1);
  color: inherit;
  font-family: inherit;
  font-weight: 900;
  cursor: pointer;
  line-height: 1;
}

.dev-cam-body { padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.dev-cam.folded .dev-cam-body { display: none; }

.dev-cam-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'name val' 'slider slider';
  gap: 2px 6px;
  align-items: center;
}
.dev-cam-name { grid-area: name; font-size: 10px; color: var(--vx-ink-soft); letter-spacing: 0.06em; }
.dev-cam-val  { grid-area: val; font-size: 11px; font-weight: 900; color: var(--vx-accent); }
.dev-cam-row input[type="range"] { grid-area: slider; width: 100%; accent-color: var(--vx-accent); }

.dev-cam-read {
  margin-top: 2px;
  padding: 6px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.06);
  font-size: 10px;
  line-height: 1.5;
  color: var(--vx-ink-soft);
}
.dev-cam-read b { color: var(--vx-ink); }

.dev-cam-actions { display: flex; gap: 6px; }
.dev-cam-actions button {
  flex: 1;
  padding: 6px;
  border: 1px solid var(--vx-edge);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--vx-ink);
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.dev-cam-actions button:hover { background: rgba(255, 255, 255, 0.16); }
