:root {
  /* Redesign foundation tokens. Each value is light-dark(light, dark), resolved
     against the element's `color-scheme`: the palette follows the OS and can be
     pinned per theme below. */
  color-scheme: light dark;

  --background: light-dark(#f4f4f7, #0c0c11);
  --surface: light-dark(#ffffff, #20202a);
  --surface-2: light-dark(#f0f0f4, #25252f);
  --surface-3: light-dark(#e6e6ec, #2f2f3a);
  --card-background: var(--surface);
  --border: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.06));
  --border-strong: light-dark(rgba(0, 0, 0, 0.14), rgba(255, 255, 255, 0.12));
  --color: light-dark(#16161c, #eceef3);
  --text-dim: light-dark(#3d3d48, #c9cad3);
  --text-mute: light-dark(#6a6a77, #8a8a97);
  --text-faint: light-dark(#9a9aa5, #6a6a72);
  --accent: light-dark(hsl(220, 72%, 50%), hsl(220, 70%, 55%));
  --accent-bright: light-dark(hsl(220, 72%, 44%), hsl(220, 70%, 62%));
  --accent-soft: light-dark(rgba(79, 131, 230, 0.12), rgba(79, 131, 230, 0.14));
  --win: light-dark(hsl(120, 50%, 38%), hsl(120, 55%, 64%));
  --warn: light-dark(hsl(42, 90%, 42%), hsl(42, 85%, 62%));
  --lose: light-dark(hsl(8, 72%, 50%), hsl(8, 72%, 60%));
  --gold: light-dark(hsl(42, 90%, 40%), hsl(45, 85%, 60%));
  /* peak: a non-supreme best (tied for the field's top), between the ramp's
     green and the reserved supreme gold. Mirrors the PEAK_COLOR hex used where
     a fill needs readableInk (calendar cell). */
  --peak: light-dark(hsl(84, 62%, 36%), hsl(84, 66%, 56%));
  /* percentile ramp, low to high: red -> magenta -> blue -> teal -> green. It
     goes the long way round the wheel to avoid yellow/olive and stay vivid;
     green reads as best. One scale for the calendar heatmap AND the banded
     percentile UI (runs, today-result, result modal); supreme sits above it in
     gold. */
  --pct-0: #dc4a2f;
  --pct-1: #d13d94;
  --pct-2: #3f6be0;
  --pct-3: #29b3a6;
  --pct-4: #46b95a;
  --font: "Space Grotesk", system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
  --radius: 14px;
  --radius-sm: 10px;

  --maze-background: hsl(222, 55%, 77%);
  --maze-grid: hsla(222, 40%, 55%, 0.22);
  --maze-checkpoint: hsl(220, 65%, 52%);
  --maze-wall: #404040;
  --maze-text: white;
  --maze-upgrade-to-thunder: hsl(120, 60%, 65%);
  --maze-player-block: hsl(120, 60%, 80%);
  --maze-game-block: hsl(320, 60%, 80%);
  --maze-placing-block: hsl(120, 60%, 80%);
  --maze-placing-error: hsl(0, 100%, 60%);
  --maze-player-thunder: hsl(120, 60%, 50%);
  --maze-game-thunder: hsl(320, 60%, 50%);
  --maze-thunder-radius: hsla(220, 60%, 50%, 20%);
  --maze-debug-block: hsla(0, 100%, 60%, 10%);
  --maze-stroke: rgba(0, 0, 0, 0.4);
  --runner: hsl(300, 60%, 60%);
  --slow-runner: hsl(258, 36%, 53%);
  --brightness: light-dark(40%, 70%);

  --full-height: 100%;
  /* Height reserved above the board: the body's top inset (breathing room +
     notch) plus the header's height (content + bottom padding). Keep in step
     with `body` padding-top and `.app-header`. */
  --header-size: calc(76px + env(safe-area-inset-top, 0px));
  /* Footprint of the HUD strip above the board (min-height + bottom margin),
     subtracted in --maze-size. Keep in step with .hud. */
  --hud-size: 48px;
  --maze-size: min(
    680px,
    100vw,
    var(--full-height) - var(--header-size) - var(--hud-size) - 8px
  );
}

/* Explicit theme pins from the settings screen. `system` (or no attribute)
   keeps `color-scheme: light dark` so light-dark() tracks the OS. */
html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }

* {
  box-sizing: border-box;
  /* iOS Safari needs the -webkit- prefix to stop double-tap from selecting text
     (e.g. the SVG HUD). Nothing here is meant to be selectable. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Clip the board's scale(2) zoom to the viewport: it may overflow its container
   over the surrounding UI but must not grow the page (which shifts the header
   and adds scroll on iOS). Full height so the clip reaches the bottom of the
   screen. */
html,
body {
  height: 100%;
  overflow: hidden;
}

body {
  font-family: var(--font);
  margin: 0;
  /* Top breathing room and the notch inset live here, not on the header, so the
     gap above it is a page-level inset. Folded into --header-size; border-box
     keeps padding + the 100% height inside the viewport. */
  box-sizing: border-box;
  padding-top: calc(32px + env(safe-area-inset-top, 0px));
  overscroll-behavior: none;
  touch-action: none;
  user-select: none;
  background-color: var(--background);
  color: var(--color);
}

/* Tabular monospace for times, scores, and other figures. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Press feedback shared across tappable controls. */
.tapc {
  transition: transform 0.08s ease, background 0.12s ease, opacity 0.12s ease;
  /* Suppress the mobile tap flash: it ignores border-radius (squaring off
     rounded controls), and .tapc has its own press feedback (the scale below).
     */
  -webkit-tap-highlight-color: transparent;
}

.tapc:active {
  transform: scale(0.96);
}

/* App header: logo + wordmark, with breathing room below (the notch clearance
   is on `body`). */
.app-header {
  display: grid;
  justify-content: center;
  align-items: center;
  gap: 24px;
  /* Mirror the .game grid (column template and padding are set per breakpoint
     to match) so the wordmark centers over the board and the avatar lines up
     with the right-hand panel's edge, not the viewport. The top inset lives on
     `body`. */
  padding: 0 16px 20px;
  /* Floor the row at the action buttons' height (36px), the tallest thing the
     header holds (logo is 24px). Without it the row grows ~12px when the
     avatar/calendar buttons load and pushes the board down; a late web-font
     swap would too. This keeps --header-size fixed from first paint. It is a
     row floor, not a container min-height: the header is border-box, so its
     padding already clears one. */
  grid-auto-rows: minmax(36px, auto);
  /* The pull-to-refresh gesture grabs the header (the board owns touch below
     it; see usePullToRefresh). Claim vertical drags so a browser pan/bounce
     can't interrupt it. Nothing scrolls from the header. */
  touch-action: none;
}

/* Pull-to-refresh indicator: a spinner chip that descends from behind the top
   edge tracking the pull, then spins while the reload fires. Fixed, above the
   dock/result, below the calendar picker (50) and modals (60). */
.pull-refresh {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) - 40px);
  left: 50%;
  margin-left: -18px;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  /* Themed via light-dark() tokens; the spinner arc takes a touch of the
     player's avatar hue (set inline). */
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
  pointer-events: none;
  /* Weighted ease back to rest on release. The finger phase turns it off
     (--dragging) so the pull tracks 1:1. */
  transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.28s ease;
}

.pull-refresh__spinner {
  transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1);
}

/* While the finger drives it, no easing: the chip and spinner follow the pull
   exactly. */
.pull-refresh--dragging,
.pull-refresh--dragging .pull-refresh__spinner {
  transition: none;
}

.pull-refresh--active .pull-refresh__spinner {
  animation: pullSpin 0.7s linear infinite;
}

@keyframes pullSpin {
  to {
    transform: rotate(360deg);
  }
}

.app-header__brand {
  justify-self: center;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Header icon buttons (calendar, profile): 36px rounded squares with a subtle
   surface fill, so the icons read as tap targets. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-mute);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, transform 0.08s ease;
}

.icon-button:hover {
  background: var(--surface-2);
  color: var(--color);
}

.profile {
  /* Anchor for the copied-link tooltip; the rest comes from .icon-button. */
  position: relative;
}

.profile__avatar {
  display: block;
  /* Decorative: the game's global touch handlers treat any touched <svg> as a
     board interaction (zooming it), so keep taps on the button, not the glyph.
     */
  pointer-events: none;
}

/* Header profile button once loaded: the user's letter avatar (fill set inline
   from their id), matching the dialog avatar. */
.profile-avatar-btn {
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  cursor: pointer;
}

.profile-avatar-btn__initial {
  font: 700 15px var(--font);
  line-height: 1;
}

/* Right-hand header cluster: the (mobile-only) calendar button and profile. */
.app-header__actions {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Hidden on desktop, where the calendar sits inline in the left column (shown
   in the max-width block below). After .icon-button so this display wins over
   its inline-flex. */
.cal-button {
  display: none;
  position: relative;
}

/* Persistent attention when a new daily is available (local midnight rolled
   over): an accent pulse + corner dot, held until the player opens the calendar
   and switches. Not dismissible. */
.cal-button--alert {
  color: var(--accent);
  animation: pbPulse 1.6s ease-in-out infinite;
}

.cal-button__dot {
  position: absolute;
  top: -2px;
  right: -2px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--background);
}

@media (prefers-reduced-motion: reduce) {
  .cal-button--alert {
    animation: none;
  }
}

.profile__tooltip {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 6px;
  padding: 2px 8px;
  border-radius: 4px;
  background: #444d;
  color: #fff;
  font-size: 12px;
  white-space: nowrap;
  z-index: 1;
}

.app-header h1 {
  margin: 0;
  font: 700 20px/1 var(--font);
  letter-spacing: 0.02em;
  color: var(--color);
}

/* Framed maze board. Rounded corners are on the SVG itself (not overflow:hidden
   here) so the scale(2) zoom can still spill over the UI. */
.board-frame {
  max-width: var(--maze-size);
  margin: 0 auto;
  border-radius: var(--radius);
  box-shadow: 0 10px 30px -14px rgba(0, 0, 0, 0.7);
}

.board-frame > svg {
  width: 100%;
  display: block;
  border-radius: var(--radius);
  font-family: var(--font);
  /* Disable iOS double-tap-to-zoom so taps on interactive text (e.g. "Ready?")
     activate. */
  touch-action: manipulation;
}

/* Full-screen fallback when a render crash unmounts the app (see
   ErrorBoundary): a plain card and a reload button. */
.crash {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.crash__card {
  max-width: 360px;
  padding: 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: grid;
  gap: 12px;
  justify-items: center;
  text-align: center;
}

.crash__title {
  font-size: 18px;
  font-weight: 700;
}

.crash__sub {
  color: var(--text-mute);
  font-size: 14px;
}

/* Ghost of a reverted (never-persisted) block: puffs up, then collapses where
   it stood, so the rollback reads as an implosion rather than a glitch.
   transform-box scopes the scale to the block (SVG transforms otherwise
   originate at the viewBox corner). */
.board-implode {
  transform-box: fill-box;
  transform-origin: center;
  animation: implode 0.28s ease-in forwards;
  pointer-events: none;
}

@keyframes implode {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  35% {
    transform: scale(1.2);
    opacity: 1;
  }
  100% {
    transform: scale(0);
    opacity: 0.4;
  }
}

.btn {
  padding: 10px 16px;
  margin: 1px;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  font: 600 15px var(--font);
  cursor: pointer;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.card {
  padding: 12px;
  margin: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-background);
}

a {
  color: var(--accent-bright);
  font-weight: bold;
}

a:hover {
  filter: brightness(75%);
  cursor: pointer;
}

/* Onboarding gets the board's side breathing room so the board doesn't run to
   the screen edges on mobile. */
.onboarding {
  padding-inline: 8px;
}

.tooltip {
  max-width: max(30%, 200px);
  /* A panel colour (not the page background) so the box and its arrow read
     against the background above the board. */
  background: var(--surface);
  padding: 8px;
  border-radius: 4px;
  position: absolute;
  text-align: left;
  /* The set position (top/left/right/bottom) marks where the ARROW points;
     shift the box by the arrow's fixed offset so the tip lands there (12px
     inset + 7px half-base = 19px in; 7px past the edge). */
  transform: translate(var(--arrow-x, -19px), var(--arrow-y, 0px));
}

/* The tip's buttons sit on one row in a box only as wide as the board allows; a
   two-word label ("Watch again") must not wrap mid-phrase into the neighbouring
   button. */
.tooltip a {
  white-space: nowrap;
}

/* Arrow as a pseudo-element on the tip so it is always flush to its edge. */
.tooltip::before {
  content: "";
  position: absolute;
  /* Fixed size: it points at fixed-size chips outside the board, so it must not
     scale with the board like the % positions. */
  left: 12px;
  border: 7px solid transparent;
}

.tooltip--right::before {
  left: auto;
  right: 12px;
}

/* Box-shift so the arrow tip (not the box corner) sits at the set position. */
.tooltip--right {
  --arrow-x: 19px;
}

.tooltip--up::before {
  bottom: 100%;
  border-bottom-color: var(--surface);
}

.tooltip--up {
  --arrow-y: 7px;
}

.tooltip--down::before {
  top: 100%;
  border-top-color: var(--surface);
}

.tooltip--down {
  --arrow-y: -7px;
}

/* Game layout: board in the middle, daily-selector and log as side panels on
   landscape and stacked below on portrait. Nothing here clips overflow, so the
   board's scale(2) zoom spills over the panels (only <body> clips). */
.game {
  display: grid;
  gap: 24px;
  /* The right rail's spacing between the dock and the runs. */
  --rail-gap: 12px;
  justify-content: center;
  align-items: start;
  box-sizing: border-box;
  padding-inline: 16px;
  height: calc(var(--full-height) - var(--header-size));
}

/* Three columns when wide enough: today-result + calendar left, board centred,
   attempts right. Width-based, not orientation, so a tall-but-wide window still
   gets the desktop layout; side columns are capped so wide screens keep outer
   margins. Narrower: board on top, then result, attempts, calendar stacked;
   attempts and calendar scroll within their own height so the board never
   moves. The board stays top-aligned (align-items: start); the scrolling rail
   opts back into filling its track with align-self: stretch. Side panels clip
   only their OWN content, so the board's scale(2) spill is not clipped. */
@media (min-width: 1200px) {
  .game {
    grid-template-columns:
      minmax(0, 340px) min(var(--maze-size), 100%) minmax(0, 340px);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas:
      "today board rail"
      "cal   board rail";
  }

  /* The right rail owns its vertical rhythm: the dock, then the runs, spaced
     by --rail-gap. Spanning both grid rows frees it
     from the left column's row heights (the today card is taller than the
     dock). */
  .game__rail {
    grid-area: rail;
    gap: var(--rail-gap);
    /* .game is align-items: start (the board stays top-aligned), so the rail
       would size to its CONTENT and the runs' flex: 1 would have no bounded
       track: the list grew past the fixed .game height, which clips without
       scrolling. Stretch restores the spanned track's height, making .attempts
       a scroll container. (Mobile scrolls the whole .game.) */
    align-self: stretch;
    min-height: 0;
  }

  /* The dock is its natural height; the runs take the rest and scroll. */
  .game__rail > .attempts {
    flex: 1;
    min-height: 0;
  }

  /* The wrapper vanishes so its three panels land directly in the grid. */
  .game__panel {
    display: contents;
  }

  /* Same three columns as the board grid: brand over the board, avatar pinned
     to the attempts column's edge. */
  .app-header {
    grid-template-columns:
      minmax(0, 340px) min(var(--maze-size), 100%) minmax(0, 340px);
  }

  .app-header__brand {
    grid-column: 2;
  }

  .app-header__actions {
    grid-column: 3;
  }
}

@media (max-width: 1199.98px) {
  /* Tighter top inset on phones. Keep in step with --header-size (inset + 44px
     header) so the board fits. */
  :root {
    --header-size: calc(60px + env(safe-area-inset-top, 0px));
  }

  body {
    padding-top: calc(16px + env(safe-area-inset-top, 0px));
  }

  /* Stacked layout that scrolls as one unit: the board sits full-size on top
     and the whole .game scrolls to the panels. overflow-x is clipped so the
     board's scale(2) zoom can't add a horizontal scrollbar (unlike desktop, the
     zoom is clipped to the game viewport). Rows are auto so content can exceed
     the fixed .game height. */
  .game {
    grid-template-columns: min(var(--maze-size), 100%);
    grid-template-rows: auto auto;
    grid-template-areas:
      "board"
      "panel";
    /* Pack the board + panel at the top. Without this the auto rows stretch to
       fill the fixed height (default align-content), pushing the runs into the
       middle. */
    align-content: start;
    /* Tighter than the desktop 24px: ~20px total with the panel's top padding,
       matching header to hud. */
    row-gap: 12px;
    padding-inline: 8px;
    overflow-x: hidden;
    overflow-y: auto;
  }

  /* One board-width column matching .game: the wordmark left-aligns to the
     board's left edge and the actions pin to its right edge (both share the
     single cell via justify-self). With only two icons, left wordmark + right
     actions reads better than centering. */
  .app-header {
    grid-template-columns: min(var(--maze-size), 100%);
    padding-inline: 8px;
  }

  .app-header__brand,
  .app-header__actions {
    grid-area: 1 / 1;
  }

  .app-header__brand {
    justify-self: start;
  }

  /* The calendar lives in a full-screen picker here, opened from the header. */
  .cal-button {
    display: inline-flex;
  }

  .game__panel {
    grid-area: panel;
    display: flex;
    flex-direction: column;
    gap: 16px;
    /* Vertical spacing only; the horizontal gutter is .game's padding-inline.
       Bottom clears the pinned standings dock (~64px), breathing room and the
       home-indicator inset so the last run row scrolls out from under it. */
    padding-block: 4px calc(92px + env(safe-area-inset-bottom, 0px));
  }

  /* Natural height, no flex compression. */
  .game__panel > * {
    flex-shrink: 0;
  }

  /* The calendar is a picker on mobile, so the runs expand to full height and
     the whole page scrolls. The descendant selector beats the later base
     .attempts rule by specificity. */
  .game__panel .attempts {
    padding: 0;
  }

  /* Today's result mostly duplicates the board state on mobile: drop it and
     lead with the runs. */
  .game__panel .today-result {
    display: none;
  }
}

/* Dock + runs as one column. The desktop block gives it the grid area spanning
   both rows and the gap. On mobile it passes its children through to the
   stacked panel (the dock is pinned to the viewport), so it needs no spacing.
   */
.game__rail {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.game__board {
  grid-area: board;
  min-width: 0;
  /* Anchor the daily "Start attempt" overlay (Prestart.tsx) to the board. */
  position: relative;
}

/* Daily "Start attempt" gate (Prestart.tsx). A ranked attempt never opens on
   load, so a background boot can't spend one. A themed blur mask over the
   board, backed by a blurred decorative copy of the tutorial maze (NOT today's
   puzzle, so the mask can't leak the layout the player is about to build). */
.prestart {
  position: absolute;
  /* Match the board square (not the taller game__board, which includes the Hud
     strip) so the mask is symmetric. Same width, centering and rounding as
     `.board-frame`. */
  bottom: 0;
  /* Center off a DEFINITE width (100% capped at --maze-size), not
     left:0/right:0 + margin-auto. Safari mis-derives an absolutely-positioned
     box's aspect-ratio height from a left+right stretch: the square renders too
     short and, with the blurred deco and scrim both inset:0, exposes a sharp
     strip of the real board above the overlay. */
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--maze-size);
  aspect-ratio: 1;
  z-index: 10;
  display: flex;
  /* A column so the account footer (.prestart__account) sits in flow below the
     content (auto margins keep that centred above it), never overlapping the
     start button on a small board. */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
  overflow: hidden;
  border-radius: var(--radius);
  /* Safari's overflow:hidden + border-radius does NOT clip a child with a
     `filter` (the blurred .prestart__deco), so its square corners poke past the
     card. clip-path clips the whole subtree, filtered descendants included, in
     both engines. */
  clip-path: inset(0 round var(--radius));
  /* New stacking context so the deco/scrim/content z-indexes order against each
     other, not the rest of the board. */
  isolation: isolate;
}

/* Bottom layer: the tutorial maze, blurred + faded to a texture. Its
   board-frame border, shadow and rounding are stripped so it fills edge to edge
   instead of reading as a nested board. */
.prestart__deco {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* 18px folds in the 7px blur a scrim backdrop-filter would add: radii compose
     in quadrature, sqrt(16² + 7²) ≈ 17.5. See the scrim below for why it has
     none. */
  filter: blur(18px) saturate(0.9);
  opacity: light-dark(0.5, 0.6);
  /* Own composited backing, so a board-sized `filter: blur` rasterizes as one
     texture instead of per raster-tile. Does NOT affect the scrim's tab-hover
     seam (see below). */
  transform: translateZ(0);
}

.prestart__deco .board-frame {
  max-width: none;
  margin: 0;
  border-radius: 0;
  box-shadow: none;
  height: 100%;
}

.prestart__deco .board-frame > svg {
  height: 100%;
  border-radius: 0;
}

/* Middle layer: a themed scrim that dims the deco for text contrast. Above the
   deco, below the content.

   It must carry NO `backdrop-filter`: that caused a tab-hover seam, a hairline
   across the overlay whenever Chrome captured the tab preview thumbnail,
   cleared only by a full repaint. Chrome renders a backdrop-filter from a
   per-raster-tile snapshot of the parent surface; the thumbnail capture
   re-rasterizes at reduced scale, the blur kernel clamps at tile edges, and the
   seam bakes into the cached texture. Unlike the deco's `filter`, layer
   promotion can't fix it.

   Nothing is lost without it: its blur on the deco is folded into the deco's
   own (see the quadrature note above). It would also blur the real board behind
   the overlay, which `enterPrestart` empties (`setBlocks([])`) to a bare grid.
   It does reach that board: `isolation: isolate` and `clip-path` on .prestart
   don't create a backdrop root in Chrome despite the spec. */
.prestart::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: light-dark(rgba(244, 244, 247, 0.64), rgba(9, 9, 14, 0.62));
}

.prestart__content {
  position: relative;
  z-index: 2;
  margin: auto 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-width: 320px;
}

.prestart__eyebrow {
  margin-top: 12px;
  font: 600 12px var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-mute);
}

.prestart__title {
  margin: 4px 0 0;
  font: 800 clamp(26px, 6vw, 34px) var(--font);
  color: var(--color);
}

.prestart__sub {
  margin: 8px 0 0;
  font: 400 14px var(--font);
  color: var(--text-mute);
}

/* Progress pips, rounded squares: spent attempts filled green, the current one
   accent (soft ring), the rest an empty outline. */
.prestart__dots {
  display: flex;
  gap: 10px;
  margin: 18px 0 0;
}

.prestart__dot {
  width: 15px;
  height: 15px;
  border-radius: 5px;
  border: 1.5px solid var(--border-strong);
  background: transparent;
}

.prestart__dot--done {
  border-color: var(--win);
  background: var(--win);
}

.prestart__dot--active {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* The previous attempt's time and, from the third attempt, a diff against the
   one before it: green when improved (higher is better), red when dropped. */
.prestart__prev {
  margin-top: 16px;
  font: 500 14px var(--font);
  color: var(--text-mute);
}

.prestart__prev .mono {
  font-weight: 700;
  color: var(--color);
}

.prestart__diff {
  margin-left: 8px;
  font-weight: 700;
}

.prestart__diff--up {
  color: var(--win);
}

.prestart__diff--down {
  color: var(--lose);
}

.prestart__diff--even {
  color: var(--text-mute);
}

.prestart__btn {
  margin-top: 20px;
  min-width: 240px;
  padding-block: 14px;
  justify-content: center;
  font-weight: 700;
  box-shadow: 0 10px 30px -8px var(--accent-soft);
}

/* The account footer: a quiet text link in the overlay's bottom padding.
   Negative margin pulls it into that padding so it doesn't shift the content;
   padded for a tap target without looking like a button. */
.prestart__account {
  position: relative;
  z-index: 2;
  flex: none;
  margin: 4px 0 -14px;
  padding: 6px 10px;
  border: 0;
  background: none;
  font: 500 12px var(--font);
  color: var(--text-mute);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.prestart__account:hover,
.prestart__account:focus-visible {
  color: var(--color);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Shared panel heading (Today / Previous days / Attempts). */
.section-title {
  font: 600 12px var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-mute);
  text-align: left;
  margin: 0 4px 10px;
}

/* Today's result: two summary chips (see TodayResult.tsx). */
.today-result {
  grid-area: today;
  text-align: left;
}

.today-result__chips {
  display: flex;
  gap: 8px;
}

/* Each chip is tinted to its percentile band (--chip, set inline); supreme adds
   the gold glow, matching the result modal. Border and faint fill come from
   .band-card. */
.today-result__chip {
  flex: 1;
  min-width: 0;
  padding: 12px 14px;
  border-radius: 14px;
  --band-card-tint: var(--chip);
}

.today-result__chip--glow {
  box-shadow: 0 0 12px -3px color-mix(in srgb, var(--chip) 60%, transparent);
}

.today-result__head {
  font: 600 11px var(--font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-mute);
}

.today-result__value {
  font-size: 22px;
  font-weight: 700;
  margin-top: 5px;
  color: var(--chip);
}

.today-result__sub {
  font: 500 11px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

/* Clickable chips/rows review that maze on the board. */
.today-result__chip--clickable {
  cursor: pointer;
  transition: background 0.12s ease;
}

.today-result__chip--clickable:hover {
  --band-card-fill: 9%;
}

/* Calendar: a bounded one-month picker, same on desktop and mobile (see
   Calendar.tsx). No scroll: prev/next month and a jump-a-year row page through
   the loaded window. */
.calendar {
  grid-area: cal;
  align-self: start;
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* Mobile full-screen calendar picker (opened from the header button). Covers
   the board; tap the backdrop or a day to dismiss. */
.calendar-modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--background);
  overflow-y: auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  box-sizing: border-box;
  padding: calc(8px + env(safe-area-inset-top, 0px)) 16px
    calc(24px + env(safe-area-inset-bottom, 0px));
}

.calendar-modal__sheet {
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* One header row: title left, pager + close grouped right. Sticky so it stays
   put while scrolling multiple months. */
.calendar-modal__header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
  padding: 6px 0 10px;
  background: var(--background);
}

.calendar-modal__header .section-title {
  margin: 0;
}

.calendar-modal__tools {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Larger cells than the desktop inline grid, for tapping. */
.calendar-modal__sheet .calendar__grid {
  gap: 7px;
}

.calendar-modal__sheet .calendar__cell .mono {
  font-size: 14px;
}

.calendar-modal__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--color);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

.attempts::-webkit-scrollbar {
  width: 8px;
}

.attempts::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

.attempts::-webkit-scrollbar-track {
  background: transparent;
}

/* "New daily available" banner at the top of the calendar (desktop inline +
   mobile picker): the switch to the day that just rolled over. That day isn't a
   normal cell (the calendar lists only days you've played), so this is the way
   to it. */
.calendar__new-daily {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  margin: 0 0 12px;
  padding: 11px 13px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--color);
  font: 600 13px var(--font);
  text-align: left;
  cursor: pointer;
}

.calendar__new-daily-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: pbPulse 1.6s ease-in-out infinite;
}

.calendar__new-daily-text {
  flex: 1;
}

.calendar__new-daily-cta {
  color: var(--accent-bright);
  font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  .calendar__new-daily-dot {
    animation: none;
  }
}

/* Each month label sits below the grid above it; the top margin is the gap
   between months. */
.calendar__label {
  font: 700 14px var(--font);
  color: var(--color);
  margin: 16px 2px 6px;
}

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 5px;
}

.calendar__weekday {
  text-align: center;
  font: 600 10px var(--font);
  color: var(--text-mute);
  padding-bottom: 2px;
}

/* Rolling weeks view: a heading with a pager on the right, the weekday row,
   then week rows (oldest on top, current week at the bottom) with a month label
   wherever the month changes. */
.calendar__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.calendar__head .section-title {
  margin: 0;
}

.calendar__pager {
  display: flex;
  gap: 6px;
}

.calendar__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--color);
  font-size: 16px;
  cursor: pointer;
}

.calendar__arrow:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Leading cells that align a month segment's first day to its weekday. */
.calendar__blank {
  aspect-ratio: 1;
}

.calendar__cell {
  position: relative;
  aspect-ratio: 1;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--text-mute);
}

.calendar__cell .mono {
  font-size: 12px;
  font-weight: 700;
}

.calendar__cell--playable {
  cursor: pointer;
}

/* Days with no daily to replay yet: dimmer than a played-or-missed day. */
.calendar__cell--future {
  background: none;
  opacity: 0.4;
}

.calendar__cell--today {
  outline: 1.5px solid var(--border-strong);
  outline-offset: -1.5px;
}

.calendar__cell--selected {
  /* White ring contrasts more than the accent blue against the dark grid and
     never clashes with the cell's percentile colour. */
  box-shadow: 0 0 0 2px var(--color);
}

.calendar__dot {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.25);
}

/* Playable cell layout: the standing % is the hero, centred; the day shrinks
   into the top-left corner. Both inherit the cell's readable ink (see
   Calendar.tsx); the day is faded via opacity to work on light or dark fills.
   Long near-100 percentages step down a size to fit. */
.calendar__day,
.calendar__pct {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  line-height: 1;
}

.calendar__day {
  position: absolute;
  top: 4px;
  left: 5px;
  font-size: 9px;
  opacity: 0.72;
}

.calendar__pct {
  font-size: 13px;
}

.calendar__pct--long {
  font-size: 10px;
}

.calendar__pct--xlong {
  font-size: 8px;
  letter-spacing: -0.02em;
}

/* The larger picker cells fit a bigger hero. */
.calendar-modal__sheet .calendar__day {
  font-size: 10px;
}

.calendar-modal__sheet .calendar__pct {
  font-size: 16px;
}

.calendar-modal__sheet .calendar__pct--long {
  font-size: 12px;
}

.calendar-modal__sheet .calendar__pct--xlong {
  font-size: 9px;
}

/* Runs panel: the viewed maze's runs (see Attempts.tsx). No root padding so its
   heading/rows line up with the other panels; slim scrollbar on desktop (the
   only place it scrolls). Mobile min-height/full-width is in the max-width
   block above, as descendant selectors that win by specificity. */
.attempts {
  min-height: 0;
  overflow-y: auto;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
  /* Breathing room under the last row at the scroll end. Desktop only: the
     mobile rule below zeroes panel padding (the whole page scrolls there). */
  padding-bottom: 16px;
}

/* Heading row: the "Runs" title and the local sort toggle, on one line above
   the list. */
.attempts__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0 4px 10px;
}

.attempts__head .section-title {
  margin: 0;
}

/* Segmented Recent / Best sort control. */
.attempts__sort {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
}

.attempts__sort-btn {
  border: 0;
  background: transparent;
  color: var(--text-mute);
  font: 600 11px var(--font);
  padding: 3px 9px;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.attempts__sort-btn:hover {
  color: var(--color);
}

.attempts__sort-btn--active {
  background: var(--surface-3);
  color: var(--color);
}

.attempts__list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Each row is tinted to its own percentile band (--band, set inline); supreme
   rows add the gold glow. */
.attempts__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 13px;
  border-radius: 13px;
  /* Border + faint fill come from .band-card, tinted to this row's band. */
  --band-card-tint: var(--band);
}

.attempts__row--glow {
  box-shadow: 0 0 12px -3px color-mix(in srgb, var(--band) 60%, transparent);
}

.attempts__row--clickable {
  cursor: pointer;
  transition: background 0.12s ease;
}

.attempts__row--clickable:hover {
  --band-card-fill: 9%;
}

/* The run currently on the board (a review): keeps the row's band colour but
   lifts it with a heavier border, the deeper "highlighted" fill (shared with
   the daily-result best row) and a matching label, with no second (blue)
   accent. Only tunes the band-card knobs, so no specificity fight. */
.attempts__row--viewing {
  --band-card-edge: 60%;
  --band-card-fill: 13%;
}

.attempts__row--viewing:hover {
  --band-card-fill: 18%;
}

.attempts__viewing {
  margin-left: auto;
  flex: none;
  font: 600 11px var(--font);
  letter-spacing: 0.02em;
  color: var(--band);
}

.attempts__time {
  font-size: 17px;
  font-weight: 700;
  min-width: 52px;
  flex: none;
  color: var(--band);
}

.attempts__meta {
  flex: 1;
  min-width: 0;
  /* One line: "2m ago" plus its tags is one caption, and wrapping in the 288px
     rail grew the row whenever a run picked up a badge. Overflow rather than
     clip: a half-cut tag reads as a rendering fault. */
  white-space: nowrap;
}

.attempts__kind {
  display: flex;
  align-items: center;
  gap: 7px;
  font: 600 13px var(--font);
  color: var(--color);
}

.attempts__badge {
  font: 700 9px var(--font);
  letter-spacing: 0.1em;
  color: var(--band);
  background: color-mix(in srgb, var(--band) 16%, transparent);
  border-radius: 5px;
  padding: 2px 6px;
}

/* How many runs built this identical maze. */
.attempts__count {
  font: 700 11px var(--font-mono);
  color: var(--text-mute);
}

/* Marks a run that was one of the day's ranked attempts (vs free play). */
.attempts__daily {
  font: 700 9px var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-bright);
  background: var(--accent-soft);
  border-radius: 5px;
  padding: 2px 6px;
}

.attempts__sub {
  display: flex;
  align-items: center;
  gap: 6px;
  font: 500 11px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

/* Ranked (daily) field percentile, pXX, tinted by the percentile ITSELF
   (inlined as --pct by the row). The card's band is the standing, a different
   metric that could paint a p100 red; it is only the fallback. */
.attempts__pct {
  font: 700 11px var(--font-mono);
  color: var(--pct, var(--band));
}

.attempts__empty {
  font: 500 13px var(--font);
  color: var(--text-mute);
  padding: 8px 2px;
}

/* Per-row pin toggle (floats a run to the top of the panel) at the row's
   trailing edge. Negative margins let its 30px tap target hang into the row's
   padding without adding height. */
.attempts__pin {
  flex: none;
  margin: -6px -5px -6px 2px;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-mute);
  cursor: pointer;
  transition: color 0.12s ease, background 0.12s ease, opacity 0.12s ease;
}

.attempts__pin svg {
  width: 16px;
  height: 16px;
  display: block;
}

.attempts__pin:hover {
  background: color-mix(in srgb, var(--color) 12%, transparent);
  color: var(--color);
}

/* Pinned state is the icon's FILL (solid vs outline) plus full-strength neutral
   foreground, with no accent hue: the row already carries meaning in colour
   (standing band, Daily chip, supreme/record badges), and --accent-bright would
   read as kin to the Daily chip. */
.attempts__pin--active {
  color: var(--color);
}

/* Where a pointer can hover, keep the button quiet until the row is engaged (or
   the run is pinned / the button focused). Touch has no hover, so the default
   rule above keeps it visible. */
@media (hover: hover) {
  .attempts__pin:not(.attempts__pin--active) {
    opacity: 0.32;
  }

  .attempts__row:hover .attempts__pin,
  .attempts__pin:focus-visible {
    opacity: 1;
  }
}

/* A pinned row lifts its band edge a touch so the floated rows read as a set,
   without a second accent colour. */
.attempts__row--pinned {
  --band-card-edge: 46%;
}

/* Build-phase HUD strip above the board (see Hud.tsx). */
.hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Reserve height when empty so the board doesn't shift as the build phase
     loads. Matches the chip height below. */
  min-height: 36px;
  max-width: var(--maze-size);
  margin: 0 auto 12px;
}

.hud__chips {
  display: flex;
  gap: 8px;
}

/* Right-hand HUD group: the (free-play) reset button beside the build clock.
   Positioned so the free-play win badge can float beneath the timer (see
   .celebrate) without joining the layout. */
.hud__controls {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Shared box metrics for the HUD pills (chips, build clock, run clock, reset).
   */
.hud__chip,
.hud__build,
.hud__run,
.hud__reset {
  height: 36px;
  border-radius: 11px;
}

/* Free-play reset: a neutral chip-styled square left of the build clock, so the
   accent build button stays the primary action. */
.hud__reset {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  padding: 0;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-mute);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, transform 0.08s ease;
}

.hud__reset:hover {
  background: var(--surface-2);
  color: var(--color);
}

.hud__chip {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 12px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.hud__chip .mono {
  font-size: 15px;
  font-weight: 700;
  color: var(--color);
}

/* The build clock: always tappable to start now. The countdown and "Ready?"
   share one grid cell, so the chip keeps the wider countdown's width and
   cross-fades without reflow. */
.hud__build {
  position: relative;
  display: inline-grid;
  place-items: center;
  padding: 0 14px;
  border: 0;
  background: var(--accent-bright);
  color: #fff;
  cursor: pointer;
}

.hud__build-face {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  gap: 9px;
  transition: opacity 0.12s ease;
}

.hud__build .mono {
  font-size: 15px;
  font-weight: 700;
  color: #fff;
}

.hud__build-label {
  font: 600 12px var(--font);
  color: rgba(255, 255, 255, 0.85);
}

/* Run clock: the build slot, counting the live run up. */
.hud__run {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 14px;
  background: var(--accent-bright);
  color: #fff;
}

.hud__run .mono {
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  /* Fixed width so the count-up doesn't jitter the chip as digits change. */
  min-width: 4ch;
  text-align: right;
}

.hud__build-ready {
  font: 700 15px var(--font);
  color: #fff;
  opacity: 0;
}

/* Play button (review to back-to-live board): reuses the build pill. */
.hud__play {
  font: 700 15px var(--font);
  color: #fff;
}

.hud__play-icon {
  font-size: 10px;
}

/* Out of resources or idle: pulse between the remaining time and "Ready?". */
.hud__build.flashing .hud__build-time {
  animation: buildFadeTime 1.5s ease-in-out infinite alternate;
}

.hud__build.flashing .hud__build-ready {
  animation: buildFadeReady 1.5s ease-in-out infinite alternate;
}

@keyframes buildFadeTime {
  0%, 50% { opacity: 1; }
  80%, 100% { opacity: 0; }
}

@keyframes buildFadeReady {
  0%, 50% { opacity: 0; }
  80%, 100% { opacity: 1; }
}

/* On hover or press, commit to showing "Ready?". */
.hud__build:hover .hud__build-time,
.hud__build.pressing .hud__build-time {
  opacity: 0;
  animation: none;
}

.hud__build:hover .hud__build-ready,
.hud__build.pressing .hud__build-ready {
  opacity: 1;
  animation: none;
}

/* Staged free-play: the clock is frozen and inert until the first placement
   opens the run, so it reads as idle and ignores hover/press. */
.hud__build--staged {
  cursor: default;
  background: var(--accent);
}

.hud__build--staged:hover .hud__build-time {
  opacity: 1;
}

/* Free-play run clock: the live stopwatch doubles as a verdict. Its fill (set
   inline) tracks the run's live score % on the standing ramp; the % rides
   beside the time. The daily clock stays the plain accent pill above. */
.hud__run--score {
  position: relative;
  /* Centre the row in the fixed-height pill; the % is nudged to the time's
     baseline below so both sit on one line. */
  align-items: center;
  gap: 8px;
  padding: 0 15px;
  /* Inline background/color (the live % colour and its readable ink) override
     the shared .hud__run accent fill. */
}

.hud__run-time {
  display: inline-flex;
  align-items: baseline;
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  color: inherit;
}

.hud__run-s {
  font-size: 11px;
  margin-left: 1px;
}

.hud__run-pct {
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  color: inherit;
  opacity: 0.78;
}

/* Countdown water-ripple (see TimerRipple): at set remaining-time marks
   (10/3/2/1s) a wave floods across the board from the timer button, flowing
   AROUND the pieces, so a player heads-down on the board still senses the 60s
   window closing. An SVG overlay in the board's 0-20 space, anchored and
   clipped like the prestart mask. The flood is one 1x1 cell per free square
   (pieces get none, so the crest never draws over a block); each cell lights on
   a delay set by its distance THROUGH the maze, so the front bends around
   obstacles. */
.board-ripple {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--maze-size);
  aspect-ratio: 1;
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
  border-radius: var(--radius);
}

/* A single crest passing over each cell: rise, peak (to its distance-dimmed
   --peak), then a slightly longer fade for a trailing wake. `animationDelay`
   and `--peak` are set per cell inline (TimerRipple). */
.board-ripple__cell {
  fill: hsl(205, 100%, 90%);
  opacity: 0;
  animation: floodCell 0.42s ease-out forwards;
}

@keyframes floodCell {
  0% { opacity: 0; }
  24% { opacity: var(--peak); }
  100% { opacity: 0; }
}

/* Reduced motion: drop the travelling delay so the flood region just glows and
   fades in place. The animation still ends, so the overlay retires itself
   (onAnimationEnd on the last cell). */
@media (prefers-reduced-motion: reduce) {
  .board-ripple__cell {
    animation-delay: 0s !important;
    animation-duration: 0.7s;
  }
}

/* The timer button is the source: at each mark it dips into the "water", a
   quick bob whose fall coincides with the crest launching (see TimerRipple /
   timerPulse). */
.hud__build.dropping {
  animation: buildDrop 0.5s ease;
}

@keyframes buildDrop {
  0% { transform: translateY(0) scale(1); }
  28% { transform: translateY(-5px) scale(1.05); }
  52% { transform: translateY(2px) scale(0.97); }
  100% { transform: translateY(0) scale(1); }
}

@keyframes pbPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(79, 131, 230, 0); }
  50% { box-shadow: 0 0 16px 2px rgba(79, 131, 230, 0.55); }
}

@keyframes wrRing {
  0% { transform: scale(0.62); opacity: 0.75; }
  100% { transform: scale(1.55); opacity: 0; }
}

@keyframes supremeGlow {
  0%, 100% { box-shadow: 0 0 12px 1px rgba(230, 180, 40, 0.4); }
  50% { box-shadow: 0 0 26px 5px rgba(230, 180, 40, 0.66); }
}

@keyframes sparkle {
  0%, 100% { opacity: 0.2; transform: scale(0.7); }
  50% { opacity: 1; transform: scale(1.15); }
}

/* The free-play win overlay: the milestone's glow / ripple over the pill, a
   badge under it, and (personal best / supreme) confetti on every side. Fixed
   to the viewport and positioned over the pill's measured box (see
   CelebrationOverlay), NOT nested in the HUD: the HUD sits at the clipped
   top-right corner of `.game`, and drawing here lets effects spill above and
   right of the button. Fires when the run commits, unmounts when the board
   re-stages at finish; fades opacity only so the glow stays aligned to the
   pill. */
.celebrate {
  position: fixed;
  pointer-events: none;
  z-index: 12;
  animation: celebrateFade 0.4s ease forwards;
}

@keyframes celebrateFade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Glow / ripple sit over the pill's footprint, transparent-centred so the time
   reads through, and cast their shadow outward unclipped. */
.celebrate__glow,
.celebrate__ring {
  position: absolute;
  inset: 0;
  border-radius: 11px;
  pointer-events: none;
}

.celebrate__glow--pb {
  animation: pbPulse 1.6s ease-in-out infinite;
}

.celebrate__glow--supreme {
  animation: supremeGlow 1.8s ease-in-out infinite;
}

.celebrate__ring {
  border: 2px solid var(--peak);
  animation: wrRing 1.4s ease-out infinite;
}

.celebrate__badge {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid transparent;
  border-radius: 999px;
  white-space: nowrap;
  backdrop-filter: blur(5px);
}

.celebrate__badge svg {
  display: block;
  flex: none;
}

.celebrate__label {
  font: 700 11px var(--font);
  letter-spacing: 0.03em;
}

.celebrate__badge--pb {
  background: rgba(20, 20, 27, 0.92);
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.7);
  color: #eceef3;
}

.celebrate__badge--record {
  background: rgba(18, 26, 14, 0.92);
  border-color: rgba(150, 210, 70, 0.5);
  box-shadow: 0 8px 20px -6px rgba(80, 140, 20, 0.5);
  color: hsl(88, 60%, 66%);
}

.celebrate__badge--record svg {
  color: hsl(88, 60%, 60%);
}

.celebrate__badge--supreme {
  background: rgba(30, 22, 0, 0.92);
  border-color: rgba(230, 180, 40, 0.5);
  box-shadow: 0 8px 22px -6px rgba(180, 140, 20, 0.5);
  color: hsl(45, 88%, 66%);
}

.celebrate__badge--supreme svg {
  color: hsl(45, 90%, 62%);
}

.celebrate__spark {
  position: absolute;
  animation: sparkle 1.3s ease-in-out infinite;
}

/* Daily result modal (see Daily.tsx). */
.result {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px;
  background: rgba(6, 6, 10, 0.72);
}

.result__card {
  width: 100%;
  max-width: 300px;
  padding: 22px;
  border: 1px solid var(--border-strong);
  border-radius: 20px;
  background: var(--surface);
  box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.8);
  text-align: left;
  /* Center on the board (which sits header + HUD below the top), not the
     viewport, but never past the viewport midpoint. translateY(-50%) centers
     the card on that line; the -24px cancels the layer's top padding. */
  margin-top: calc(
    min(
      var(--header-size) + var(--hud-size) + var(--maze-size) / 2,
      var(--full-height) / 2
    ) - 24px
  );
  transform: translateY(-50%);
}

.result__header {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 16px;
}

.result__title {
  margin: 0;
  font: 700 17px var(--font);
  color: var(--color);
}

.result__rows {
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin-bottom: 12px;
}

.result__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 9px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--surface-2);
}

/* Best attempt: the shared band-card look, tinted to --accent with a heavier
   border and the same deeper "highlighted" fill as the viewed runs row. */
.result__row--best {
  --band-card-tint: var(--accent);
  --band-card-edge: 55%;
  --band-card-fill: 13%;
}

/* The glow is reserved for supreme (record) results, on the row and the
   matching "you beat 100%" banner. */
.result__row--glow {
  box-shadow: 0 0 16px -2px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Shared "card tinted to a percentile band" look: a band-derived border and a
   faint band fill over the surface. A caller sets --band-card-tint (its band /
   chip / accent colour); --band-card-edge strengthens the border. Used by the
   runs rows, today cards and the daily-result best row. Defined after
   .result__row so it wins over that neutral base; modifiers that must override
   it (e.g. the viewed runs row) raise their own specificity. */
.band-card {
  border: 1px solid
    color-mix(
      in srgb,
      var(--band-card-tint) var(--band-card-edge, 30%),
      var(--border)
    );
  background: color-mix(
    in srgb,
    var(--band-card-tint) var(--band-card-fill, 3%),
    var(--surface)
  );
}

.result__banner--glow {
  box-shadow: 0 0 16px -2px color-mix(in srgb, var(--beat) 55%, transparent);
}

.result__metric {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.result__label {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 600 13px var(--font);
  color: var(--text-dim);
}

.result__badge {
  padding: 2px 6px;
  border-radius: 5px;
  font: 700 9px var(--font);
  letter-spacing: 0.1em;
  color: var(--badge, var(--win));
  background: color-mix(in srgb, var(--badge, var(--win)) 16%, transparent);
}

.result__pct {
  font: 500 11px var(--font);
  color: var(--text-mute);
}

.result__time {
  font-size: 17px;
  font-weight: 700;
  color: var(--color);
}

.result__banner {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  padding: 11px 14px;
  border: 1px solid color-mix(in srgb, var(--beat) 34%, transparent);
  border-radius: 11px;
  background: color-mix(in srgb, var(--beat) 9%, transparent);
  font: 600 13px var(--font);
  color: var(--text-dim);
}

.result__banner b {
  color: var(--beat);
}

.result__stats {
  display: flex;
  gap: 8px;
  margin-bottom: 18px;
}

.result__stat {
  flex: 1;
  padding: 10px 12px;
  border-radius: 11px;
  background: var(--surface-2);
  text-align: center;
}

.result__stat-value {
  font-size: 15px;
  font-weight: 700;
  color: var(--color);
}

.result__stat-label {
  margin-top: 2px;
  font: 500 10px var(--font);
  color: var(--text-mute);
}

.result__actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.result__btn {
  width: 100%;
  margin: 0;
  padding: 13px;
  border-radius: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.result__btn--keep {
  background: var(--slow-runner);
}

/* Profile dialog (see Profile.tsx): a card over a dimmed board. Mobile: a
   bottom sheet that hugs its content (a sparse profile doesn't strand a
   full-screen void). Desktop: a centred dialog card, like the daily result.
   Dismissed by the backdrop or the close button. */
/* Shared dismissible overlay (see Modal.tsx): a bottom sheet on mobile, a
   centered card on desktop. Used by the profile dialog and the move sheet.
   Children own their padding; a per-use class (e.g. .profile-sheet) adds it. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  box-sizing: border-box;
  background: rgba(6, 6, 10, 0.68);
  /* Mobile default: anchored to the bottom as a sheet. */
  align-items: flex-end;
  justify-content: center;
}

.modal__sheet {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  /* Let the flex item shrink to the viewport, not its grid's min-content (which
     would push the sheet wider than a phone). */
  min-width: 0;
  max-width: 480px;
  /* Cap so a tall sheet always leaves a backdrop gap at the top (the board
     stays visible): at least 100px on short screens, 15vh on tall ones. The
     body scrolls internally past that. Desktop drops the cap. */
  max-height: min(85vh, calc(100vh - 100px));
  overflow-y: auto;
  text-align: left;
  background: var(--surface);
  /* Bottom sheet: rounded top only, flush to the bottom edge. */
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -20px 60px -20px rgba(0, 0, 0, 0.7);
  animation: profileSheetIn 0.22s ease;
}

/* Desktop: card centred, free-floating, rounded on all sides. */
@media (min-width: 1200px) {
  .modal {
    align-items: center;
    padding: 24px;
  }

  .modal__sheet {
    max-width: 420px;
    /* Leave a 24px backdrop gap top and bottom and scroll the body internally,
       so a tall dialog (the full profile) never runs off the viewport. */
    max-height: calc(100vh - 48px);
    overflow-y: auto;
    border: 1px solid var(--border-strong);
    border-radius: 20px;
    box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.8);
    animation: none;
  }
}

@keyframes profileSheetIn {
  from {
    transform: translateY(12%);
    opacity: 0.6;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Profile dialog content padding (the move sheet manages its own). One spacing
   scale for the dialog: 24 at the sheet's edges, 20 between sections, 8 between
   cards inside a section, and the shared .section-title's own 10 under a
   heading. */
.profile-sheet {
  padding: 24px 20px calc(24px + env(safe-area-inset-bottom, 0px));
}

@media (min-width: 1200px) {
  .profile-sheet {
    padding: 22px;
  }
}

/* Decorative dialog icons (edit pencil, copy/link) must not act as board-input
   targets: the game's touch handlers zoom the board on any touched <svg>. Taps
   fall through to the buttons beneath. */
.modal svg {
  pointer-events: none;
}

/* Shared header/close icon button for both modals (profile x, move < / x). */
.modal__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border: 0;
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.modal__icon:hover {
  background: var(--surface-3);
  color: var(--color);
}

/* The profile's close is the same button, pinned to the sheet corner. */
.profile-modal__close {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 1;
}

/* Header: avatar, name + join date, rating pinned right. Held to the avatar's
   height so swapping in the shorter rename bar doesn't shrink the dialog. */
.profile-head {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 52px;
  margin: 8px 0 20px;
  padding-right: 38px;
}

.profile-head__avatar {
  flex: none;
  width: 52px;
  height: 52px;
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 24px var(--font);
  color: #fff;
  background: var(--accent);
  box-shadow: 0 8px 20px -8px var(--accent);
}

.profile-head__id {
  flex: 1;
  min-width: 0;
}

.profile-head__name-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.profile-head__name {
  font: 700 20px var(--font);
  color: var(--color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.profile-head__edit {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text-mute);
  cursor: pointer;
}

.profile-head__edit:hover {
  color: var(--color);
}

.profile-head__joined {
  font-size: 12px;
  color: var(--text-mute);
  margin-top: 3px;
}

.profile-head__rating {
  flex: none;
  text-align: right;
}

.profile-head__rating-value {
  font-size: 24px;
  font-weight: 700;
  color: var(--gold);
}

.profile-head__rating-label {
  font: 600 10px var(--font);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-top: 1px;
}

/* Inline rename: a full-width bar that replaces the header row while editing,
   so the input isn't crushed between avatar and rating. */
.profile-rename {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

.profile-rename__input {
  flex: 1;
  min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--color);
  font: 600 16px var(--font);
}

.profile-rename__input:focus {
  outline: none;
  border-color: var(--accent);
}

.profile-rename__save,
.profile-rename__cancel {
  flex: none;
  padding: 6px 10px;
  border: 0;
  border-radius: 9px;
  font: 600 12px var(--font);
  cursor: pointer;
}

.profile-rename__save {
  background: var(--accent);
  color: #fff;
}

.profile-rename__save:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.profile-rename__cancel {
  background: var(--surface-2);
  color: var(--text-mute);
}

/* Three equal stat tiles. */
.profile-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 8px;
}

.profile-stat {
  padding: 14px 10px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
  text-align: center;
}

.profile-stat__value {
  font-size: 22px;
  font-weight: 700;
  color: var(--color);
}

.profile-stat__label {
  font: 500 11px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

/* What the figure is out of ("29 of 75 days"), so a tile reads on its own. */
.profile-stat__sub {
  font: 500 10px var(--font);
  color: var(--text-faint);
  margin-top: 1px;
}

/* The separator between a tile's two figures (won alone · shared). */
.profile-stat__dot {
  color: var(--text-faint);
  font-weight: 400;
  margin: 0 2px;
}

/* Desktop: the dialog widens. The left column is the half you read (ranked,
   free play, head-to-head), the right the half you operate (notifications,
   preferences, community, account): split by what a section is FOR, not by what
   fits. Below the breakpoint the columns stack in source order (the mobile
   layout). Uses the 1200px breakpoint the centred card already uses. */
.profile-cols {
  display: grid;
  /* Stacked, the gap is the gap between two sections, so it matches the 20 they
     keep between themselves; side by side it is the gutter. */
  gap: 20px;
  align-items: start;
}

.profile-col > :last-child {
  margin-bottom: 0;
}

@media (min-width: 1200px) {
  .modal__sheet.profile-sheet {
    max-width: 940px;
    padding: 24px;
  }

  .profile-cols {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: 24px;
  }

  /* A hairline instead of a wider gutter: the halves are one dialog, and the
     rule keeps the right column's cards from reading as a second sheet. */
  .profile-col--side {
    border-left: 1px solid var(--border);
    padding-left: 24px;
  }
}

/* A stat block: its section title (and, for head-to-head, the board switch)
   over whatever the block shows. */
.profile-block {
  margin-bottom: 20px;
}

.profile-block__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

/* A pill on the sheet reads as an outline unless its track is a step above the
   surface behind it. The runs panel's pill fills with --surface (it sits on the
   page); here it fills with --surface-2. */
.profile-sheet .pref__seg {
  background: var(--surface-2);
  margin-bottom: 10px;
}

/* Solve rows: an icon tile, what the stat means, and the figure. */
.profile-rows {
  padding: 2px 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
}

.profile-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
}

.profile-row + .profile-row {
  border-top: 1px solid var(--border-strong);
}

.profile-row__tile {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: color-mix(in srgb, currentColor 16%, transparent);
}

.profile-row__text {
  flex: 1;
  min-width: 0;
}

.profile-row__title {
  font: 600 14px var(--font);
  color: var(--color);
}

.profile-row__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 1px;
}

.profile-row__fig {
  flex: none;
  text-align: right;
}

.profile-row__value {
  font-size: 18px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--color);
}

.profile-row__small {
  font: 500 11px var(--font);
  color: var(--text-faint);
}

/* Free play in one tally, a progression through the library: boards held alone,
   shared, played, and that exist. */
.profile-tally {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
  overflow: hidden;
}

.profile-tally__cell {
  flex: 1;
  min-width: 0;
  padding: 14px 10px;
  text-align: center;
}

.profile-tally__cell + .profile-tally__cell {
  border-left: 1px solid var(--border);
}

.profile-tally__value {
  font-size: 22px;
  font-weight: 700;
  color: var(--color);
}

.profile-tally__label {
  font: 500 11px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

/* Head-to-head: the standings row's shape, one rival per row. */
.profile-rivals {
  display: grid;
  gap: 2px;
  padding: 4px 0;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
}

.profile-rivals__empty {
  padding: 10px 14px;
  font: 500 12px var(--font);
  color: var(--text-mute);
}

.profile-rival {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
}

.profile-rival__avatar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  font: 700 15px var(--font);
  color: #fff;
}

.profile-rival__who {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.profile-rival__name {
  font: 600 15px var(--font);
  color: var(--color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.profile-rival__sub {
  font: 500 11px var(--font);
  color: var(--text-mute);
}

.profile-rival__nums {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

/* Wins lead in the text colour, losses recede: the pair reads "yours against
   theirs" without a second hue fighting the record states. */
.profile-rival__record {
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: var(--color);
}

/* The separator is punctuation, not a figure: it must not inherit the record's
   colour and outshine the number beside it. */
.profile-rival__dash {
  color: var(--text-faint);
  font-weight: 400;
  margin: 0 1px;
}

.profile-rival__behind {
  color: var(--text-mute);
}

.profile-rival__small {
  font: 500 11px var(--font);
  color: var(--text-faint);
}

/* The way into the rest of the list, under the rows it extends. */
.profile-rivals__more {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  margin-top: 2px;
  padding: 10px 14px;
  border: 0;
  border-top: 1px solid var(--border-strong);
  background: transparent;
  font: 600 13px var(--font);
  color: var(--accent-bright);
  text-align: left;
  cursor: pointer;
}

.profile-rivals__chev {
  font-size: 18px;
  line-height: 1;
}

/* Head-to-head sheet: the whole list, searchable and sortable. Rows are the
   profile's (.profile-rival*); only the chrome above them is new. */
.rivals-sheet {
  padding: 0 0 calc(16px + env(safe-area-inset-bottom, 0px));
}

.rivals__search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 20px 10px;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-mute);
}

.rivals__search-input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--color);
  font: 500 14px var(--font);
}

.rivals__search-input:focus {
  outline: none;
}

/* Both switches on one line: which board the record is from, and the order. */
.rivals__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 0 20px 10px;
}

.rivals-sheet .profile-rivals {
  margin: 0 20px;
}

/* Copy-login-link action row. */
.profile-action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  padding: 14px;
  border: 1px solid var(--border-strong);
  border-radius: 13px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: 600 14px var(--font);
  cursor: pointer;
  transition: background 0.12s ease;
}

.profile-action:hover {
  background: var(--surface-3);
}

/* Move-to-device row: a left-aligned action with a title + sub and a chevron.
   */
.profile-action--row {
  justify-content: flex-start;
  gap: 12px;
  text-align: left;
}

.profile-action__text {
  flex: 1;
  min-width: 0;
}

.profile-action__title {
  font: 600 14px var(--font);
  color: var(--color);
}

.profile-action__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

.profile-action__chev {
  flex: none;
  font-size: 20px;
  color: var(--text-mute);
}

/* Community: Discord join card, an external link styled like a pref row. */
.community-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
  text-decoration: none;
  /* Opt out of the global link styling (accent colour, bold, hover brightness)
     so this reads as a surface row like the Account button; children set their
     own colour/weight. */
  color: var(--color);
  font-weight: normal;
  transition: background 0.12s ease;
}

.community-card:hover {
  background: var(--surface-3);
  /* Override the global `a:hover` brightness filter, which would dim the whole
     card on top of the surface change (the Account <button> never gets it). */
  filter: none;
}

.community-card__tile {
  flex: none;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  color: #fff;
  /* Discord blurple, fixed across themes to keep the mark on-brand. */
  background: #5865f2;
}

.community-card__text {
  flex: 1;
  min-width: 0;
}

.community-card__title {
  font: 600 15px var(--font);
  color: var(--color);
}

.community-card__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 1px;
}

.community-card__online {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  /* Fixed line box so the row is the same height before and after the count
     lands; the card never grows on open. */
  min-height: 14px;
  font-size: 12px;
  line-height: 14px;
  color: var(--text-dim);
}

.community-card__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--win);
}

/* Grey the dot when there's no live presence to show (before the first count
   lands, or a genuine zero), so the green "live" dot appears only when someone
   is on. */
.community-card__dot--idle {
  background: var(--text-faint);
}

.community-card__ext {
  flex: none;
  display: flex;
  color: var(--text-mute);
}

/* Preferences: appearance segmented control + zoom slider. */
.pref {
  margin-bottom: 20px;
}

.pref__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
}

.pref__row + .pref__row {
  margin-top: 8px;
}

.pref__row--stack {
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}

.pref__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.pref__label {
  font: 600 14px var(--font);
  color: var(--color);
}

.pref__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

.pref__value {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-dim);
  /* Keep a two-token value (e.g. "175 ms") on one line instead of wrapping the
     unit under the number in the narrow value column. */
  white-space: nowrap;
}

/* Segmented System / Light / Dark, same shape as the runs sort toggle. */
.pref__seg {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
}

.pref__seg-btn {
  border: 0;
  background: transparent;
  color: var(--text-mute);
  font: 600 12px var(--font);
  padding: 4px 10px;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.pref__seg-btn:hover {
  color: var(--color);
}

.pref__seg-btn--active {
  background: var(--surface-3);
  color: var(--color);
}

.pref__slider {
  width: 100%;
  accent-color: var(--accent-bright);
  cursor: pointer;
}

/* Language picker: a custom dropdown that sizes its trigger to the current
   value (so the chevron hugs the text) and positions its own menu. */
.lang-select {
  position: relative;
}

.lang-select__btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: 600 13px var(--font);
  color: var(--color);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 6px 10px;
  cursor: pointer;
  transition: border-color 0.12s ease;
}

.lang-select__btn:hover {
  border-color: var(--text-mute);
}

.lang-select__chev {
  color: var(--text-mute);
  flex: none;
}

.lang-select__menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 5;
  min-width: 100%;
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.5);
}

.lang-select__opt {
  padding: 7px 10px;
  border-radius: 6px;
  font: 500 13px var(--font);
  color: var(--color);
  white-space: nowrap;
  cursor: pointer;
}

.lang-select__opt:hover {
  background: var(--surface-3);
}

.lang-select__opt--on {
  background: var(--surface-3);
  font-weight: 700;
}

/* ===== Move-to-device sheet (shell is the shared .modal, see Modal.tsx) ==== */
.move__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 12px 4px;
}

.move__title {
  flex: 1;
  font: 700 18px var(--font);
  color: var(--color);
  text-align: left;
}

.move__body {
  padding: 8px 20px calc(24px + env(safe-area-inset-bottom, 0px));
}

.move__lead {
  font: 500 13px var(--font);
  color: var(--text-mute);
  line-height: 1.5;
  margin: 0 0 16px;
}

.move__lead .mono {
  color: var(--text-dim);
}

/* QR card: full width, always a light field (scannable), logo tile centered. */
.move__qr {
  position: relative;
  width: 100%;
  margin: 0 0 16px;
  padding: 16px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 2px 10px -2px rgba(0, 0, 0, 0.4);
}

.move__qr-logo {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  padding: 4px;
  border-radius: 7px;
  background: #fff;
}

.move__qr-logo img {
  display: block;
}

.move__link {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

.move__url {
  flex: 1;
  min-width: 0;
  padding: 0 12px;
  height: 40px;
  /* Block (not flex) so text-overflow works: the start of the URL stays visible
     and the tail ellipsises. line-height centres it in the pill. */
  line-height: 38px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 13px;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.move__copy {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  padding: 0 15px;
  height: 40px;
  border: 0;
  border-radius: 11px;
  background: var(--accent-bright);
  color: #fff;
  font: 600 14px var(--font);
  cursor: pointer;
}

.move__safety {
  display: flex;
  gap: 9px;
  padding: 11px 13px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--warn) 28%, var(--border));
}

.move__safety svg {
  flex: none;
  margin-top: 1px;
}

.move__safety div {
  font: 500 12px var(--font);
  color: var(--text-dim);
  line-height: 1.5;
}

.move__name {
  font-weight: 700;
  color: var(--color);
}

/* ===== Account: data export / delete rows + delete-confirm sheet ========== */

/* The destructive "Delete my data" row: same shape as the other account rows
   (profile-action--row), tinted with the loss colour so it reads as the one
   that erases. */
.profile-action--danger {
  color: var(--lose);
  border-color: color-mix(in srgb, var(--lose) 42%, var(--border-strong));
  background: color-mix(in srgb, var(--lose) 8%, var(--surface-2));
}

/* Title, sub-line, chevron and trash icon (currentColor) all take the full loss
   colour, so the whole row reads red. */
.profile-action--danger .profile-action__title,
.profile-action--danger .profile-action__sub,
.profile-action--danger .profile-action__chev {
  color: var(--lose);
}

.profile-action--danger:hover {
  background: color-mix(in srgb, var(--lose) 15%, var(--surface-2));
  border-color: color-mix(in srgb, var(--lose) 55%, var(--border-strong));
}

/* Stacked account rows (Move / Export / Delete) sit flush otherwise: .pref has
   no gap; spacing between siblings is per-component (cf. .pref__row). */
.profile-action + .profile-action {
  margin-top: 8px;
}

/* Quiet text link under the account rows, out to the static privacy page. */
.profile-privacy-link {
  display: block;
  margin-top: 16px;
  text-align: center;
  font: 500 12px var(--font);
  color: var(--text-mute);
}

/* Delete-confirm sheet (shell is the shared .modal; head/body reuse .move__*).
   */
.delete-confirm {
  margin: 4px 0 14px;
}

.delete-confirm__label {
  display: block;
  margin-bottom: 8px;
  font: 500 13px var(--font);
  color: var(--text-dim);
}

.delete-confirm__label .mono {
  color: var(--lose);
  letter-spacing: 0.06em;
}

.delete-confirm__input {
  width: 100%;
  height: 44px;
  padding: 0 14px;
  border: 1px solid color-mix(in srgb, var(--lose) 30%, var(--border));
  border-radius: 11px;
  background: var(--surface-2);
  color: var(--color);
  font: 600 15px var(--font);
  letter-spacing: 0.04em;
}

.delete-confirm__input:focus {
  outline: none;
  border-color: var(--lose);
}

.delete-confirm__btn {
  width: 100%;
  height: 46px;
  margin-bottom: 16px;
  border: 0;
  border-radius: 12px;
  background: var(--lose);
  color: #fff;
  font: 700 15px var(--font);
  cursor: pointer;
  transition: opacity 0.12s ease;
}

.delete-confirm__btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.delete-error {
  margin-bottom: 14px;
  padding: 10px 12px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--lose) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--lose) 30%, transparent);
  font: 500 12px var(--font);
  color: var(--lose);
  line-height: 1.5;
}

/* The reassurance note reuses .move__safety's shape in a neutral tint. */
.delete-safety {
  background: var(--surface-2);
  border-color: var(--border);
}

/* ── Move / merge gate ─────────────────────────────────────────────────────
   The pre-app takeover for a sign-in link opened on a device that already has
   a profile (see MoveGate.tsx): clean-adopt confirm, the fork, the primary
   picker, and the switch/save-link screen. A centered card on a plain backdrop;
   it stands in for the whole app, ahead of onboarding. */
.mg {
  min-height: var(--full-height, 100vh);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Top-anchored, not vertically centered, so the logo sits near the top and
     content flows down (a lone confirm card shouldn't float mid-screen).
     Clamped to stay high on a phone without hugging the top of a tall desktop
     viewport. */
  justify-content: flex-start;
  padding: clamp(40px, 11vh, 120px) 18px 24px;
  background: var(--background);
}
.mg-card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
}
.mg-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 700 19px var(--font);
  color: var(--color);
}
.mg-inner {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 16px;
}
.mg-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font);
  font-weight: 700;
  color: #fff;
  flex: none;
}
.mg-name {
  font: 700 17px var(--font);
  color: var(--color);
}
.mg-name--lg {
  font-size: 22px;
}
.mg-sub {
  font-size: 11.5px;
  color: var(--text-mute);
  margin-top: 2px;
}
.mg-sub--block {
  margin: -4px 2px 4px;
  line-height: 1.5;
  text-align: center;
}
.mg-heading {
  font: 700 18px var(--font);
  color: var(--color);
  text-align: center;
}

/* Clean-adopt (1d): one card holding the label, profile and both buttons. */
.mg-confirm {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 26px 22px;
  box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.6);
}
.mg-confirm-label {
  text-align: center;
  font: 600 12px var(--font);
  letter-spacing: 0.05em;
  color: var(--text-mute);
  margin-bottom: 20px;
}
.mg-actions--inset {
  margin-top: 20px;
}
.mg-profile--solo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* The two-profile pair (fork) */
.mg-pair {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 18px;
  box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.6);
}
.mg-lbl {
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--text-mute);
  margin: 0 2px 8px;
}
.mg-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.mg-rowtext {
  flex: 1;
  min-width: 0;
  text-align: left;
}
.mg-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 2px;
}
.mg-divider::before,
.mg-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
.mg-divider span {
  font: 600 10px var(--font-mono);
  letter-spacing: 0.08em;
  color: var(--text-faint);
}

.mg-actions {
  display: flex;
  flex-direction: column;
  gap: 9px;
  width: 100%;
}

/* Buttons */
.mg-btn {
  width: 100%;
  border: 0;
  border-radius: 15px;
  padding: 15px 16px;
  font: 700 15px var(--font);
  cursor: pointer;
}
.mg-btn--primary {
  background: var(--accent);
  color: #fff;
  box-shadow: 0 8px 22px -12px var(--accent);
}
.mg-btn--ghost {
  background: transparent;
  color: var(--text-mute);
  font-weight: 600;
  padding: 12px;
}
.mg-opt {
  display: flex;
  align-items: center;
  gap: 13px;
  text-align: left;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 15px;
  padding: 14px 16px;
  cursor: pointer;
  color: var(--text-mute);
}
.mg-opt-icon {
  flex: none;
  display: flex;
  color: var(--accent);
}
.mg-opt-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.mg-opt-chev {
  flex: none;
  display: flex;
  color: var(--text-faint);
}
.mg-opt--merge {
  border-color: transparent;
  background: var(--accent);
  box-shadow: 0 8px 22px -14px var(--accent);
}
.mg-opt--merge .mg-opt-icon,
.mg-opt--merge .mg-opt-chev {
  color: rgba(255, 255, 255, 0.9);
}
.mg-opt--merge .mg-opt-title {
  color: #fff;
}
.mg-opt--merge .mg-opt-sub {
  color: rgba(255, 255, 255, 0.82);
}
.mg-opt-title {
  font: 700 15px var(--font);
  color: var(--color);
}
.mg-opt-sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
}

/* Primary picker */
.mg-picklist {
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.mg-pickrow {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  border: 1.5px solid var(--border);
  background: var(--surface);
  border-radius: 15px;
  padding: 12px 14px;
  cursor: pointer;
}
.mg-pickrow--on {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.mg-radio {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
}
.mg-radio--on {
  border-color: var(--accent);
  background: var(--accent);
}
.mg-pickname {
  display: flex;
  align-items: center;
  gap: 7px;
}
.mg-badge {
  font: 600 9.5px var(--font-mono);
  letter-spacing: 0.05em;
  color: var(--accent-bright);
  background: var(--accent-soft);
  border-radius: 5px;
  padding: 2px 6px;
}

/* Sub-page frame (pick, switch): header bar + left-justified body. Shares the
   brand frame's top padding (via .mg) so stepping fork to pick keeps the top
   edge fixed and the dialog only grows downward. */
.mg-header {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  margin-bottom: 6px;
}
.mg-headback {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.mg-headtitle {
  flex: 1;
  text-align: left;
  font: 700 18px var(--font);
  color: var(--color);
}
.mg-body {
  width: 100%;
  text-align: left;
  display: flex;
  flex-direction: column;
}
.mg-section-lbl {
  font: 600 10px var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-mute);
  margin: 20px 2px 9px;
}
.mg-body > .mg-section-lbl:first-child {
  margin-top: 2px;
}
.mg-desc {
  font: 500 12px var(--font);
  color: var(--text-mute);
  line-height: 1.45;
  margin: 0 2px 12px;
}
/* Helper caption under a control (e.g. the link on switch). */
.mg-caption {
  font: 500 11.5px var(--font);
  color: var(--text-mute);
  line-height: 1.45;
  text-align: left;
  margin: 8px 2px 0;
}
.mg-body-btn {
  margin-top: 20px;
}
.mg-btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

/* "What we'll combine" info card. */
.mg-combine {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 15px;
  overflow: hidden;
}
.mg-combine-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 15px;
}
.mg-combine-row + .mg-combine-row {
  border-top: 1px solid var(--border);
}
.mg-combine-icon {
  flex: none;
  display: flex;
  /* Warn amber: the earliest-attempt rule is a soft caveat worth noticing. */
  color: var(--warn);
  margin-top: 1px;
}
.mg-combine-icon--ok {
  color: var(--win);
}
.mg-combine-title {
  font: 600 14px var(--font);
  color: var(--color);
}
.mg-combine-sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  line-height: 1.45;
  margin-top: 2px;
}
.mg-combine-sub b {
  color: var(--text-dim);
  font-weight: 600;
}
.mg-note {
  display: flex;
  gap: 9px;
  margin: 10px 3px 0;
  color: var(--text-faint);
}
.mg-note svg {
  flex: none;
  margin-top: 1px;
}
.mg-note span {
  font: 500 11px var(--font);
  line-height: 1.5;
}
.mg-note-center {
  text-align: center;
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 10px;
}

/* Switch (2c): the K to V hop + a warning. */
.mg-switch-avatars {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 2px 0 0;
  margin-bottom: 18px;
}
.mg-switch-one {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.mg-switch-name {
  font: 600 13px var(--font);
  color: var(--text-dim);
}
.mg-switch-arrow {
  font-size: 22px;
  color: var(--text-faint);
}
.mg-warn {
  display: flex;
  gap: 11px;
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  border-radius: 14px;
  padding: 13px 15px;
  text-align: left;
  font: 500 12.5px var(--font);
  /* Amber-tinted body so the white profile names pop. */
  color: color-mix(in srgb, var(--warn) 55%, var(--text-dim));
  line-height: 1.5;
}
.mg-warn b {
  color: var(--color);
  font-weight: 700;
}
.mg-warn-icon {
  flex: none;
  display: flex;
  color: var(--warn);
  margin-top: 1px;
}
.mg-link {
  display: flex;
  align-items: stretch;
  gap: 9px;
}
.mg-url {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  background: var(--surface-2);
  border-radius: 12px;
  padding: 0 14px;
  font-size: 13px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mg-copy {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  border: 0;
  background: var(--surface-2);
  color: var(--text-dim);
  border-radius: 12px;
  padding: 11px 14px;
  font: 600 13px var(--font);
  cursor: pointer;
}

.mg-back {
  position: absolute;
  top: -4px;
  left: -4px;
  border: 0;
  background: transparent;
  color: var(--text-mute);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}

.mg-spinner {
  width: 30px;
  height: 30px;
  border: 3px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: mg-spin 0.8s linear infinite;
  margin: 24px auto;
}
@keyframes mg-spin {
  to {
    transform: rotate(360deg);
  }
}

/* The non-blocking "Connecting…" (Disconnected.tsx's compact form): free play
   works offline, so it says so without covering the board. */
.disconnected-pill {
  position: fixed;
  left: 50%;
  top: calc(10px + env(safe-area-inset-top));
  transform: translateX(-50%);
  z-index: 60;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 16px;
  font: 600 13px var(--font);
  color: var(--text-mute);
  box-shadow: 0 10px 28px -14px rgba(0, 0, 0, 0.6);
}

/* Transient status toast (bottom-centered over the board), currently the silent
   profile-adopt confirmation from the move/merge gate. See Toast.tsx. */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(20px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 11px;
  width: min(360px, calc(100vw - 32px));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 12px 12px 12px 14px;
  box-shadow: 0 18px 44px -16px rgba(0, 0, 0, 0.6);
  text-align: left;
  animation: toast-in 0.28s cubic-bezier(0.2, 0.9, 0.3, 1);
}
.toast__icon {
  flex: none;
  display: flex;
}
.toast__body {
  flex: 1;
  min-width: 0;
}
.toast__title {
  font: 700 13.5px var(--font);
  color: var(--color);
}
.toast__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 1px;
}
.toast__close {
  flex: none;
  border: 0;
  background: transparent;
  color: var(--text-faint);
  font-size: 20px;
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translate(-50%, 12px);
  }
}

/* ==== Standings (dock + expanded sheet) ==================================
   The collapsed sliver shows your rank and the current leader; tapping it opens
   the full board. One accent per row, as in the runs panel: gold for a #1 that
   beat everyone outright, lime (--peak) for rows tying a shared record, green
   (--win) for your own row; everything else stays neutral. */

.standings-dock {
  display: flex;
  align-items: center;
  gap: 12px;
  box-sizing: border-box;
  width: 100%;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 13px 16px;
  cursor: pointer;
  text-align: left;
  color: var(--color);
}

/* Board-zoom guard, mirroring `.modal svg`: the game's touch handlers zoom on
   any touched <svg>, so dock/sheet icons must not be touch targets. */
.standings-dock svg,
.standings-modal svg {
  pointer-events: none;
}

.standings-crown {
  flex: none;
}

.standings-dock__chevron {
  display: flex;
  flex: none;
  color: var(--text-mute);
}

.standings-dock__text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.standings-dock__title {
  font: 700 15px var(--font);
  line-height: 1.1;
  color: var(--color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Fallback only: the component inlines the standing colour (gold / chartreuse /
   percentile ramp) over this. */
.standings-dock__rank {
  color: var(--win);
}

.standings-dock__sub {
  display: flex;
  align-items: center;
  gap: 5px;
  font: 500 12px var(--font);
  color: var(--text-mute);
  white-space: nowrap;
  overflow: hidden;
}

.standings-dock__lead--gold {
  color: var(--gold);
}

/* A leader tied at the record wears the shared-record lime, like the sheet's
   match rows (gold is for an outright #1). */
.standings-dock__lead--lime {
  color: var(--peak);
}

/* Desktop: a peek card at the top of the right rail (see .game__rail, which
   stacks it above the runs). */
@media (min-width: 1200px) {
  /* The sheet is a side drawer here (slides in from the right; see
     .standings-sheet), so the "expands upward" caret is rotated to point left.
     */
  .standings-dock__chevron {
    transform: rotate(-90deg);
  }
}

/* Mobile: pinned to the viewport bottom; the panel scrolls out from under it
   (see .game__panel's bottom padding). */
@media (max-width: 1199.98px) {
  .standings-dock {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Above the scrolling panel, below every overlay (result 20, calendar
       picker 50, modals 60). */
    z-index: 15;
    border: 0;
    border-top: 1px solid var(--border-strong);
    border-radius: 22px 22px 0 0;
    box-shadow: 0 -12px 30px rgba(0, 0, 0, 0.35);
    padding: 14px 18px calc(14px + env(safe-area-inset-bottom, 0px));
  }
}

/* Scrim + sheet: a bottom sheet on mobile, a right-hand side sheet on desktop.
   */
.standings-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: rgba(6, 6, 10, 0.55);
}

.standings-sheet {
  position: relative;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 0;
  max-height: 86%;
  background: var(--surface);
  border-top: 1px solid var(--border-strong);
  border-radius: 26px 26px 0 0;
  box-shadow: 0 -24px 60px rgba(0, 0, 0, 0.5);
  text-align: left;
  animation: profileSheetIn 0.22s ease;
}

.standings-sheet__handle {
  flex: none;
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 11px 0 5px;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.standings-sheet__handle::before {
  content: "";
  width: 38px;
  height: 4px;
  border-radius: 99px;
  background: var(--border-strong);
}

.standings-sheet__head {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 4px 20px 10px;
}

.standings-sheet__title {
  font: 700 22px var(--font);
  color: var(--color);
}

.standings-sheet__date {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 2px;
}

.standings-sheet__side {
  display: flex;
  align-items: center;
  gap: 14px;
}

.standings-sheet__timer {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.standings-sheet__timer .mono {
  font-size: 14px;
  font-weight: 700;
  color: var(--color);
}

.standings-sheet__timer span:last-child {
  font: 500 10px var(--font);
  color: var(--text-mute);
}

/* Mobile closes via the handle / scrim; the x appears on the desktop side
   sheet, which has neither. */
.standings-sheet__close {
  display: none;
}

/* Sort control: a "SORT BY" label, then DAILY / PB as bare text buttons; the
   active one is accent-coloured, underlined, with a caret. */
.standings-sortby {
  flex: none;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 20px 10px;
}

.standings-sortby__label {
  flex: 1;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--text-faint);
}

.standings-sortby__tab {
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  font: 700 11px var(--font-mono);
  letter-spacing: 0.04em;
  color: var(--text-mute);
  text-underline-offset: 4px;
}

.standings-sortby__tab--active {
  color: var(--accent-bright);
  text-decoration: underline;
}

.standings-sheet__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 20px calc(20px + env(safe-area-inset-bottom, 0px));
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

.standings-sheet__empty {
  padding: 18px 2px;
  font: 500 13px var(--font);
  color: var(--text-mute);
}

@media (min-width: 1200px) {
  .standings-modal {
    flex-direction: row;
    justify-content: flex-end;
  }

  .standings-sheet {
    width: 412px;
    height: 100%;
    max-height: none;
    border-radius: 0;
    border-top: 0;
    border-left: 1px solid var(--border-strong);
    box-shadow: -18px 0 50px rgba(0, 0, 0, 0.45);
    animation: standingsSideIn 0.18s ease;
  }

  .standings-sheet__handle {
    display: none;
  }

  .standings-sheet__head {
    padding-top: 20px;
  }

  .standings-sheet__close {
    display: flex;
  }
}

@keyframes standingsSideIn {
  from {
    transform: translateX(6%);
    opacity: 0.6;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* One board row: rank | avatar | name/sub | time. Accents ride the band-card
   tint; neutral rows zero it out and stay flat. */
.standings-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  border-radius: var(--radius);
  padding: 10px 14px;
  --band-card-tint: transparent;
  --band-card-edge: 0%;
  --band-card-fill: 0%;
}

.standings-row--gold {
  --band-card-tint: var(--gold);
  --band-card-edge: 32%;
  --band-card-fill: 9%;
}

.standings-row--lime {
  --band-card-tint: var(--peak);
  --band-card-edge: 32%;
  --band-card-fill: 9%;
}

/* --you is your row's accent, inlined by the row (Sheet.tsx): your identity
   colour, or the record hue (gold/lime) when you hold one, so the "you"
   treatments survive at #1. --win is only the fallback. */
.standings-row--you {
  --band-card-tint: var(--you, var(--win));
  --band-card-edge: 42%;
  --band-card-fill: 11%;
}

.standings-row__rank {
  flex: none;
  min-width: 34px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text-mute);
}

.standings-row--gold .standings-row__rank {
  color: var(--gold);
}

.standings-row--lime .standings-row__rank {
  color: var(--peak);
}

.standings-row--you .standings-row__rank {
  color: var(--you, var(--win));
}

.standings-row__avatar {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 15px var(--font);
  color: #fff;
}

.standings-row__who {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.standings-row__name {
  font: 600 15px var(--font);
  color: var(--color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.standings-row__sub {
  font: 500 11px var(--font);
  color: var(--text-mute);
}

.standings-row--you .standings-row__sub {
  color: var(--you, var(--win));
}

.standings-row--you .standings-row__time {
  color: var(--you, var(--win));
}

/* The ranked value (big) over the other board's value (small), right-aligned.
   */
.standings-row__nums {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.standings-row__time {
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: var(--color);
}

.standings-row__small {
  font-size: 11px;
  font-weight: 500;
  color: var(--text-faint);
}

.standings-row--gold .standings-row__time {
  color: var(--gold);
}

.standings-row--lime .standings-row__time {
  color: var(--peak);
}

.standings-row__unit {
  font-size: 11px;
  color: var(--text-faint);
  margin-left: 1px;
}

/* "N between" separator where the display window skips players. */
.standings-gap {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 3px 6px;
}

.standings-gap > span:first-child,
.standings-gap > span:last-child {
  flex: 1;
  height: 1px;
  background: var(--border);
}

.standings-gap > .mono {
  font-size: 10px;
  font-weight: 600;
  color: var(--text-faint);
}

/* Hidden but space-reserving until the daily result is closed (see Dock.tsx):
   the dock's slot exists from boot, so revealing it can't shift the runs below
   (desktop rail). visibility also drops it from the tab order and pointer
   targets. */
.standings-dock--hidden {
  visibility: hidden;
}

/* ==== Mobile sheet dismissal (shared) =====================================
   On mobile every sheet closes with a grab notch at the top (tap or drag down;
   useDragToClose) instead of the per-dialog x, which stays for desktop.
   Absolute so existing sheet padding is untouched; touch-action lets the drag
   own the gesture instead of the scroller. */
.modal__handle {
  display: none;
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  border: 0;
  background: transparent;
  padding: 10px 44px 8px;
  cursor: pointer;
  touch-action: none;
}

.modal__handle::before {
  content: "";
  display: block;
  width: 38px;
  height: 4px;
  border-radius: 99px;
  background: var(--border-strong);
}

.standings-sheet__handle,
.standings-sheet__head {
  touch-action: none;
}

@media (max-width: 1199.98px) {
  .modal__handle {
    display: block;
  }

  /* visibility (not display) so headers that balance a back button against the
     x (move sheet) keep their centring. */
  .modal__close-x {
    visibility: hidden;
    pointer-events: none;
  }
}

/* ============================================================================
   Notifications: the header bell + badge, the slide-out panel (a bottom sheet
   on mobile, a right-hand drawer on desktop; adapts .standings-modal), the
   cards, and the profile preference rows with their switches.
   ========================================================================== */

.notif-bell {
  position: relative;
}

.notif-bell__badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 16px;
  height: 16px;
  box-sizing: border-box;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 99px;
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  border: 2px solid var(--background);
}

/* A quick, silent swing when a notification arrives while the app is open: the
   bell pivots from its top, the badge pops. Visual only. */
.notif-bell--swing svg {
  transform-origin: 50% 20%;
  animation: bell-swing 0.75s ease;
}

.notif-bell--swing .notif-bell__badge {
  animation: bell-badge-pop 0.5s ease;
}

@keyframes bell-swing {
  0%,
  100% {
    transform: rotate(0);
  }
  12% {
    transform: rotate(15deg);
  }
  26% {
    transform: rotate(-12deg);
  }
  40% {
    transform: rotate(9deg);
  }
  54% {
    transform: rotate(-6deg);
  }
  68% {
    transform: rotate(3deg);
  }
}

@keyframes bell-badge-pop {
  0%,
  100% {
    transform: scale(1);
  }
  45% {
    transform: scale(1.28);
  }
}

@media (prefers-reduced-motion: reduce) {
  .notif-bell--swing svg,
  .notif-bell--swing .notif-bell__badge {
    animation: none;
  }
}

/* Scrim + sheet, mirroring the standings drawer. */
.notif-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: rgba(6, 6, 10, 0.55);
}

.notif-sheet {
  position: relative;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 0;
  max-height: 86%;
  background: var(--surface);
  border-top: 1px solid var(--border-strong);
  border-radius: 26px 26px 0 0;
  box-shadow: 0 -24px 60px rgba(0, 0, 0, 0.5);
  text-align: left;
  animation: profileSheetIn 0.22s ease;
}

.notif-sheet__handle {
  flex: none;
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 11px 0 5px;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.notif-sheet__handle::before {
  content: "";
  width: 38px;
  height: 4px;
  border-radius: 99px;
  background: var(--border-strong);
}

.notif-sheet__head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 20px 12px;
}

.notif-sheet__title {
  font: 700 22px var(--font);
  color: var(--color);
}

.notif-sheet__mark {
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  font: 600 14px var(--font);
  color: var(--accent-bright);
}

.notif-sheet__mark:disabled {
  color: var(--text-faint);
  cursor: default;
}

/* Filter chips scope the list by kind; a right-aligned toggle hides reclaimed
   lost-tops. Echoes the unread card's accent when active. */
.notif-sheet__filters {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 20px 12px;
}

.notif-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--border-strong);
  border-radius: 99px;
  padding: 6px 12px;
  background: transparent;
  cursor: pointer;
  font: 600 13px var(--font);
  color: var(--text-mute);
  transition: background 0.14s, border-color 0.14s, color 0.14s;
}

.notif-chip[aria-pressed="true"] {
  color: var(--accent-bright);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.notif-chip__count {
  font-size: 11px;
  color: var(--text-faint);
}

.notif-chip[aria-pressed="true"] .notif-chip__count {
  color: var(--accent-bright);
}

.notif-hide {
  margin-left: auto;
  border: 1px solid var(--border-strong);
  border-radius: 99px;
  padding: 6px 12px;
  background: transparent;
  cursor: pointer;
  font: 600 12px var(--font);
  color: var(--text-mute);
  transition: background 0.14s, border-color 0.14s, color 0.14s;
}

.notif-hide[aria-pressed="true"] {
  color: var(--accent-bright);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.notif-sheet__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 0 20px calc(20px + env(safe-area-inset-bottom, 0px));
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

.notif-sheet__section {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 8px 2px 2px;
}

.notif-sheet__empty {
  padding: 24px 2px;
  font: 500 13px var(--font);
  color: var(--text-mute);
  text-align: center;
}

/* One card. Read cards are plain; unread carry an accent border + wash and a
   dot. */
.notif-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  box-sizing: border-box;
  text-align: left;
  padding: 12px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  background: transparent;
  cursor: pointer;
  position: relative;
  transition: background 0.14s, border-color 0.14s;
}

.notif-card:hover {
  background: var(--surface-2);
}

.notif-card--unread {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.notif-card__tile {
  flex: none;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  color: var(--text-mute);
  background: color-mix(in srgb, currentColor 16%, transparent);
}

.notif-card__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.notif-card__title {
  font: 700 15px var(--font);
  color: var(--color);
}

.notif-card__text {
  font: 500 13px var(--font);
  color: var(--text-mute);
  line-height: 1.35;
}

.notif-card__detail {
  font-size: 12px;
  color: var(--text-faint);
  margin-top: 1px;
}

.notif-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 4px;
}

.notif-card__link {
  font: 600 12px var(--font);
  color: var(--accent-bright);
}

.notif-card__ago {
  font-size: 11px;
  color: var(--text-faint);
}

.notif-card__dot {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
}

/* A reclaimed lost-top: kept but struck through and dimmed, with a tag saying
   why. History now, not a live nudge. */
.notif-card--reclaimed {
  opacity: 0.62;
}

.notif-card--reclaimed .notif-card__title {
  text-decoration: line-through;
  text-decoration-thickness: 2px;
  text-decoration-color: color-mix(in srgb, var(--text-faint) 80%, transparent);
  color: var(--text-mute);
}

/* The tag hue follows the standings record palette: gold for an outright top,
   chartreuse (--peak) for a shared one. `currentColor` carries into the check.
   */
.notif-card__tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  align-self: flex-start;
  margin-top: 5px;
  padding: 3px 8px;
  border-radius: 99px;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
  background: color-mix(in srgb, currentColor 12%, transparent);
  font: 700 10px var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Normalise the icon size: the solo crown is authored larger than the check. */
.notif-card__tag svg {
  width: 12px;
  height: 12px;
}

.notif-card__tag--solo {
  color: var(--gold);
}

.notif-card__tag--tied {
  color: var(--peak);
}

@media (min-width: 1200px) {
  .notif-modal {
    flex-direction: row;
    justify-content: flex-end;
  }

  .notif-sheet {
    width: 412px;
    height: 100%;
    max-height: none;
    border-radius: 0;
    border-top: 0;
    border-left: 1px solid var(--border-strong);
    box-shadow: -18px 0 50px rgba(0, 0, 0, 0.45);
    animation: standingsSideIn 0.18s ease;
  }

  .notif-sheet__handle {
    display: none;
  }

  .notif-sheet__head {
    padding-top: 20px;
  }
}

/* Profile > Notifications: an icon-tile row with a switch, echoing the card. */
.notif-pref {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
}

.notif-pref + .notif-pref {
  border-top: 1px solid var(--border-strong);
}

.notif-pref__tile {
  flex: none;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  background: color-mix(in srgb, currentColor 16%, transparent);
}

.notif-pref__text {
  flex: 1;
  min-width: 0;
}

.notif-pref__title {
  font: 600 15px var(--font);
  color: var(--color);
}

.notif-pref__sub {
  font: 500 12px var(--font);
  color: var(--text-mute);
  margin-top: 1px;
}

/* Why push isn't arriving, shown above the toggles when this device can't
   deliver (blocked permission, no push support). Amber, not red: in-app
   notifications still work, so it is a degraded state, not a failure. */
.push-notice {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 4px 0 2px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
  border-radius: 8px;
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}

.push-notice__text {
  flex: 1;
  min-width: 0;
  font: 500 12px var(--font);
  color: var(--warn);
}

.push-notice__action {
  flex: none;
  padding: 5px 12px;
  border: 1px solid var(--warn);
  border-radius: 999px;
  background: transparent;
  font: 600 12px var(--font);
  color: var(--warn);
  cursor: pointer;
}

/* On/off switch. */
.switch {
  flex: none;
  position: relative;
  width: 46px;
  height: 28px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-3);
  cursor: pointer;
  transition: background 0.16s;
}

.switch--on {
  background: var(--accent);
}

.switch__knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  transition: transform 0.16s;
}

.switch--on .switch__knob {
  transform: translateX(18px);
}
