:root {
  /* Redesign foundation tokens. Each value is light-dark(light, dark):
     it resolves against the element's `color-scheme`, so the palette
     follows the OS by default 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) — a green-yellow one
     step above the ramp's green, below 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: red -> magenta -> blue -> teal -> green, low to high.
     Goes the long way round the wheel so it never hits yellow/olive and
     stays 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 (which carries the
     breathing room + notch) plus the header's own height (content + bottom
     padding). The board and side panels size off this, so keep it in step
     with `body` padding-top and `.app-header` below. */
  --header-size: calc(76px + env(safe-area-inset-top, 0px));
  /* Footprint the HUD strip reserves above the board (min-height + bottom
     margin), folded into the board size so board + HUD fit under the
     header. 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 on the page is meant to
     be user-selectable. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Clip the board's scale(2) zoom to the viewport: it should be free to
   overflow its container and spill over the surrounding UI, but must not
   grow the page (which shifts the header and adds scroll on iOS). Full
   height so the clip region reaches the bottom of the screen — otherwise
   the body box stops at the board and the zoom is cut off below it. */
html,
body {
  height: 100%;
  overflow: hidden;
}

body {
  font-family: var(--font);
  margin: 0;
  /* Top breathing room (and the notch inset) lives here, not on the header,
     so the gap above the header is a page-level inset. Folded into
     --header-size so the board still fits; 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's drawn as a rectangle ignoring
     border-radius (so it squares off rounded controls), and .tapc already
     provides its own press feedback (the scale below). */
  -webkit-tap-highlight-color: transparent;
}

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

/* App header: logo + wordmark, with generous breathing room above
   (clearing the status bar / notch) and below. */
.app-header {
  display: grid;
  justify-content: center;
  align-items: center;
  gap: 24px;
  /* Mirror the .game grid (column template + padding are set per breakpoint
     to match it exactly) so the wordmark centers over the board and the
     avatar lines up with the right-hand panel's edge — not the raw viewport.
     Top inset lives on `body`; the header just keeps its bottom gap. */
  padding: 0 16px 20px;
  /* Reserve the action buttons' height (36px) as the row's floor — the tallest
     thing the header ever holds, above the 24px logo and the wordmark's line
     box. Without it the row is only as tall as the logo until the
     avatar/calendar buttons finish loading, then grows ~12px and pushes the
     whole board down; a late web-font swap would nudge it too. Pinning the row
     keeps --header-size (and the board's top) fixed from first paint. It's on
     the row, not min-height — the header is border-box, so its padding already
     clears any container min-height. */
  grid-auto-rows: minmax(36px, auto);
  /* The pull-to-refresh gesture grabs the header (the board owns touch below
     it — see usePullToRefresh), so claim vertical drags here to keep the
     gesture from being interrupted by a browser pan/bounce. Nothing scrolls
     from the header, so this costs no scrolling either way. */
  touch-action: none;
}

/* Pull-to-refresh indicator. A spinner chip that descends from behind the top
   edge as the header is pulled, tracking the pull distance, then spins on its
   own while the reload fires. Fixed so it floats over the header and board;
   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, so the chip matches whichever theme is
     active; 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 this off
     (--dragging) so the pull tracks 1:1; letting go plays this spring. */
  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 its 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, mirroring the design's notif button so the icons read
   as proper tap targets rather than bare glyphs. */
.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 coloured 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
   (revealed in the max-width block below). Placed 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): a soft accent pulse + a corner dot, held until the player opens the
   calendar and switches. Non-dismissible by design. */
.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 live on the SVG itself (not via
   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 instead of zooming. */
  touch-action: manipulation;
}

/* Full-screen fallback when a render crash unmounts the app (see
   ErrorBoundary) — a plain card and a reload button, no game chrome. */
.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 for a beat, then
   collapses to nothing where it stood — the rollback reads as an implosion
   rather than a glitch. transform-box scopes the scale to the block itself
   (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 same side breathing room the game board has, 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 its tip lands exactly there
     (12px inset + 7px half-base = 19px in; 7px past the edge). */
  transform: translate(var(--arrow-x, -19px), var(--arrow-y, 0px));
}

/* Arrow as a pseudo-element on the tip so it's always flush to its edge
   (a separately-positioned arrow drifted out of alignment). */
.tooltip::before {
  content: "";
  position: absolute;
  /* Fixed size: the arrow points at fixed-size chips outside the board, so it
     shouldn't scale with the board like the % positions do. */
  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 / stacked below on portrait. A grid so nothing depends on
   hand-computed offsets. Crucially nothing here clips overflow, so the
   board's scale(2) zoom still spills over the panels (only <body> clips). */
.game {
  display: grid;
  gap: 24px;
  justify-content: center;
  align-items: start;
  box-sizing: border-box;
  padding-inline: 16px;
  height: calc(var(--full-height) - var(--header-size));
}

/* Wide enough for three columns: today-result + calendar down the left,
   board centred, attempts on the right. The switch is width-based (not
   orientation) so a tall-but-wide window still gets the desktop layout; the
   side columns are capped so centring leaves outer margins on wide screens.
   Narrower: board on top, then result, attempts, calendar stacked — the
   attempts and calendar rows scroll within their own height so the board
   never moves. Board stays top-aligned (align-items: start); the scrolling
   panels opt back into filling their track with align-self: stretch.
   Nothing here clips the board's scale(2) zoom spill — the side panels only
   clip their OWN content, not the centred board sibling. */
@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 standings"
      "cal   board attempts";
  }

  /* 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 right (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 — less wasted space above the header. Kept in
     step with --header-size (inset + 44px header) so the board still 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 you scroll the whole .game down to the panels below. overflow-x is
     clipped so the board's scale(2) zoom can't add a horizontal scrollbar —
     here the zoom is clipped to the game viewport (unlike desktop, where the
     board is free to spill). Rows are auto/auto so content can exceed the
     fixed .game height and scroll. */
  .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; leftover height falls to the bottom.
       Without this the auto rows stretch to fill the fixed height (default
       align-content), pushing the runs down into the middle now that the
       calendar no longer fills the panel. */
    align-content: start;
    /* Tighter than the desktop 24px so the runs sit closer under the board
       (~20px total with the panel's top padding, matching header→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, positioned by justify-self), so both line up with the
     board/panel inset rather than the raw viewport corners. With only two
     icons here, a 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, reached from the header. */
  .cal-button {
    display: inline-flex;
  }

  .game__panel {
    grid-area: panel;
    display: flex;
    flex-direction: column;
    gap: 16px;
    /* Only vertical spacing; horizontal gutter comes from .game's
       padding-inline, so the panels line up with the board's inset. Bottom
       clears the pinned standings dock (~64px tall) plus 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 no longer sits below the runs on mobile (it's a picker now),
     so the runs are free to expand to their full height and the whole page
     scrolls. Descendant selector so it beats the later base rule by
     specificity (base .attempts comes after in source). */
  .game__panel .attempts {
    padding: 0;
  }

  /* Today's result would only ever sit at the top on mobile, where it mostly
     duplicates the board state — drop it and lead with the runs. */
  .game__panel .today-result {
    display: none;
  }
}

.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 no longer opens
   on load — the player starts it here, so a background boot can't spend one.
   A themed blur mask (light and dark) over the board, backed by a decorative,
   blurred copy of the tutorial maze — a real-looking board that is 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 exactly (not the taller game__board, which includes
     the Hud strip) so the mask is symmetric on all sides. Same width + centering
     + rounding as `.board-frame`, anchored to the board at the bottom. */
  bottom: 0;
  /* Center off a DEFINITE width (100% capped at --maze-size), not a
     left:0/right:0 + margin-auto stretch. Safari mis-derives an
     absolutely-positioned box's aspect-ratio height when the width comes from a
     left+right stretch: the square renders too short, and since the blurred
     deco and scrim are both inset:0, the shortfall exposes a sharp strip of the
     real board above the overlay (the blur "not aligning" at the top). A
     definite width makes the aspect-ratio height resolve correctly in Safari
     and is identical in Chromium/Firefox. */
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--maze-size);
  aspect-ratio: 1;
  z-index: 10;
  display: flex;
  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
     rounded card. clip-path clips the whole subtree — filtered descendants
     included — in both engines, so round the corners here. */
  clip-path: inset(0 round var(--radius));
  /* New stacking context so the deco/scrim/content z-indexes below order
     against each other, not the rest of the board. */
  isolation: isolate;
}

/* Bottom layer: the tutorial maze, blurred + faded to a texture. Its own board
   frame (border, shadow, rounding) is stripped so it fills edge to edge rather
   than reading as a second board nested inside the overlay. */
.prestart__deco {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  filter: blur(16px) saturate(0.9);
  opacity: light-dark(0.5, 0.6);
  /* Pin this blurred subtree to its own composited backing. Without it, when
     Chrome re-rasterizes the page at a reduced scale for the tab-hover preview
     thumbnail, the board-sized `filter: blur` is applied per raster-tile and
     leaves a hairline seam at a tile boundary — which stays baked into the
     cached filter texture until the next full repaint (opening dev tools or
     tabbing out and back clears it). A dedicated layer makes the blur rasterize
     as one texture, so the capture can't inject the seam. */
  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 below it for text contrast,
   and (via backdrop-blur) softens the empty real board behind the whole
   overlay. Sits ABOVE the deco, BELOW the content. */
.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));
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}

.prestart__content {
  position: relative;
  z-index: 2;
  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 (with a 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 it improved (higher time is better), red when it
   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);
}

/* 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, not a fixed green/blue. Border
   + 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 + 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 on the left, pager + close grouped on the right. It
   sticks to the top 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, comfortably tappable cells in the picker than the desktop inline
   grid. */
.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. The new day
   isn't a normal cell (the calendar only lists days you've played), so this is
   how you get there. */
.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, so a top margin gives the
   gap between one month and the next. */
.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 reads with more contrast against the dark grid than the
     accent blue, 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 so it works on light or dark
   fills. Long near-100 percentages step down a size to keep fitting the cell. */
.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 lives in the
   max-width media block above (as descendant selectors, to win by
   specificity). */
.attempts {
  grid-area: attempts;
  align-self: stretch;
  min-height: 0;
  overflow-y: auto;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
  /* Breathing room under the last row when scrolled to the end, so it doesn't
     sit flush against the viewport edge. Desktop only — the mobile rule below
     zeroes panel padding (the whole page scrolls there). A block scroll
     container honours bottom padding at the scroll end. */
  padding-bottom: 16px;
}

/* Heading row: the "Runs" title and the local sort toggle. Carries the title's
   old margin so the two sit 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): keep the row's own band colour but
   lift it — a heavier border and the deeper "highlighted" fill (shared with the
   daily-result best row) — plus a matching label, so it's easy to spot which row
   you're looking at without a second (blue) accent. Just 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;
}

.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 remains 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 (pushes a run to the top of the panel). Sits 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 carried by the icon's FILL (solid vs outline) plus full-
   strength neutral foreground — deliberately no accent hue. The row is already
   saturated with meaning-bearing colour (the standing band, the Daily chip, the
   supreme/record badges), so the pin stays monochrome and doesn't add a colour
   that competes — or, at --accent-bright, reads as kin to the Daily chip. */
.attempts__pin--active {
  color: var(--color);
}

/* Where a pointer can hover, keep the affordance quiet until the row is engaged
   (or the run is already pinned / the button focused) — so it's present but not
   loud. Touch has no hover, so there it stays visible at all times (the default
   rule above). */
@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 own band edge a touch so the floated-to-top rows read
   as a set, without a second accent colour fighting the band. */
.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 even when empty so the board doesn't shift when the
     build phase (and the chips) load in. 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?"
   are stacked in one grid cell so the chip keeps the (wider) countdown's
   width and can cross-fade between them without a 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, now 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 → back to a 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?" outright. */
.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 doubled as a verdict. Its fill (set
   inline) tracks the run's live score % on the standing ramp; the % rides
   alongside 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 time + % still sit on one line. */
  align-items: center;
  gap: 8px;
  padding: 0 15px;
  /* Inline background/color (the live % colour + 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's blue surface 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 own 0–20
   space, anchored over the board square exactly like the prestart mask and
   clipped to it. The flood is one 1×1 cell per free square (pieces get none, so
   the crest never draws over a block); each lights and fades 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; }
}

/* Respect reduced-motion: drop the travelling delay so the flood-shaped region
   just softly glows and fades in place, no racing front. 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 up-and-down 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 (for a personal best / supreme) confetti bursting out on
   every side. It's 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`, so drawing here lets the effects spill
   above and right of the button without being cut. It fires when the run commits
   and is unmounted when the board re-stages at finish; it 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 they cast their shadow outward unclipped up here. */
.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
     whole viewport — but never let it drift 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 tunes the border strength for a
   heavier variant. Used by the runs rows, today cards, and the daily-result best
   row so the treatment stays identical. Defined here (after .result__row) so it
   wins over that neutral base; component modifiers that need to 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 (so a sparse profile doesn't strand a
   full-screen void). Desktop: a centred dialog card, like the daily result.
   Both are 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 rather than to its grid's
     min-content (which would push the sheet wider than a phone screen). */
  min-width: 0;
  max-width: 480px;
  /* Cap so a tall sheet never eats the whole screen: always leave a backdrop
     gap at the top (the board stays visible behind it) — at least 100px on
     short screens, 15vh on tall ones — and scroll the body internally past
     that. Desktop drops the cap (centred card, see below). */
  max-height: min(85vh, calc(100vh - 100px));
  overflow-y: auto;
  text-align: left;
  background: var(--surface);
  /* Bottom sheet: rounded top only, flush to the screen's 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 and rounded on all sides. */
@media (min-width: 1200px) {
  .modal {
    align-items: center;
    padding: 24px;
  }

  .modal__sheet {
    max-width: 420px;
    /* Centred card: leave the 24px backdrop gap top+bottom and scroll the body
       internally past that, so a tall dialog (the full profile) never runs off
       the top/bottom of 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). */
.profile-sheet {
  padding: 24px 20px calc(22px + 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>. Let
   the taps fall through to the buttons beneath. */
.modal svg {
  pointer-events: none;
}

/* Shared header/close icon button used across both modals so the affordances
   (profile ×, move ‹ / ×) look identical. */
.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 to the right. Held to the
   avatar's height so swapping in the (shorter) rename bar doesn't shrink the
   dialog — the bar just centres in the same space. */
.profile-head {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 52px;
  margin: 8px 0 18px;
  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 whole header row while
   editing, so the input isn't crushed between the 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: 10px;
}

.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;
}

/* Best build — a wide tile, clickable to review the maze on the board. */
.profile-best {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  margin: 0 0 18px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
  text-align: left;
}

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

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

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

/* Neutral, not the accent blue — blue is a mid-percentile band in this app's
   ramp, and best build carries no percentile, so it shouldn't imply one. */
.profile-best__value {
  font-size: 22px;
  font-weight: 700;
  color: var(--color);
}

/* 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, not a
     text link — 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 otherwise dims the
     whole card on top of the surface change (the Account button, a <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 occupies 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, and when it's a genuine zero — so the green "live" dot only appears
   when someone is actually 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: 14px;
}

.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 rather than 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, 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) but 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 the 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 done 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: 12px;
  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 but in a neutral tint. */
.delete-safety {
  background: var(--surface-2);
  border-color: var(--border);
}

/* ── Move / merge gate ─────────────────────────────────────────────────────
   The pre-app takeover that resolves a sign-in link opening 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, matching the mock — a lone confirm card shouldn't float
     mid-screen. Clamped so it's high on a phone without gluing to the very 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 → pick keeps the top edge
   fixed and the dialog only grows downward instead of jumping up. */
.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 tucked 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 → 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 against it. */
  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);
  }
}

/* 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. Colour discipline mirrors the runs panel: one accent
   per row — 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 so the board reads calm. */

.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 the 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, mirroring the
   sheet's match rows (gold stays reserved for an outright #1). */
.standings-dock__lead--lime {
  color: var(--peak);
}

/* Desktop: a peek card at the top of the right rail (its own grid area, above
   the runs). */
@media (min-width: 1200px) {
  .standings-dock {
    grid-area: standings;
    align-self: start;
  }

  /* The sheet is a side drawer here (slides in from the right — see
     .standings-sheet below), not a bottom sheet, so the "expands upward" caret
     is rotated to point left: an "opens a side panel" affordance instead. */
  .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 × appears on the desktop side
   sheet, which has neither. */
.standings-sheet__close {
  display: none;
}

/* Sort control per the design: a "SORT BY" label, then DAILY / PB as bare
   text buttons — the active one 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 and your row stays findable. --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 its reveal can't shift the runs below
   it (desktop rail) — and visibility drops it from the tab order and pointer
   targets while concealed. */
.standings-dock--hidden {
  visibility: hidden;
}

/* ==== Mobile sheet dismissal (shared) =====================================
   On mobile every sheet closes like a sheet: a grab notch at the top — tap or
   drag down (useDragToClose) — replacing the per-dialog ×, which stays for
   desktop. Absolute so existing sheet padding/layout 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 × (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 side drawer on desktop — adapting .standings-modal), the
   cards, and the profile preference rows with their on/off 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 glyph pivots from its top, the badge gives a small pop. Purely visual. */
.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 treatment 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. The whole card is de-emphasised — it's 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;
}

/* 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);
}
