/* ═══════════════════════════════════════════════════════════════════
   Zoomies — Design System
   Apple Liquid Glass inspired UI with spring-physics interactions
   ═══════════════════════════════════════════════════════════════════ */

/* ─── Design Tokens ──────────────────────────────────────────────── */
:root {
  /* Liquid Glass surfaces */
  --glass: rgba(18, 24, 38, 0.38);
  --glass-light: rgba(18, 24, 38, 0.28);
  --glass-border: rgba(255, 255, 255, 0.14);
  --glass-border-strong: rgba(255, 255, 255, 0.24);
  --glass-blur: blur(20px) saturate(1.6) brightness(1.05);
  --glass-soft: rgba(255, 255, 255, 0.04);
  --glass-stroke-soft: rgba(255, 255, 255, 0.08);

  /* Text shadow for legibility on glass */
  --text-shadow: 0 1px 2px rgba(0, 0, 0, 0.16);

  /* Liquid Glass specular — prismatic light refraction highlights */
  --glass-specular:
    radial-gradient(ellipse at 20% 20%, rgba(180, 220, 255, 0.14) 0%, transparent 50%),
    radial-gradient(ellipse at 80% 15%, rgba(200, 180, 255, 0.1) 0%, transparent 45%),
    radial-gradient(ellipse at 30% 80%, rgba(160, 240, 255, 0.07) 0%, transparent 50%),
    radial-gradient(ellipse at 85% 75%, rgba(220, 200, 255, 0.06) 0%, transparent 45%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 30%, transparent 50%);

  /* Accent — cool blue-white glass tint */
  --accent: rgba(160, 210, 255, 0.9);
  --accent-glow: rgba(160, 210, 255, 0.25);
  --accent-dim: rgba(160, 210, 255, 0.1);
  /* Inside fill: subtle prismatic wash */
  --accent-gradient: linear-gradient(135deg,
      rgba(180, 220, 255, 0.12) 0%, rgba(200, 180, 255, 0.1) 40%,
      rgba(160, 240, 255, 0.1) 70%, rgba(220, 200, 255, 0.12) 100%);
  /* Border: iridescent shimmer — visible pastel gradient */
  --accent-border: linear-gradient(135deg,
      rgba(160, 200, 255, 0.85) 0%, rgba(190, 170, 255, 0.7) 30%,
      rgba(130, 230, 255, 0.7) 60%, rgba(200, 180, 255, 0.8) 100%);

  /* Text — vibrant on glass */
  --text: #e8f4ff;
  --text-dim: rgba(255, 255, 255, 0.6);
  --text-muted: rgba(255, 255, 255, 0.3);

  /* Gold (coins) */
  --gold: #ffd700;
  --gold-border: rgba(255, 215, 0, 0.4);

  /* Danger */
  --danger: #ff5e5e;
  --danger-bg: rgba(255, 60, 60, 0.15);
  --danger-border: rgba(255, 60, 60, 0.35);

  /* Timing — spring-inspired curves */
  --spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --spring-soft: cubic-bezier(0.25, 1.2, 0.5, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

  /* Durations */
  --dur-fast: 0.15s;
  --dur-med: 0.25s;
  --dur-slow: 0.4s;
  --dur-bounce: 0.5s;

  /* Radius — proportional smooth corners */
  --r-xs: 6px;
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 16px;
  --r-xl: 22px;
  --r-pill: 100px;
  --r-circle: 50%;

  /* Typography */
  --font-ui: "Nunito Sans Variable", "Nunito Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --font-mono: "SF Mono", "IBM Plex Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --type-leading-tight: 1.2;
  --type-leading-copy: 1.45;
  --type-tracking-tight: -0.01em;
  --type-tracking-base: 0.01em;
  --type-tracking-wide: 0.045em;
  --weight-regular: 500;
  --weight-medium: 600;
  --weight-semibold: 700;
  --weight-bold: 800;
}

/* ─── Base ────────────────────────────────────────────────────────── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Reset default browser styles on interactive elements */
button,
input,
select,
textarea {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  outline: none;
  padding: 0;
  margin: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #d4dce8;
  color: var(--text);
  font-family: var(--font-ui);
  letter-spacing: var(--type-tracking-base);
  line-height: var(--type-leading-copy);
  text-rendering: optimizeLegibility;
  font-feature-settings: "kern" 1, "liga" 1;
  -webkit-font-smoothing: antialiased;
}

canvas#c {
  display: block;
  width: 100%;
  height: 100%;
}

h1,
h2,
h3,
h4 {
  line-height: var(--type-leading-tight);
  letter-spacing: var(--type-tracking-tight);
  font-weight: var(--weight-bold);
}

/* ─── Loading ────────────────────────────────────────────────────── */
/* Loading overlay styles live inline in vite-index.html so the spinner
   renders instantly on first paint, before this stylesheet arrives. */

/* FPS counter lives inside the run-timer pill (see .run-pill__fps). */

/* ═══════════════════════════════════════════════════════════════════
   GLASS BUTTON SYSTEM — shared base + variant modifiers
   All interactive glass buttons use .glass-btn as the base class.
   ═══════════════════════════════════════════════════════════════════ */
.glass-btn {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--r-md);
  color: #fff;
  font-family: inherit;
  font-weight: var(--weight-semibold);
  letter-spacing: var(--type-tracking-base);
  line-height: var(--type-leading-tight);
  cursor: pointer;
  text-shadow: var(--text-shadow);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
  transition: all 0.35s var(--ease-out-expo);
  position: relative;
}

.glass-btn:hover {
  transform: translateY(-2px);
  border-color: rgba(160, 210, 255, 0.5);
  box-shadow: 0 10px 32px rgba(120, 180, 255, 0.2);
  filter: brightness(1.12);
  background-image: radial-gradient(circle 220px at var(--mx, 50%) var(--my, 50%),
      rgba(180, 210, 255, 0.12) 0%,
      transparent 70%);
}

.glass-btn:active {
  transform: translateY(1px);
}

/* ── Primary: colored gradient overlays ─────────────────────────── */
.glass-btn--primary {
  border-color: rgba(130, 190, 255, 0.35);
  background-image:
    radial-gradient(ellipse at 30% 0%, rgba(120, 180, 255, 0.22) 0%, transparent 50%),
    radial-gradient(ellipse at 70% 0%, rgba(180, 140, 255, 0.18) 0%, transparent 50%),
    radial-gradient(ellipse at 50% 100%, rgba(100, 220, 255, 0.12) 0%, transparent 50%);
}

/* ── Secondary: lighter glass, subtler border ───────────────────── */
.glass-btn--secondary {
  background-color: var(--glass-light);
  border-color: rgba(255, 255, 255, 0.14);
  color: #e0e8f4;
}

.glass-btn--secondary:hover {
  border-color: rgba(255, 255, 255, 0.35);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
  filter: brightness(1.1);
}

/* ── Danger: red-tinted glass ───────────────────────────────────── */
.glass-btn--danger {
  background-color: var(--glass-light);
  background-image:
    radial-gradient(ellipse at 50% 0%, rgba(255, 100, 100, 0.1) 0%, transparent 50%);
  color: rgba(255, 180, 180, 0.9);
  border-color: rgba(255, 120, 120, 0.3);
}

.glass-btn--danger:hover {
  border-color: rgba(255, 120, 120, 0.55);
  box-shadow: 0 8px 28px rgba(255, 80, 80, 0.15);
  filter: brightness(1.12);
}

/* ── Tile: brighter recipe for buttons sitting INSIDE a glass card.
   The base .glass-btn paints var(--glass) (the same dark navy used
   by glass cards) and applies its own backdrop-filter. When such a
   button sits inside an already-glass surface (.pause-card,
   #finishDialogCard, future panels) it stacks dark-on-dark and
   reads as a sunken patch instead of a raised tile.

   --tile overrides three things to fix that:
     1. background-color: whisper-white tint (~6% white) so the
        button reads as elevated against the card.
     2. backdrop-filter: none — the host card already blurs the
        scene behind it; a per-button blur is redundant AND costs
        a stacking-context pop-in during view enter animations.
     3. box-shadow: slim raised-tile recipe instead of the heavier
        floating-pill ambient shadow.

   Variant gradients (.glass-btn--primary / --danger) still
   composite on top via background-image, so color hierarchy works
   on tiles too. --danger pins background-color to the dark
   --glass-light at the variant level, so we re-pin a brighter
   tile-danger bg below. */
.glass-btn--tile {
  background-color: rgba(255, 255, 255, 0.06);
  border-color: var(--glass-border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    0 1px 2px rgba(0, 0, 0, 0.18);
}

.glass-btn--tile:hover {
  background-color: rgba(255, 255, 255, 0.10);
  border-color: var(--glass-border-strong);
}

.glass-btn--tile.glass-btn--danger {
  background-color: rgba(255, 255, 255, 0.05);
}

/* ═══════════════════════════════════════════════════════════════════
   CONTROL PANEL
   ═══════════════════════════════════════════════════════════════════ */
.panel {
  position: fixed;
  top: 12px;
  z-index: 200;
  background-color: var(--glass);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--r-xl);
  padding: 0;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: radial-gradient(white, black);
  color: var(--text);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 24px);
  transform-origin: top left;
  animation: panelIn 0.5s var(--spring) both;
  box-shadow:
    0 12px 48px rgba(0, 0, 0, 0.2),
    0 2px 6px rgba(0, 0, 0, 0.1);
  /* Liquid glass specular top edge */
  background-image: var(--glass-specular);
  background-size: 100% 100%;
}

@keyframes panelIn {
  from {
    opacity: 0;
    transform: scale(0.92) translateY(-8px);
  }

  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

.panel-l {
  left: 12px;
  width: 290px;
}


.panel-header {
  padding: 12px 14px 8px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid rgba(145, 222, 255, 0.08);
}

.panel-header h3 {
  font-size: 14px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
  color: #fff;
  text-shadow: var(--text-shadow);
}

.panel-close {
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  border: none;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  cursor: pointer;
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--dur-med) var(--spring);
}

.panel-close:hover {
  background: var(--danger-bg);
  color: var(--danger);
  transform: scale(1.1) rotate(90deg);
}

.panel-close:active {
  transform: scale(0.9) rotate(90deg);
}

.panel-scroll {
  overflow-y: auto;
  flex: 1;
  padding: 4px 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 100%);
  transition: -webkit-mask-image 0.3s ease, mask-image 0.3s ease;
}

.panel-scroll.scroll-fade {
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 32px), transparent);
  mask-image: linear-gradient(to bottom, black calc(100% - 32px), transparent);
}

.panel-scroll::-webkit-scrollbar {
  width: 4px;
}

.panel-scroll::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.12);
  border-radius: 2px;
}

/* Panel FAB (show when panel closed) */
.panel-fab {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 199;
  width: 48px;
  height: 48px;
  border-radius: var(--r-lg);
  border: 1px solid var(--glass-border);
  background-color: var(--glass);
  color: var(--accent);
  font-size: 20px;
  cursor: pointer;
  display: none;
  backdrop-filter: var(--glass-blur);
  align-items: center;
  justify-content: center;
  transition: all var(--dur-med) var(--spring);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

.panel-fab:hover {
  transform: scale(1.08);
  border-color: var(--glass-border-strong);
}

.panel-fab:active {
  transform: scale(0.92);
}

/* ─── Section groups ─────────────────────────────────────────────── */
.section-group {
  margin-bottom: 6px;
  background: var(--glass-soft);
  border: 1px solid var(--glass-stroke-soft);
  border-radius: var(--r-md);
  padding: 2px 0;
}

.section-group:last-child {
  margin-bottom: 0;
}

.section-group h4 {
  font-size: 11px;
  letter-spacing: var(--type-tracking-wide);
  color: rgba(255, 255, 255, 0.9);
  margin: 0;
  font-weight: var(--weight-semibold);
  padding: 8px 10px;
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  gap: 6px;
  transition: background 0.15s;
  border-radius: var(--r-md);
  text-shadow: var(--text-shadow);
}

.section-group h4 i {
  font-size: 13px;
  opacity: 0.7;
}

.section-group h4:hover {
  background: rgba(255, 255, 255, 0.04);
}

/* ─── Control rows ───────────────────────────────────────────────── */
.ctrl-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 2px;
  min-height: 28px;
  padding: 2px 10px;
}

.ctrl-row label {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.9);
  font-weight: var(--weight-medium);
  letter-spacing: 0.015em;
  display: flex;
  align-items: center;
  gap: 5px;
  text-shadow: var(--text-shadow);
}

.ctrl-row label i {
  font-size: 13px;
  opacity: 0.6;
  flex-shrink: 0;
}

/* ─── Segmented buttons ──────────────────────────────────────────── */
.seg {
  display: inline-flex;
  gap: 3px;
  border-radius: var(--r-sm);
}

.seg button {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--r-xs);
  color: rgba(255, 255, 255, 0.8);
  padding: 6px 10px;
  font-size: 11px;
  font-weight: var(--weight-medium);
  cursor: pointer;
  position: relative;
  z-index: 1;
  transition: all var(--dur-med) var(--spring);
  letter-spacing: 0.01em;
  font-family: inherit;
  text-shadow: var(--text-shadow);
}

.seg button.on {
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
  font-weight: var(--weight-semibold);
  border-color: rgba(255, 255, 255, 0.3);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 2px 8px rgba(0, 0, 0, 0.15);
}

.seg button:not(.on):hover {
  background: rgba(255, 255, 255, 0.12);
  transform: scale(1.04);
  border-color: rgba(255, 255, 255, 0.2);
}

.seg button:active {
  transform: scale(0.88);
  transition-duration: 0.08s;
}

/* ─── Toggle switch (Liquid Glass pill) ──────────────────────────── */
.toggle-sw {
  position: relative;
  width: 36px;
  height: 20px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: var(--r-pill);
  cursor: pointer;
  flex-shrink: 0;
  transition: all 0.3s var(--spring);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
  /* No mask-image — border-radius already rounds the pill, and a mask
     creates a compositing layer that can clip the keyboard focus
     indicator (outline + halo box-shadow) on Chromium. The thumb
     pseudo-element stays fully inside the pill at all times so we
     don't need a clip mask for visual safety. */
}

.toggle-sw.on {
  background: rgba(100, 180, 255, 0.3);
  border-color: rgba(100, 180, 255, 0.4);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05), 0 0 12px rgba(100, 180, 255, 0.15);
}

.toggle-sw::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.6);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  transition: all 0.3s var(--spring);
}

.toggle-sw.on::after {
  transform: translateX(16px);
  background: #fff;
  box-shadow: 0 0 8px rgba(100, 180, 255, 0.4), 0 1px 3px rgba(0, 0, 0, 0.15);
}

.toggle-sw:active {
  transform: scale(0.9);
}

.toggle-sw:active::after {
  transform: scaleX(1.25);
}

.toggle-sw.on:active::after {
  transform: translateX(12px) scaleX(1.25);
}

/* ─── Sliders ────────────────────────────────────────────────────── */
.slider-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 2px;
  padding: 2px 10px;
}

.slider-row label {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.9);
  min-width: 50px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.01em;
  text-shadow: var(--text-shadow);
}

.slider-row span {
  font-size: 11px;
  color: var(--text-dim);
  min-width: 42px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.015em;
  text-shadow: var(--text-shadow);
}

input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.1);
  transition: background var(--dur-fast);
}

input[type=range]:hover {
  background: rgba(255, 255, 255, 0.15);
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: var(--r-circle);
  background: radial-gradient(circle at 35% 35%, rgba(255, 255, 255, 0.9), rgba(200, 230, 255, 0.7));
  border: 1px solid rgba(255, 255, 255, 0.4);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 0 12px rgba(145, 222, 255, 0.2),
    inset 0 1px 2px rgba(255, 255, 255, 0.8);
  cursor: pointer;
  transition: transform var(--dur-med) var(--spring), box-shadow var(--dur-med);
}

input[type=range]::-webkit-slider-thumb:hover {
  transform: scale(1.25);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3), 0 0 20px rgba(145, 222, 255, 0.35),
    inset 0 1px 2px rgba(255, 255, 255, 0.9);
}

input[type=range]::-webkit-slider-thumb:active {
  transform: scale(1.1);
}

input[type=range]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: var(--r-circle);
  border: none;
  background: radial-gradient(circle at 35% 35%, rgba(255, 255, 255, 0.9), rgba(200, 230, 255, 0.7));
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 0 12px rgba(145, 222, 255, 0.2);
  cursor: pointer;
}

/* ─── Part tooltip ───────────────────────────────────────────────── */
.part-tooltip {
  position: fixed;
  z-index: 500;
  pointer-events: none;
  background-color: var(--glass);
  border-radius: var(--r-sm);
  padding: 8px 12px;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  opacity: 0;
  transition: opacity 0.2s var(--ease-out-expo);
  font-size: 11px;
  color: var(--text);
  text-shadow: var(--text-shadow);
}

.part-tooltip.visible {
  opacity: 1;
}

.tip-name {
  font-weight: 700;
  font-size: 12px;
  margin-bottom: 2px;
}

.tip-dims {
  color: var(--text-dim);
  font-size: 10px;
}

/* ═══════════════════════════════════════════════════════════════════
   PLAY MODE BUTTON — hero CTA, gradient, large
   ═══════════════════════════════════════════════════════════════════ */
#playDockBtn {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;
  padding: 16px 40px;
  font-size: 16px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  line-height: 1;
}

#playDockBtn i {
  font-size: 18px;
  line-height: 1;
}

#playDockBtn:hover {
  transform: translateX(-50%) translateY(-3px);
}

#playDockBtn:active {
  transform: translateX(-50%) scale(0.96);
}

.key-hint {
  opacity: 1;
  font-size: 12px;
  margin-left: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  color: #f6fbff;
  background: linear-gradient(180deg, rgba(150, 208, 255, 0.26), rgba(120, 165, 255, 0.22));
  border: 1px solid rgba(205, 235, 255, 0.55);
  border-radius: 6px;
  padding: 0 8px;
  font-weight: var(--weight-bold);
  letter-spacing: normal;
  line-height: 1;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), 0 2px 8px rgba(70, 120, 210, 0.35);
}

/* ═══════════════════════════════════════════════════════════════════
   GAME HUD
   ═══════════════════════════════════════════════════════════════════ */
#fpHud {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 300;
  pointer-events: none;
}

/* Run HUD — timer + coins live inside a single center-anchored pill so
   they stay near the middle of the screen on ultrawide monitors instead
   of sticking to the far corners. */
.run-hud-center {
  position: fixed;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
  pointer-events: none;
}

.run-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid rgba(170, 220, 255, 0.26);
  border-radius: 999px;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.22);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  transition: border-radius 0.25s var(--ease-out-expo), padding 0.25s var(--ease-out-expo);
}

/* Inner sub-groups inside the combo pill have no chrome of their own. */
.run-pill--combo>#runTimerHud,
.run-pill--combo>#coinHud {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.run-pill__divider {
  width: 1px;
  align-self: stretch;
  background: linear-gradient(to bottom,
      rgba(255, 255, 255, 0) 0%,
      rgba(255, 255, 255, 0.18) 25%,
      rgba(255, 255, 255, 0.18) 75%,
      rgba(255, 255, 255, 0) 100%);
  margin: 0 2px;
}

/* When the timer is showing its stacked FPS readout, soften the hard
   999px pill so the short label below doesn't look crammed. */
.run-pill--combo:has(.run-pill__fps:not([hidden])) {
  border-radius: 22px;
  padding: 14px 16px;
}

.run-pill__icon {
  font-size: 18px;
  color: rgba(207, 236, 255, 0.85);
}

.run-pill__icon--gold {
  color: #ffd700;
  font-size: 20px;
}

.run-pill__icon--cyan {
  color: #4ab8ff;
}

/* Timer (left of center) */
.run-pill--timer {
  color: #f4fbff;
}

.run-pill--timer #runTimerText {
  font-family: var(--font-mono);
  font-size: 30px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
  color: #f4fbff;
  text-shadow: 0 1px 10px rgba(159, 227, 255, 0.22);
  transition: color 0.25s ease;
}

.run-pill__stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.05;
  gap: 4px;
}

.run-pill__fps {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  color: rgba(215, 238, 255, 0.82);
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
  white-space: nowrap;
  text-transform: lowercase;
}

.run-pill__fps[hidden] {
  display: none;
}

#runTimerHud.ready #runTimerText {
  color: rgba(232, 244, 255, 0.7);
}

#runTimerHud.running #runTimerText {
  color: #f4fbff;
}

#runTimerHud.finished #runTimerText {
  color: #ffe199;
  text-shadow: 0 1px 12px rgba(255, 215, 115, 0.35);
}

#runTimerHud.finished .run-pill__icon {
  color: #ffd873;
}

#runTimerHud.running .run-pill__icon {
  color: #9fe3ff;
}

/* Coins (right of center) — secret-coin chip grows the pill rightward */
.run-pill--coins {
  color: var(--gold);
  font-size: 17px;
  font-weight: 800;
  transform-origin: 0 50%;
}

/* Yellow-coin pickup: flip the gold coin icon like a tossed coin and pop
 * the count number with a brief warm gold glow. Scoped to the gold icon
 * + #coinCount only so the nested #secretCoinHud (blue chip) doesn't
 * get dragged along. */
#coinHud.bump .run-pill__icon--gold {
  display: inline-block;
  transform-origin: 50% 50%;
  animation: coinFlip 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}

#coinHud.bump #coinCount {
  display: inline-block;
  transform-origin: 50% 60%;
  animation: coinCountPop 0.45s var(--spring);
}

@keyframes coinFlip {
  0% {
    transform: rotateY(0deg) scale(1);
    filter: drop-shadow(0 0 0 rgba(255, 215, 0, 0));
  }

  35% {
    transform: rotateY(180deg) scale(1.18);
    filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.95));
  }

  70% {
    transform: rotateY(360deg) scale(1.06);
    filter: drop-shadow(0 0 6px rgba(255, 200, 80, 0.55));
  }

  100% {
    transform: rotateY(360deg) scale(1);
    filter: drop-shadow(0 0 0 rgba(255, 215, 0, 0));
  }
}

@keyframes coinCountPop {
  0% {
    transform: scale(1) translateY(0);
    color: var(--gold);
    text-shadow: none;
  }

  35% {
    transform: scale(1.22) translateY(-1px);
    color: #fff7c8;
    text-shadow: 0 0 10px rgba(255, 215, 0, 0.9), 0 0 20px rgba(255, 180, 60, 0.5);
  }

  100% {
    transform: scale(1) translateY(0);
    color: var(--gold);
    text-shadow: none;
  }
}

/* MPH speedometer (skateboard section of the HUD pill) */
.run-pill--combo>#mphHud {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.run-pill--mph {
  color: #f4fbff;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

#mphValue {
  font-family: var(--font-mono);
  font-size: 28px;
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  color: #f4fbff;
  text-shadow: 0 1px 10px rgba(159, 227, 255, 0.22);
  transition: color 0.2s ease;
  min-width: 2.2ch;
  text-align: right;
  display: inline-block;
}

/* Glow hotter as speed increases — driven by data-heat attribute */
#mphValue[data-heat="1"] {
  color: #ffe066;
  text-shadow: 0 0 12px rgba(255, 220, 80, 0.5);
}

#mphValue[data-heat="2"] {
  color: #ffb347;
  text-shadow: 0 0 16px rgba(255, 160, 60, 0.65);
}

#mphValue[data-heat="3"] {
  color: #ff6b6b;
  text-shadow: 0 0 20px rgba(255, 80, 80, 0.7);
}

.mph-unit {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(207, 236, 255, 0.6);
  margin-left: -2px;
  align-self: flex-end;
  padding-bottom: 2px;
}

/* Yellow-coin sparkle burst (see ui-interactions.js coinBump). Lives on
 * a fixed-position overlay anchored to the gold icon's viewport center
 * so it isn't constrained by the run-pill clipping/overflow. */
.coin-burst {
  position: fixed;
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 400;
}

.coin-burst__spark {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--sz, 4px);
  height: var(--sz, 4px);
  margin-left: calc(var(--sz, 4px) * -0.5);
  margin-top: calc(var(--sz, 4px) * -0.5);
  background: var(--c, #ffd700);
  border-radius: 50%;
  box-shadow: 0 0 6px var(--c, #ffd700), 0 0 10px rgba(255, 200, 80, 0.55);
  animation: coinSpark 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  will-change: transform, opacity;
}

/* 4-point sparkle star: two crossed thin bars via a clip-path-free trick
 * (use radial gradient + ::before pseudo). Cheaper: use a CSS mask shape
 * built from two perpendicular linear gradients. */
.coin-burst__spark--star {
  background: transparent;
  box-shadow: none;
  border-radius: 0;
}

.coin-burst__spark--star::before,
.coin-burst__spark--star::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--c, #ffd700);
  filter: drop-shadow(0 0 4px var(--c, #ffd700));
}

.coin-burst__spark--star::before {
  /* horizontal needle */
  clip-path: polygon(0 50%, 50% 42%, 100% 50%, 50% 58%);
}

.coin-burst__spark--star::after {
  /* vertical needle */
  clip-path: polygon(50% 0, 58% 50%, 50% 100%, 42% 50%);
}

@keyframes coinSpark {
  0% {
    transform: translate(0, 0) scale(0.3) rotate(0deg);
    opacity: 0;
  }

  18% {
    opacity: 1;
    transform: translate(calc(var(--dx) * 0.25), calc(var(--dy) * 0.25)) scale(1.05) rotate(calc(var(--rot) * 0.25));
  }

  100% {
    transform: translate(var(--dx), calc(var(--dy) + 10px)) scale(0.2) rotate(var(--rot));
    opacity: 0;
  }
}

/* Secret coin pickup — body-level particle burst (see ui-interactions.js
 * secretCoinBump). The burst is a fixed-position layer anchored to the
 * chip's viewport center; its children animate independently so the
 * sparkle isn't constrained by the nested .run-pill > #coinHud chain. */
.secret-burst {
  position: fixed;
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 400;
  /* All children are positioned relative to this 0x0 anchor. */
}

.secret-burst__flash {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: radial-gradient(circle,
      rgba(255, 255, 255, 0.95) 0%,
      rgba(170, 230, 255, 0.85) 30%,
      rgba(74, 184, 255, 0.55) 55%,
      rgba(74, 184, 255, 0) 75%);
  filter: blur(1px);
  animation: secretFlash 0.55s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes secretFlash {
  0% {
    transform: scale(0.3);
    opacity: 0;
  }

  18% {
    transform: scale(1);
    opacity: 1;
  }

  100% {
    transform: scale(6);
    opacity: 0;
  }
}

.secret-burst__ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  border: 2px solid rgba(145, 222, 255, 0.9);
  box-shadow:
    0 0 12px rgba(74, 184, 255, 0.7),
    inset 0 0 8px rgba(145, 222, 255, 0.5);
  animation: secretRing 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes secretRing {
  0% {
    transform: scale(0.4);
    opacity: 0;
    border-width: 3px;
  }

  20% {
    transform: scale(1);
    opacity: 1;
  }

  100% {
    transform: scale(4.5);
    opacity: 0;
    border-width: 1px;
  }
}

.secret-burst__spark {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--sz, 6px);
  height: var(--sz, 6px);
  margin-left: calc(var(--sz, 6px) * -0.5);
  margin-top: calc(var(--sz, 6px) * -0.5);
  background: var(--c, #9fe3ff);
  border-radius: 50%;
  box-shadow: 0 0 8px var(--c, #9fe3ff), 0 0 14px rgba(74, 184, 255, 0.5);
  animation: secretSpark 0.85s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  will-change: transform, opacity;
}

@keyframes secretSpark {
  0% {
    transform: translate(0, 0) scale(0.2) rotate(0deg);
    opacity: 0;
  }

  15% {
    opacity: 1;
    transform: translate(calc(var(--dx) * 0.2), calc(var(--dy) * 0.2)) scale(1.1) rotate(calc(var(--rot) * 0.2));
  }

  100% {
    transform: translate(var(--dx), calc(var(--dy) + 18px)) scale(0.3) rotate(var(--rot));
    opacity: 0;
  }
}

.secret-burst__plus {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-weight: 800;
  font-size: 18px;
  color: #d6f3ff;
  text-shadow:
    0 0 8px rgba(74, 184, 255, 0.95),
    0 0 16px rgba(74, 184, 255, 0.6),
    0 1px 2px rgba(0, 0, 0, 0.5);
  letter-spacing: 0.5px;
  animation: secretPlus 1.1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  will-change: transform, opacity;
}

@keyframes secretPlus {
  0% {
    transform: translate(-50%, -50%) scale(0.4);
    opacity: 0;
  }

  20% {
    transform: translate(-50%, -120%) scale(1.25);
    opacity: 1;
  }

  60% {
    transform: translate(-50%, -260%) scale(1);
    opacity: 1;
  }

  100% {
    transform: translate(-50%, -380%) scale(0.9);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {

  .secret-burst__flash,
  .secret-burst__ring {
    animation-duration: 0.4s;
  }

  .secret-burst__plus {
    animation-duration: 0.6s;
  }
}

.run-pill__secret {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: #4ab8ff;
}

/* Secret chip pop-in — appears from the right with a quick scale+fade. */
.run-pill__secret.pop {
  animation: secretChipPop 0.5s var(--spring);
}

@keyframes secretChipPop {
  0% {
    opacity: 0;
    transform: translateX(10px) scale(0.6);
  }

  55% {
    opacity: 1;
    transform: translateX(0) scale(1.18);
  }

  100% {
    opacity: 1;
    transform: translateX(0) scale(1);
  }
}

.run-pill__secret.pop .run-pill__icon--cyan {
  animation: secretIconSpin 0.6s var(--spring);
}

@keyframes secretIconSpin {
  0% {
    transform: rotate(-180deg) scale(0.6);
    filter: drop-shadow(0 0 0 rgba(74, 184, 255, 0));
  }

  60% {
    transform: rotate(10deg) scale(1.25);
    filter: drop-shadow(0 0 10px rgba(74, 184, 255, 0.8));
  }

  100% {
    transform: rotate(0deg) scale(1);
    filter: drop-shadow(0 0 0 rgba(74, 184, 255, 0));
  }
}

.run-pill__sep {
  width: 1px;
  height: 18px;
  background: rgba(170, 220, 255, 0.3);
}

/* First-person reticle.
 *
 * Default: small white dot (industry standard "neutral" crosshair).
 * Aiming at an interactive target: dot expands into a hollow cyan ring
 *   with a tiny inner dot — the "you can interact" indicator pattern used
 *   in Portal / Half-Life 2 / Apex Legends / Outer Wilds.
 * Click pulse: a quick outward shockwave via box-shadow keyframe.
 *
 * The element is sized 16x16 and centered with translate(-50%,-50%); the
 * visible dot is drawn with `background` so the box can stay a fixed size
 * (prevents the visual jumping around as classes toggle).
 */
#fpCrosshair {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  transform: translate(-50%, -50%);
  z-index: 301;
  pointer-events: none;
  /* Dot is drawn via radial-gradient so the element bounds stay constant. */
  background:
    radial-gradient(circle,
      rgba(255, 255, 255, 0.92) 0,
      rgba(255, 255, 255, 0.92) 2px,
      transparent 2.5px);
  /* Hollow ring border — invisible by default, lights up on .aiming. */
  border: 2.5px solid transparent;
  border-radius: 50%;
  box-sizing: border-box;
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6));
  transition:
    border-color 0.18s ease,
    background 0.18s ease,
    filter 0.18s ease;
}

#fpCrosshair.aiming {
  border-color: #91deff;
  background:
    radial-gradient(circle,
      rgba(145, 222, 255, 1) 0,
      rgba(145, 222, 255, 1) 2px,
      transparent 2.5px);
  filter:
    drop-shadow(0 0 6px rgba(145, 222, 255, 0.8)) drop-shadow(0 0 1px rgba(0, 0, 0, 0.7));
}

/* Click pulse — quick outward shockwave that doesn't fight the hover state. */
#fpCrosshair.click-pulse {
  animation: fpCrosshairClick 220ms ease-out;
}

@keyframes fpCrosshairClick {
  0% {
    box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.55);
  }

  100% {
    box-shadow: 0 0 0 14px rgba(255, 255, 255, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  #fpCrosshair.click-pulse {
    animation: none;
  }
}

/* Crosshair tooltip — frosted glass pill below the crosshair.
 * Shows "Verb · Noun" (e.g. "Open · Bedroom", "Toggle · Lamp").
 */
#fpCrosshairTooltip {
  display: none;
  position: fixed;
  top: calc(50% + 22px);
  left: 50%;
  transform: translateX(-50%) translateY(0);
  z-index: 301;
  pointer-events: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.015em;
  color: rgba(225, 245, 255, 0.92);
  white-space: nowrap;
  background: rgba(8, 16, 28, 0.58);
  border: 1px solid rgba(146, 210, 255, 0.18);
  border-radius: 12px;
  padding: 4px 12px 6px 6px;
  backdrop-filter: blur(10px);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  transition: opacity 0.18s ease, transform 0.18s ease;
  opacity: 0;
}

#fpCrosshairTooltip.no-input {
  padding: 6px 12px;
}

#fpCrosshairTooltip.visible {
  display: block;
  opacity: 1;
}

#fpCrosshairTooltip .tt-verb {
  color: rgba(145, 222, 255, 0.88);
}

#fpCrosshairTooltip .tt-input {
  margin-right: 8px;
  vertical-align: 1px;
}

#fpCrosshairTooltip .tt-sep {
  color: rgba(255, 255, 255, 0.3);
  margin: 0 5px;
}

#fpControls {
  display: none;
  position: fixed;
  bottom: 16px;
  left: 16px;
  background: var(--glass-light);
  color: var(--text-dim);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--r-md);
  padding: 8px 12px;
  font-size: 11px;
  line-height: 1.7;
  backdrop-filter: blur(8px);
  animation: slideUp 0.4s var(--spring) both;
}

/* Share button in HUD */
#fpShareRow {
  position: fixed;
  top: 64px;
  right: 16px;
  margin: 0;
  z-index: 306;
  pointer-events: auto;
}

#fpShareBtn {
  appearance: none;
  border: 1px solid rgba(159, 227, 255, 0.45);
  background: rgba(19, 60, 80, 0.44);
  color: #d9f5ff;
  border-radius: 9px;
  padding: 6px 10px;
  font-size: 11px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.01em;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s;
}

#fpShareBtn:hover {
  background: rgba(30, 80, 110, 0.6);
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

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

#fpControls kbd {
  background: rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 10px;
  font-weight: 600;
  border: 1px solid rgba(255, 255, 255, 0.06);
}

/* Note class used in the controls panel for parenthetical hints. */
.kbd-hint-note {
  opacity: 0.6;
  font-size: 0.85em;
  margin-left: 2px;
}

#fpChargeBar {
  display: none;
  position: fixed;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  width: min(92vw, 420px);
  /* Match the top .run-pill (run timer / coins / mph) glass material
     exactly so the top and bottom HUD chrome read as the same surface. */
  border-radius: 22px;
  padding: 14px 16px 18px;
  background-color: var(--glass);
  background-image:
    linear-gradient(rgba(10, 22, 36, 0.26), rgba(10, 22, 36, 0.26)),
    var(--glass-specular);
  border: 1px solid rgba(170, 220, 255, 0.26);
  box-shadow:
    0 10px 30px rgba(0, 0, 0, 0.28),
    inset 0 1px 0 rgba(255, 255, 255, 0.22);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  pointer-events: none;
  z-index: 320;
}

body.play-mode #fpChargeBar {
  display: block;
}

.fp-charge-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}

.fp-charge-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: rgba(215, 240, 255, 0.9);
  font-weight: var(--weight-medium);
}

.fp-charge-label i {
  color: #9fe3ff;
}

.fp-charge-label kbd {
  background: rgba(255, 255, 255, 0.13);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 9px;
  line-height: 1;
  font-family: var(--font-mono);
  color: rgba(215, 240, 255, 0.98);
}

#fpChargeValue {
  font-family: var(--font-mono);
  font-size: 11px;
  color: rgba(215, 240, 255, 0.95);
  font-variant-numeric: tabular-nums;
}

.fp-charge-track {
  width: 100%;
  height: 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.12);
  overflow: hidden;
  position: relative;
}

/* Threshold markers: 75% = max normal jump, 90% = max MEGA jump.
   Beyond each marker the bar represents progress to the next tier. */
.fp-charge-track::before,
.fp-charge-track::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: rgba(255, 255, 255, 0.55);
  box-shadow: 0 0 4px rgba(255, 255, 255, 0.35);
  pointer-events: none;
  z-index: 2;
}

.fp-charge-track::before {
  left: 75%;
}

.fp-charge-track::after {
  left: 90%;
}

/* When already Super Saiyan, only two phases remain (normal → MEGA);
   collapse the markers to a single tick at the 80% boundary. */
#fpChargeBar.ss-active .fp-charge-track::before {
  left: 80%;
}

#fpChargeBar.ss-active .fp-charge-track::after {
  display: none;
}

#fpChargeFill {
  width: 0%;
  height: 100%;
  background: linear-gradient(90deg, rgba(80, 180, 255, 0.7), rgba(140, 220, 255, 0.95));
  border-radius: 999px;
  transition: width 0.06s linear;
  box-shadow: 0 0 14px rgba(145, 222, 255, 0.45);
}

#fpChargeHint {
  margin-top: 6px;
  font-size: 10px;
  color: rgba(207, 236, 255, 0.68);
}

#fpChargeHint kbd {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 9px;
}

line-height: 1;
}

#fpChargeBar.charging #fpChargeFill {
  background: linear-gradient(90deg, rgba(110, 200, 255, 0.75), rgba(170, 235, 255, 0.98));
}

#fpChargeBar.charged #fpChargeFill {
  background: linear-gradient(90deg, rgba(255, 207, 110, 0.85), rgba(255, 228, 145, 0.98));
  box-shadow: 0 0 16px rgba(255, 216, 115, 0.45);
}

/* ─── Charge tiers (Small / Big / MEGA) ─── */
.fp-charge-ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.fp-charge-track {
  position: relative;
}

.fp-charge-tick {
  position: absolute;
  top: 0;
  width: 2px;
  height: 100%;
  background: rgba(255, 255, 255, 0.28);
  transform: translateX(-1px);
}

/* Tier 2 — bigger jump, cyan/teal pulse + small shake */
#fpChargeBar.tier-2 {
  border-color: rgba(140, 230, 255, 0.55);
  box-shadow: 0 8px 28px rgba(80, 180, 255, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 0 24px rgba(120, 210, 255, 0.35);
  animation: fpChargeShake1 0.18s linear infinite;
}

#fpChargeBar.tier-2 #fpChargeFill {
  background: linear-gradient(90deg, rgba(110, 235, 255, 0.9), rgba(180, 255, 235, 1));
  box-shadow: 0 0 18px rgba(140, 235, 255, 0.6);
}

/* Tier 3 — MEGA jump, gold + violent shake + glow ring */
#fpChargeBar.tier-3 {
  border-color: rgba(255, 220, 130, 0.7);
  box-shadow: 0 10px 36px rgba(255, 170, 60, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 0 38px rgba(255, 200, 110, 0.6);
  animation: fpChargeShake2 0.09s linear infinite;
}

#fpChargeBar.tier-3 #fpChargeFill {
  background: linear-gradient(90deg, rgba(255, 180, 80, 1), rgba(255, 235, 160, 1), rgba(255, 255, 200, 1));
  background-size: 200% 100%;
  animation: fpChargeFlow 0.6s linear infinite;
  box-shadow: 0 0 26px rgba(255, 210, 120, 0.85);
}

#fpChargeBar.tier-3 .fp-charge-label {
  color: #fff3c8;
  text-shadow: 0 0 8px rgba(255, 210, 120, 0.6);
}

#fpChargeBar.tier-3 #fpChargeValue {
  color: #fff8d8;
  text-shadow: 0 0 8px rgba(255, 210, 120, 0.7);
}

/* At-gate pulse — bar pulses when holding at a tier threshold */
#fpChargeBar.at-gate .fp-charge-label {
  animation: fpGatePulse 0.5s ease-in-out infinite;
}

@keyframes fpGatePulse {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.4;
  }
}

/* Super Saiyan active indicator — persistent glow + short entry burst */
#fpChargeBar.ss-active {
  border-color: rgba(255, 220, 130, 0.82);
  box-shadow: 0 10px 34px rgba(255, 160, 52, 0.42),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 0 34px rgba(255, 200, 110, 0.58);
}

#fpChargeBar.ss-active .fp-charge-label {
  color: #fff4ca;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  text-shadow: 0 0 10px rgba(255, 210, 120, 0.62);
}

#fpChargeBar.ss-active .fp-charge-label i {
  color: #ffd66d;
  text-shadow: 0 0 10px rgba(255, 210, 120, 0.62);
}

#fpChargeBar.ss-active #fpChargeValue {
  color: #fff8db;
  text-shadow: 0 0 9px rgba(255, 210, 120, 0.74);
}

#fpChargeBar.ss-active #fpChargeFill {
  background: linear-gradient(90deg, rgba(255, 170, 68, 1), rgba(255, 226, 145, 1), rgba(255, 252, 205, 1));
  background-size: 220% 100%;
  animation: fpChargeFlow 0.5s linear infinite;
  box-shadow: 0 0 28px rgba(255, 210, 120, 0.9);
}

#fpChargeBar.ss-active.ss-enter {
  animation: fpChargeSsEnter 0.55s cubic-bezier(0.2, 0.75, 0.2, 1.25) 1,
    fpChargeSsPulse 1.05s ease-in-out infinite;
}

#fpChargeBar.ss-active:not(.ss-enter) {
  animation: fpChargeSsPulse 1.05s ease-in-out infinite;
}

@keyframes fpChargeShake1 {

  0%,
  100% {
    transform: translate(-50%, 0);
  }

  25% {
    transform: translate(calc(-50% + 1px), -1px);
  }

  50% {
    transform: translate(calc(-50% - 1px), 0);
  }

  75% {
    transform: translate(calc(-50% + 1px), 1px);
  }
}

@keyframes fpChargeShake2 {

  0%,
  100% {
    transform: translate(-50%, 0);
  }

  20% {
    transform: translate(calc(-50% + 2px), -2px);
  }

  40% {
    transform: translate(calc(-50% - 2px), 1px);
  }

  60% {
    transform: translate(calc(-50% + 2px), 2px);
  }

  80% {
    transform: translate(calc(-50% - 2px), -1px);
  }
}

@keyframes fpChargeFlow {
  from {
    background-position: 0% 0%;
  }

  to {
    background-position: 200% 0%;
  }
}

@keyframes fpChargeSsPulse {

  0%,
  100% {
    transform: translate(-50%, 0) scale(1);
  }

  50% {
    transform: translate(-50%, -1px) scale(1.013);
  }
}

@keyframes fpChargeSsEnter {
  0% {
    transform: translate(-50%, 0) scale(0.92);
  }

  38% {
    transform: translate(-50%, -2px) scale(1.045);
  }

  100% {
    transform: translate(-50%, 0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {

  #fpChargeBar.tier-2,
  #fpChargeBar.tier-3,
  #fpChargeBar.at-gate .fp-charge-label,
  #fpChargeBar.ss-active,
  #fpChargeBar.ss-active.ss-enter {
    animation: none;
  }

  #fpChargeBar.tier-3 #fpChargeFill,
  #fpChargeBar.ss-active #fpChargeFill {
    animation: none;
  }
}

/* ─── Timer HUD (legacy positioning — now lives inside .run-hud) ─── */
#runTimerState.running {
  color: #9fe3ff;
}

#runTimerState.finished {
  color: #ffd873;
}

#runTimerState.ready {
  color: rgba(210, 235, 255, 0.7);
}

#fpInputDiag {
  display: block;
  position: relative;
  top: 40px;
  margin-top: 6px;
  line-height: 1.3;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: rgba(210, 235, 255, 0.86);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-12px);
  }

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

/* ═══════════════════════════════════════════════════════════════════
   PAUSE OVERLAY — frosted glass modal
   ═══════════════════════════════════════════════════════════════════ */
#fpPauseOverlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 400;
  /* Mirrors the #bgScrim / #titleSplash recipe used on /home, /about,
     and /leaderboard so the live room reads through the same piece of
     glass during pause. The .pause-card now lives OUTSIDE this overlay
     (in .pause-card-host, sibling), so it's safe to put backdrop-filter
     here — it can't trap a sibling's blur. The card adds its own
     stronger --glass-blur on top, exactly like .title-splash-card on
     /home. */
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(8, 10, 16, 0.55) 0%, rgba(8, 10, 16, 0) 55%),
    radial-gradient(120% 80% at 50% 100%, rgba(8, 10, 16, 0.6) 0%, rgba(8, 10, 16, 0) 55%);
  backdrop-filter: blur(2px) saturate(1.1);
  -webkit-backdrop-filter: blur(2px) saturate(1.1);
  animation: fadeIn 0.3s var(--ease-out-expo) both;
}

/* Centering wrapper for the pause card. Sibling of #fpPauseOverlay so
   the card's heavy --glass-blur samples the live canvas directly
   (see notes on .title-splash-card for the same architecture). The
   wrapper is shown/hidden purely from CSS, driven by the overlay's
   inline display style toggled by setPaused(). */
.pause-card-host {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 401;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

#fpPauseOverlay[style*="display: flex"]+.pause-card-host,
#fpPauseOverlay[style*="display:flex"]+.pause-card-host,
#fpPauseOverlay[style*="display: block"]+.pause-card-host,
#fpPauseOverlay[style*="display:block"]+.pause-card-host {
  display: flex;
}

/* Skateboard onboarding overlay */
#fpSkateOnboarding {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 450;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(14px) saturate(0.9);
  animation: fadeIn 0.3s var(--ease-out-expo) both;
}

.skate-onboard-card {
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-xl);
  padding: 28px 30px 22px;
  max-width: 540px;
  width: 92%;
  color: var(--text);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow:
    0 24px 64px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 0 32px 0 rgba(255, 255, 255, 0.03);
  animation: bounceIn 0.45s var(--spring) both;
}

.skate-onboard-header {
  text-align: center;
  margin-bottom: 18px;
}

.skate-onboard-emoji {
  font-size: 48px;
  line-height: 1;
  margin-bottom: 8px;
}

.skate-onboard-card h2 {
  color: #fff;
  font-size: 24px;
  font-weight: var(--weight-bold);
  margin: 0 0 4px;
  letter-spacing: -0.02em;
}

.skate-onboard-sub {
  margin: 0;
  font-size: 13px;
  opacity: 0.78;
}

.skate-onboard-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  margin-bottom: 18px;
}

.skate-onboard-row {
  display: grid;
  grid-template-columns: 110px 1fr;
  align-items: center;
  gap: 14px;
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--r-md);
}

.skate-onboard-keys {
  display: flex;
  gap: 4px;
  align-items: center;
  flex-wrap: wrap;
}

.skate-onboard-keys kbd {
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 6px;
  padding: 3px 8px;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  min-width: 22px;
  text-align: center;
}

.skate-onboard-desc {
  font-size: 13px;
  line-height: 1.35;
}

.skate-onboard-desc b {
  font-weight: 600;
}

.skate-onboard-desc span {
  opacity: 0.7;
  font-size: 12px;
}

.skate-onboard-footer {
  text-align: center;
}

.skate-onboard-footer .glass-btn {
  padding: 10px 22px;
}

.skate-onboard-footer kbd {
  background: rgba(255, 255, 255, 0.18);
  border-radius: 5px;
  padding: 2px 6px;
  font-size: 11px;
  margin-left: 8px;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

#fpPauseOverlay h2 {
  color: #fff;
  font-size: 28px;
  font-weight: var(--weight-bold);
  margin: 0 0 4px;
  letter-spacing: -0.02em;
}

@keyframes bounceIn {
  from {
    opacity: 0;
    transform: scale(0.8) translateY(20px);
  }

  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

/* Pause overlay close animation — sequential to bounceIn/fadeIn used
   on open. Triggered by JS adding .is-closing on #fpPauseOverlay and
   .pause-card-host immediately before the overlay is hidden. The
   underlying paused state (fpPaused, pointer lock) flips back the
   instant Resume/Esc fires, so this animation never blocks input —
   it's purely a visual exit so the menu doesn't snap away. Kept
   short (~160ms) so the overlay clears before the player's next
   action would feel late. */
#fpPauseOverlay.is-closing {
  animation: pauseScrimOut 160ms var(--ease-in-out) forwards;
}

.pause-card-host.is-closing .pause-card {
  animation: pauseCardOut 160ms var(--ease-in-out) forwards;
}

@keyframes pauseScrimOut {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

@keyframes pauseCardOut {
  from {
    opacity: 1;
    transform: scale(1) translateY(0);
  }

  to {
    opacity: 0;
    transform: scale(0.94) translateY(8px);
  }
}

.pause-card {
  position: relative;
  pointer-events: auto;
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid var(--glass-border);
  border-radius: 32px;
  padding: 0;
  overflow: hidden;
  max-width: 680px;
  width: 92%;
  color: var(--text);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    inset 0 0 32px 0 rgba(255, 255, 255, 0.03),
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 1px 0 rgba(0, 0, 0, 0.5);
  isolation: isolate;
  animation: bounceIn 0.4s var(--spring) both;
  /* NOTE: do NOT add mask-image here. It composites the card into a
     separate layer in Chromium/WebKit and silently kills the
     backdrop-filter sample, leaving the card looking like a flat dark
     panel. border-radius + overflow:hidden is enough to clip children
     to the rounded corners (matches .hero / .card on /home, /about,
     /leaderboard, none of which use mask-image). */
}

/* Top sheen — slim refractive glaze across the very top of the card,
   matches .hero/.card on /home, /about, /leaderboard. */
.pause-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 6%;
  right: 6%;
  height: 1px;
  background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.7) 50%, transparent 100%);
  border-radius: 32px 32px 0 0;
  pointer-events: none;
  z-index: 1;
}

.pause-body {
  padding: 16px 24px 20px;
}

/* ─── Pause card: drill-down nav (Launcher ↔ Settings) ───────────── */
/* The pause card hosts a two-view stack:
     1. Launcher — Resume / Restart / Settings / Exit
     2. Settings — back button + the schema-driven settings panel
   pause-nav.js animates the .pause-card's box (width + height) and
   cross-fades the two views with a directional slide. The card auto-
   sizes to whichever view is active in its rest state — the inline
   width/height set by JS is only present mid-transition. */

.pause-card--nav {
  /* Card shrink-wraps to whichever .pause-view is currently in DOM-
     flow. Per-view widths live on the views themselves (see below)
     so the card's measured size in any state — resting, mid-morph,
     or post-morph — always matches the active view's natural size.
     This is what lets the FLIP morph in pause-nav.js land precisely
     instead of snapping at the end. */
  width: max-content;
  max-width: min(840px, 96vw);
  will-change: width, height;
}

.pause-views {
  position: relative;
  /* During the morph we promote the outgoing view to absolute
     positioning so it overlays the incoming one for the cross-fade
     while the incoming one alone determines the card's natural
     size. In the rest state, only the active view is in DOM-flow
     (the other has hidden=true) so this just behaves like a normal
     block container. */
}

.pause-view {
  width: 100%;
}

/* Per-view widths. Because the card uses width:max-content, whichever
   view is in flow drives the card's box. */
.pause-view--launcher {
  width: min(460px, 92vw);
}

.pause-view--settings {
  width: min(840px, 96vw);
}

/* During a morph, the outgoing view is taken out of flow so the card
   can shrink-wrap to the incoming view's box for measurement. */
.pause-view.is-out-of-flow {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Cross-fade + slide direction states.
   forward = drilling deeper (launcher → settings):
     outgoing slides LEFT, incoming enters from RIGHT.
   back    = drilling out (settings → launcher):
     outgoing slides RIGHT, incoming enters from LEFT. */
.pause-view[data-state="exit-back"] {
  animation: pauseViewExitBack 220ms cubic-bezier(.4, 0, .2, 1) forwards;
}

.pause-view[data-state="exit-forward"] {
  animation: pauseViewExitForward 220ms cubic-bezier(.4, 0, .2, 1) forwards;
}

.pause-view[data-state="enter-from-forward"] {
  /* Pre-position; no animation — JS flips to enter-active on next frame. */
  opacity: 0;
  transform: translateX(28px);
  filter: blur(6px);
}

.pause-view[data-state="enter-from-back"] {
  opacity: 0;
  transform: translateX(-28px);
  filter: blur(6px);
}

.pause-view[data-state="enter-active"] {
  animation: pauseViewEnter 280ms cubic-bezier(.22, 1, .36, 1) 60ms forwards;
}

@keyframes pauseViewExitBack {
  to {
    opacity: 0;
    transform: translateX(-28px);
    filter: blur(6px);
  }
}

@keyframes pauseViewExitForward {
  to {
    opacity: 0;
    transform: translateX(28px);
    filter: blur(6px);
  }
}

@keyframes pauseViewEnter {
  to {
    opacity: 1;
    transform: translateX(0);
    filter: blur(0);
  }
}

/* ─── Launcher view content ──────────────────────────────────────── */

.pause-launcher {
  padding: 26px 26px 22px;
}

.pause-launcher__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
}

.pause-launcher__head h2 {
  margin: 0;
  font-size: 22px;
  letter-spacing: 0.01em;
}

.pause-launcher__btns {
  display: grid;
  /* Hero on row 1 spans full width; Restart / Settings / Exit share
     row 2 as three equal columns. Eliminates the previous 3-size
     mishmash (giant hero / half-width pair / full-width Exit). */
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.pause-launcher__btns .pause-btn--hero {
  grid-column: 1 / -1;
}

/* ─── Settings drill-down view content ───────────────────────────── */

.pause-view--settings {
  padding: 18px 20px 20px;
}

.pause-settings__head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.pause-settings__head h2 {
  margin: 0;
  font-size: 18px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
  color: rgba(225, 240, 255, 0.98);
}

/* Top-left circular icon button, glass-tinted to match the rest of
   the UI. Tactile press + focus ring. */
.pause-back {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--glass-border);
  color: rgba(225, 240, 255, 0.95);
  cursor: pointer;
  font-family: inherit;
  flex-shrink: 0;
  transition: background var(--dur-fast) var(--ease-out-expo),
    border-color var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-out-expo);
}

.pause-back:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: var(--glass-border-strong);
}

.pause-back:active {
  transform: scale(0.94);
}

.pause-back:focus-visible {
  outline: 2px solid rgba(159, 227, 255, 0.55);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {

  .pause-card--nav,
  .pause-view {
    transition: none !important;
    animation: none !important;
  }

  .pause-view[data-state="enter-from-forward"],
  .pause-view[data-state="enter-from-back"],
  .pause-view[data-state="enter-active"] {
    opacity: 1;
    transform: none;
    filter: none;
  }
}

/* ─── Action zone (top band with the 3 big buttons) ─── */
.pause-action-zone {
  padding: 22px 24px 20px;
  background: linear-gradient(180deg, rgba(159, 227, 255, 0.05) 0%, transparent 100%);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.pause-action-header {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 16px;
}

.pause-action-header h2 {
  margin: 0;
}

.pause-action-btns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.pause-btn--hero {
  grid-column: 1 / -1;
  padding: 18px 20px;
  font-size: 16px;
}

.pause-btn--secondary {
  padding: 13px 16px;
  font-size: 13px;
}

/* Small inline text link tucked next to the "Paused" heading. Used for
   secondary entry points (e.g. Inspect air purifier) that we want
   reachable but visually de-prioritized. */
.pause-action-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.pause-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: rgba(220, 240, 255, 0.7);
  font-size: 11px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  text-decoration: underline;
  text-decoration-color: rgba(220, 240, 255, 0.35);
  text-underline-offset: 2px;
}

.pause-link:hover {
  color: rgba(220, 240, 255, 0.95);
  text-decoration-color: rgba(220, 240, 255, 0.7);
}

.pause-link--footer {
  margin-top: 10px;
  font-size: 11px;
  color: rgba(220, 240, 255, 0.65);
}

.pause-sub {
  color: var(--text-dim);
  font-size: 11px;
  margin: 0;
}

/* ─── Grouped sections ─── */
.pause-section {
  margin-top: 14px;
  text-align: left;
}

.pause-section-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: var(--weight-bold);
  letter-spacing: var(--type-tracking-wide);
  text-transform: uppercase;
  /* Bumped from 0.55 → 0.82 so the eyebrow label reads cleanly against
     the live scene through the glass card (mirrors the contrast bump
     applied to #fpChargeBar's labels). */
  color: rgba(220, 240, 255, 0.82);
  margin: 0 0 8px;
}

.pause-section-title i {
  font-size: 13px;
  color: rgba(159, 227, 255, 0.95);
}

.pause-section-title .pause-section-emoji {
  font-size: 15px;
  line-height: 1;
}

/* Sub-section header inside the keyboard reference (e.g. Skate group
   under the main Keyboard header). Adds breathing room above so the
   group reads as distinct without introducing a second card. */
.pause-section-title--sub {
  margin-top: 20px;
}

.pause-section--kbd {
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  padding-top: 12px;
}

/* ── Pause-card action buttons ────────────────────────────────────
   Just sizing/layout here — the brighter tile surface (background,
   border, backdrop-filter override, raised-tile shadow) lives in
   .glass-btn--tile, which the markup composes alongside .glass-btn
   and any color variant (--primary / --danger). See the
   .glass-btn--tile rules in the GLASS BUTTON SYSTEM block. */
.pause-btn {
  padding: 18px 20px;
  font-size: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  white-space: nowrap;
  min-width: 0;
}

/* Hero (Resume): modest size lift + bold weight so the primary
   action reads first without dwarfing the secondaries. The colored
   gradient from .glass-btn--primary does the rest of the hierarchy
   work. */
.pause-btn--hero {
  padding: 22px 22px;
  font-size: 16px;
  font-weight: var(--weight-bold);
}

/* (No .pause-btn.glass-btn--danger override needed — the shared
   .glass-btn--tile.glass-btn--danger rule handles re-pinning the
   tile background under the red gradient.) */

.pause-btn-key {
  background: rgba(255, 255, 255, 0.2);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 3px;
  padding: 1px 5px;
  font-size: 9px;
  font-weight: var(--weight-semibold);
  opacity: 1;
  margin-left: 2px;
}

@media (max-width: 520px) {
  .pause-launcher__btns {
    grid-template-columns: 1fr;
  }

  .pause-action-btns {
    grid-template-columns: 1fr;
  }

  .pause-btn--hero {
    grid-column: 1;
  }
}

/* ─── Toggle rows (audio + display) ─── */
.pause-toggles {
  display: grid;
  /* Bumped min from 170→220px after the label bump to 13px. At the
     pause card's max-width (680px), the prior 170px min squeezed labels
     into 3 narrow columns where 13px copy wrapped to 2–3 lines
     ("Outside window sunlight", "Cat animation stack"). 220px min
     gives the auto-fit layout enough headroom to land on 2 wider
     columns at typical card widths, so most labels fit on a single
     line; 3-col only kicks in on ultra-wide viewports. */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 8px;
}

.pause-toggle-row {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Light, mostly transparent surface — same recipe as the unselected
     .char-card tiles on the character picker: a whisper of white tint
     and a slim inset top highlight so each row reads as its own glass
     pane on the card without flattening into a dark slab. */
  background-color: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-sm);
  padding: 8px 12px;
  font-size: 13px;
  color: rgba(225, 240, 255, 0.95);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  min-width: 0;
  transition: background-color var(--dur-fast) var(--ease-out-expo),
    border-color var(--dur-fast) var(--ease-out-expo);
}

.pause-toggle-row:hover {
  background-color: rgba(255, 255, 255, 0.07);
  border-color: var(--glass-border-strong);
}

/* ── Settings panel: localhost-only rows ──────────────────────────
   `.pause-toggle-row--localhost` is hidden unless the host also has
   `.settings-panel--localhost` (set by mountSettings when
   location.hostname is localhost / 127.0.0.1). Keeps Wall labels off
   production builds entirely. */
.settings-panel:not(.settings-panel--localhost) .pause-toggle-row--localhost {
  display: none;
}

.pause-toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  /* Tighten leading so the occasional 2-line wrap (e.g. "Cat animation
     stack") reads as a deliberate two-liner instead of a tall hollow
     row. Default 1.45 was too airy for a settings grid. */
  line-height: 1.25;
  color: rgba(225, 240, 255, 0.95);
}

.pause-toggle-label i {
  color: rgba(159, 227, 255, 0.95);
}

.pause-toggle-row .toggle-sw {
  margin-left: auto;
}

.pause-toggle-row .pause-inline-btn {
  margin-left: auto;
}

.pause-toggle-state {
  /* Sit flush against the toggle/slider — no padded text box. */
  text-align: right;
  font-size: 12px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.02em;
  color: rgba(225, 240, 255, 0.98);
}

.pause-toggle-state.off {
  color: rgba(255, 195, 195, 0.95);
}

/* Slider row — 2-row grid so the label and current value sit on the
   same top line (label left, value right), and the track spans the
   full width on the row below. Previously this was a flex-wrap row
   with the slider forcing `flex: 1 1 100%`; because the source order
   is label → slider → value, the value got pushed onto a *third*
   line under the track, leaving a tall hollow row. Grid placement
   lets us keep markup order and still pin the value to the header. */
.pause-toggle-row--slider {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  column-gap: 12px;
  /* Generous gap between the header line and the track so the thumb
     never crowds the label text above it. */
  row-gap: 14px;
  /* Symmetric horizontal padding keeps the track edges aligned with
     the label/value columns above; bottom padding gives the thumb
     (which extends ~5px below the track centerline) breathing room. */
  padding: 10px 16px 14px;
}

.pause-toggle-row--slider .pause-toggle-label {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
  /* Allow long slider labels (e.g. "FP shadow cadence") to wrap to a
     second line rather than ellipsizing. After the row label bump to
     13px and the grid widen to minmax(220px), most labels fit on one
     line; the rare 2-liner reads cleanly thanks to the 1.25 leading
     on .pause-toggle-label. The previous nowrap+ellipsis approach
     truncated "Shadow cadence" mid-word, which is worse than wrap. */
  white-space: normal;
  overflow-wrap: anywhere;
}

.pause-toggle-row--slider .pause-toggle-state {
  grid-column: 2;
  grid-row: 1;
  white-space: nowrap;
  /* Numeric values look best tabular so changes don't shift width. */
  font-variant-numeric: tabular-nums;
}

.pause-toggle-row--slider .pause-range {
  grid-column: 1 / -1;
  grid-row: 2;
  /* Track spans the full grid cell so its left/right edges line up
     exactly with the label and value above. The thumb's overhang at
     min/max is absorbed by the row's 16px horizontal padding. */
}

.pause-range {
  width: 100%;
  min-width: 0;
  height: 4px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  outline: none;
  cursor: pointer;
  accent-color: #5cd8ff;
}

.pause-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #9fe3ff;
  border: 1px solid rgba(9, 13, 20, 0.6);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  transition: transform 160ms var(--spring), box-shadow 160ms ease,
    background 160ms ease, border-color 160ms ease;
}

.pause-range::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #9fe3ff;
  border: 1px solid rgba(9, 13, 20, 0.6);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  transition: transform 160ms var(--spring), box-shadow 160ms ease,
    background 160ms ease, border-color 160ms ease;
}

.pause-range:focus-visible {
  /* Soft halo around the thin track. The global rule paints the
     outline ring outside this; the thumb pseudo-element has its own
     glow at .pause-range:focus-visible::-{webkit,moz}-slider-thumb. */
  box-shadow: 0 0 0 2px rgba(92, 216, 255, 0.32);
}

/* Inline button (e.g. Camera cycle) inside a toggle row */
.pause-inline-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(170, 220, 255, 0.32);
  border-radius: var(--r-sm);
  padding: 4px 10px;
  font-size: 11px;
  font-weight: var(--weight-medium);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  transition: all var(--dur-med) var(--spring);
}

.pause-inline-btn:hover {
  background: rgba(255, 255, 255, 0.2);
  border-color: rgba(170, 220, 255, 0.5);
}

.pause-inline-btn kbd {
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 3px;
  padding: 0 4px;
  font-size: 9px;
  font-weight: var(--weight-semibold);
  opacity: 1;
  color: rgba(225, 240, 255, 0.98);
}

/* ─── Keyboard reference grid ─── */
.pause-controls {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* row-gap × column-gap. Vertical was 4px which crowded the kbd
     glyphs against neighbouring rows; 8px gives the keys room to
     breathe without making the section feel sparse. */
  gap: 8px 16px;
  font-size: 12px;
  /* Bumped from 0.45 → 0.82 — the keyboard reference was nearly
     invisible against the live scene. Same contrast push as
     #fpChargeBar's hint labels. */
  color: rgba(225, 240, 255, 0.82);
  text-align: left;
}

.pause-controls kbd {
  background: rgba(255, 255, 255, 0.14);
  border-radius: 3px;
  padding: 1px 5px;
  font-size: 11px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  color: rgba(225, 240, 255, 0.98);
  font-weight: var(--weight-semibold);
  /* Single-letter keys (W, R, V, K, E, Q, F …) render at slightly
     different widths because the body font is proportional. A
     min-width with center-aligned text gives every 1-char kbd the
     same visual footprint, so the W A S D run reads as a row of
     equal tiles instead of an irregular ladder. Multi-char keys
     (Shift, Space, Esc) auto-grow past the min-width. */
  min-width: 18px;
  text-align: center;
  box-sizing: border-box;
}

/* Adjacent <kbd> siblings (W A S D) render with no whitespace
   between them, so they touched edge-to-edge. A small left margin
   on every kbd-after-kbd separates the glyphs without affecting the
   kbd→label spacing (handled by the literal space in row markup). */
.pause-controls kbd+kbd {
  margin-left: 3px;
}

/* ─── Settings panel (shared between pause + home) ──────────────── */
/* Schema-rendered tabbed settings UI with always-on search and a
   vertical animated rail. Lives inside .pause-body in the pause card
   and (later) inside the home page. The row markup reuses the
   existing .pause-toggle-row classes so look & feel is consistent. */

.settings-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 0;
}

/* ─── Reusable search field (.app-search) ─────────────────────────
   Pill-shaped search input with a leading icon. The wrapper IS the
   focus surface: the <input> inside is transparent and never shows
   its own focus ring, so the visible focus state always follows the
   wrapper's rounded shape. Drop in anywhere a filter/search field
   is needed.

   Markup:
     <div class="app-search">
       <i class="ph ph-magnifying-glass" aria-hidden="true"></i>
       <input class="app-search__input" type="search" aria-label="…">
     </div>

   Variants: add .app-search--sticky to pin to the top of a scroll
   container. The settings panel keeps .settings-search-wrap /
   .settings-search-input as JS hooks alongside the new classes. */
.app-search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  /* Match the .pause-toggle-row recipe so the search field reads as
     part of the same glass family as the rows beneath it: a whisper
     of white tint, the shared --glass-border, and a slim inset top
     highlight. No backdrop-filter here — the parent glass card is
     already sampling the scene; stacking another blur creates a
     darker slab that fights the rest of the panel. */
  background-color: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-sm);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  transition: background-color var(--dur-fast) var(--ease-out-expo),
    border-color var(--dur-fast) var(--ease-out-expo),
    box-shadow var(--dur-fast) var(--ease-out-expo);
}

.app-search:hover {
  background-color: rgba(255, 255, 255, 0.07);
  border-color: var(--glass-border-strong);
}

.app-search--sticky {
  position: sticky;
  top: 0;
  z-index: 2;
}

.app-search>i {
  /* WCAG: this icon is decorative, but bump alpha so it still reads
     against light scene backdrops behind the glass card. */
  color: rgba(159, 227, 255, 0.95);
  font-size: 15px;
  flex-shrink: 0;
}

.app-search__input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  /* The wrapper renders the focus state. Suppress the input's own
     outline in every state so the global :focus-visible rule (which
     would otherwise paint a sharp rectangle inside the rounded pill)
     never wins here. */
  outline: none;
  box-shadow: none;
  color: rgba(225, 240, 255, 0.98);
  font-family: inherit;
  font-size: 13px;
  padding: 2px 0;
  caret-color: var(--accent);
}

.app-search__input:focus,
.app-search__input:focus-visible {
  outline: none;
  box-shadow: none;
}

/* Hide the WebKit search clear button so the field stays consistent
   across browsers; Esc clears via our keydown handler. */
.app-search__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

/* Placeholder needs to clear WCAG AA (4.5:1) against the glass card
   even when the scene behind happens to be bright. 0.5 alpha drops
   to ~2.4:1 on light backdrops; 0.72 holds the line at ~4.5:1 while
   still reading as muted next to live input text. */
.app-search__input::placeholder {
  color: rgba(225, 240, 255, 0.72);
}

/* Soft glow whenever the field is focused (mouse or keyboard) so
   users always get feedback. */
.app-search:focus-within {
  border-color: rgba(159, 227, 255, 0.45);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 0 0 4px rgba(159, 227, 255, 0.10);
}

/* Stronger, accessible ring for keyboard-only focus — matches the
   rest of the :focus-visible system. The shadow is rounded by the
   wrapper's border-radius, so it follows the pill shape. */
.app-search:has(:focus-visible) {
  border-color: rgba(159, 227, 255, 0.7);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 0 0 3px rgba(159, 227, 255, 0.32);
}

/* Body: rail (left) + pages (right). */
.settings-body {
  display: grid;
  grid-template-columns: 144px 1fr;
  gap: 12px;
  min-height: 0;
  align-items: start;
}

/* Vertical tab rail. */
.settings-rail {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-sm);
}

.settings-tab-indicator {
  position: absolute;
  left: 4px;
  right: 4px;
  top: 0;
  height: 0;
  pointer-events: none;
  background: rgba(159, 227, 255, 0.16);
  border: 1px solid rgba(159, 227, 255, 0.4);
  border-radius: var(--r-sm);
  /* No inset top highlight: it skewed the pill optically toward the
     top edge and made the active tab look mis-centered against the
     label below. The 1px border alone provides enough definition. */
  z-index: 0;
  /* transform/height animations are driven inline by the panel JS. */
}

.settings-tab {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Fixed whole-pixel height keeps the rail consistent with other
     32–36px controls and prevents the 36.59px fractional box that
     the inherited 1.55 line-height + 9px vertical padding produced.
     line-height: 1 collapses the font's half-leading so flex
     align-items: center actually centers the glyphs, not a
     leading-padded line box. */
  height: 36px;
  padding: 0 12px;
  background: transparent;
  border: none;
  border-radius: var(--r-sm);
  /* Inactive tab labels: alpha 0.78 was borderline 3.3:1 against
     bright scene backdrops. 0.86 keeps the muted-vs-active hierarchy
     while clearing WCAG AA (~4.5:1) on the worst-case glass surface. */
  color: rgba(225, 240, 255, 0.86);
  font-family: inherit;
  /* Rail tabs are navigation — they should read as the loudest text on
     the rail. After the option-label bump to 13px, 12px tabs felt
     drowned out by the dense toggle grid. Bump to 14px semibold so
     the rail's hierarchy survives next to a busy options column. */
  font-size: 14px;
  line-height: 1;
  font-weight: var(--weight-semibold);
  text-align: left;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out-expo),
    background-color var(--dur-fast) var(--ease-out-expo);
}

.settings-tab i {
  font-size: 15px;
  color: rgba(159, 227, 255, 0.95);
}

/* Nudge the label text down 3px so it sits visually centered against
   the icon — Phosphor glyphs render slightly above their baseline at
   this size, leaving the label looking top-heavy without the offset. */
.settings-tab>span {
  padding-top: 3px;
}

/* Each tab renders both icon variants (regular + fill); CSS picks the
   one that matches the active state. Stacking both glyphs in the same
   inline slot avoids the tiny optical jiggle Phosphor's fill weight
   has against its regular weight on certain icons. Same pattern as
   .site-tab on the top-level pages. */
.settings-tab .settings-tab__icon--fill {
  display: none;
}

.settings-tab.is-active .settings-tab__icon--regular {
  display: none;
}

.settings-tab.is-active .settings-tab__icon--fill {
  display: inline-block;
}

.settings-tab:hover {
  color: rgba(225, 240, 255, 0.98);
  background: rgba(255, 255, 255, 0.04);
}

.settings-tab.is-active {
  color: rgba(225, 240, 255, 1);
}

.settings-tab:focus-visible {
  outline: 2px solid rgba(159, 227, 255, 0.55);
  outline-offset: 1px;
}

/* Pages container — right side. Pages stack and are toggled visible.
   Fixed-height window with internal scroll: switching tabs (Display
   ↔ Audio ↔ Controls) keeps the surrounding card a stable size, so
   the user never sees the dialog jump. The clamp scales the window
   sensibly across viewport heights. Anything taller than the window
   scrolls inside this box rather than resizing the parent. */
.settings-pages {
  /* Single-cell grid: every .settings-page lives in row 1, column 1
     so the outgoing + incoming pages overlap during a tab swap
     instead of stacking vertically (which read as a vertical jump
     rather than a cross-fade). The grid row sizes to the tallest
     visible child; with [hidden] applied to inactive pages only the
     active page contributes to the height at rest. */
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto;
  position: relative;
  height: clamp(340px, 52vh, 520px);
  overflow-y: auto;
  /* Tiny inset so focus rings on the inner controls aren't clipped
     by the scroll container's edges. */
  padding: 2px 4px;
  /* Match the dark UI scrollbar style used elsewhere in the panel. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.settings-pages::-webkit-scrollbar {
  width: 8px;
}

.settings-pages::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.18);
  border-radius: 999px;
}

.settings-page {
  /* All pages share the same grid cell so they overlap during the
     transition. The active page's [hidden]-cleared sibling sits on
     top during the swap; once the outgoing page transitions out it
     gets [hidden] again and stops painting. */
  grid-column: 1;
  grid-row: 1;
  /* Direction-aware enter/exit animation. The panel JS sets data-state
     to one of: active, inactive, enter-from-below, enter-from-above,
     exit-up, exit-down. Incoming uses an ease-out curve so it settles
     in smoothly; outgoing uses an ease-in curve (overridden below) so
     it accelerates away — the asymmetric pacing reads as "this page
     is leaving" / "this one is arriving" rather than a flat crossfade. */
  transition:
    transform 380ms cubic-bezier(.22, 1, .36, 1),
    opacity 260ms ease-out,
    filter 260ms ease-out;
  will-change: transform, opacity, filter;
}

.settings-page[data-state="active"] {
  transform: translateY(0) scale(1);
  opacity: 1;
  filter: none;
  /* Active page sits above any still-fading-out sibling so the swap
     reads as new content arriving over the old, not a crossfade. */
  z-index: 1;
}

.settings-page[data-state="inactive"] {
  /* hidden via [hidden] attribute; this rule just ensures no leftover transform. */
  transform: translateY(0) scale(1);
  opacity: 1;
  filter: none;
}

.settings-page[data-state="enter-from-below"],
.settings-page[data-state="enter-from-above"] {
  /* Incoming page is already on top before its transition kicks in,
     so the user only ever sees the outgoing page through a clean
     fade rather than a paint-order flicker. */
  z-index: 1;
}

.settings-page[data-state="exit-up"],
.settings-page[data-state="exit-down"] {
  z-index: 0;
  /* Outgoing accelerates away (ease-in) and clears faster than the
     incoming page settles, so the user reads it as "leaving" rather
     than dissolving in place. */
  transition:
    transform 220ms cubic-bezier(.4, 0, 1, 1),
    opacity 180ms ease-in,
    filter 180ms ease-in;
}

.settings-page[data-state="enter-from-below"] {
  transform: translateY(32px) scale(.96);
  opacity: 0;
  filter: blur(8px);
}

.settings-page[data-state="enter-from-above"] {
  transform: translateY(-32px) scale(.96);
  opacity: 0;
  filter: blur(8px);
}

.settings-page[data-state="exit-up"] {
  transform: translateY(-24px) scale(.98);
  opacity: 0;
  filter: blur(6px);
}

.settings-page[data-state="exit-down"] {
  transform: translateY(24px) scale(.98);
  opacity: 0;
  filter: blur(6px);
}

/* Row-level stagger on the incoming page: rows start slightly offset
   and invisible, then cascade into place once the page becomes active.
   Transitions don't fire on initial mount (rows already match the
   active defaults), so this only runs on tab swaps. */
.settings-page .pause-toggle-row,
.settings-page .settings-keyboard-ref {
  transition:
    opacity 300ms ease-out,
    transform 360ms cubic-bezier(.22, 1, .36, 1);
}

.settings-page[data-state="enter-from-below"] .pause-toggle-row,
.settings-page[data-state="enter-from-below"] .settings-keyboard-ref {
  opacity: 0;
  transform: translateY(14px);
}

.settings-page[data-state="enter-from-above"] .pause-toggle-row,
.settings-page[data-state="enter-from-above"] .settings-keyboard-ref {
  opacity: 0;
  transform: translateY(-14px);
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(1) {
  transition-delay: 70ms;
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(2) {
  transition-delay: 95ms;
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(3) {
  transition-delay: 120ms;
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(4) {
  transition-delay: 145ms;
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(5) {
  transition-delay: 170ms;
}

.settings-page[data-state="active"] .pause-toggle-row:nth-child(n+6) {
  transition-delay: 195ms;
}

.settings-page[data-state="active"] .settings-keyboard-ref {
  transition-delay: 215ms;
}

/* Keyboard reference block at the bottom of the Controls tab. */
.settings-keyboard-ref {
  margin-top: 14px;
  /* 20px above the Keyboard header matches the breathing room added
     above the Skateboard sub-header (.pause-section-title--sub), so
     both groups read with the same vertical rhythm. */
  padding-top: 20px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  text-align: left;
}

/* ── Controller rebind UI ────────────────────────────────────────
   Lives inside .settings-keyboard-ref. Each row is action label +
   click-to-listen button showing the current button name. */
.gp-rebind-fixed {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 4px;
}
/* Static reference row: label on the left, glyph chip on the right.
   Keep this read-only block visually quieter than remappable rows so
   L/R sticks + D-pad don't look clickable. */
.gp-rebind-row--static {
  opacity: 0.86;
}
.gp-rebind-row--static .gp-rebind-label {
  color: rgba(225, 240, 255, 0.72);
}
.gp-rebind-row--static .gp-rebind-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  min-width: 0;
  min-height: 0;
  color: rgba(225, 240, 255, 0.8);
}
.gp-rebind-row--static .gp-rebind-glyph kbd {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 11px;
  letter-spacing: 0.01em;
  color: rgba(225, 240, 255, 0.9);
}
.gp-rebind-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 4px;
}
.gp-rebind-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 30px;
}
.gp-rebind-label {
  font-size: 13px;
  color: rgba(225, 240, 255, 0.85);
}
.gp-rebind-btn {
  appearance: none;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(170, 220, 255, 0.32);
  border-radius: 8px;
  padding: 4px 10px;
  cursor: pointer;
  color: rgba(225, 240, 255, 0.96);
  font-size: 11px;
  font-weight: var(--weight-medium);
  min-width: 88px;
  text-align: center;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
}
.gp-rebind-btn:hover {
  background: rgba(255, 255, 255, 0.2);
  border-color: rgba(170, 220, 255, 0.5);
}
.gp-rebind-btn:focus-visible {
  outline: 2px solid #5cd8ff;
  outline-offset: 2px;
}
.gp-rebind-btn.is-listening {
  background: rgba(92, 216, 255, 0.22);
  border-color: rgba(140, 200, 255, 0.7);
  animation: gp-rebind-pulse 1.1s ease-in-out infinite;
}
.gp-rebind-btn kbd {
  background: transparent;
  border: 0;
  padding: 0;
  font-size: 12px;
  letter-spacing: 0.02em;
}

kbd.input-glyph {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  min-width: 22px !important;
  height: 20px !important;
  padding: 0 6px !important;
  border-radius: 6px !important;
  border: 1px solid rgba(219, 232, 241, 0.28) !important;
  background: rgba(214, 228, 239, 0.12) !important;
  color: rgba(244, 249, 253, 0.98) !important;
  box-sizing: border-box !important;
  font-size: 10px !important;
  font-weight: 700 !important;
  line-height: 1 !important;
  letter-spacing: 0.04em !important;
  text-transform: uppercase !important;
  text-align: center !important;
  font-family: var(--font-mono) !important;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08), 0 1px 2px rgba(0, 0, 0, 0.16) !important;
}

kbd.input-glyph[data-input-kind="kb"] {
  min-width: 24px !important;
}

kbd.input-glyph[data-input-kind="gp"] {
  font-family: inherit !important;
  letter-spacing: 0.02em !important;
}

kbd.input-glyph[data-input-kind="gp"][data-gp-role^="face"] {
  width: 20px !important;
  min-width: 20px !important;
  padding: 0 !important;
  border-radius: 999px !important;
  font-size: 11px !important;
  font-weight: 800 !important;
}

kbd.input-glyph[data-pad-type="xbox"][data-gp-role="face-south"] {
  border-color: rgba(115, 232, 156, 0.65) !important;
  background: rgba(39, 125, 75, 0.92) !important;
  color: #f3fff8 !important;
}

kbd.input-glyph[data-pad-type="xbox"][data-gp-role="face-east"] {
  border-color: rgba(255, 132, 132, 0.65) !important;
  background: rgba(152, 45, 45, 0.92) !important;
  color: #fff4f4 !important;
}

kbd.input-glyph[data-pad-type="xbox"][data-gp-role="face-west"] {
  border-color: rgba(127, 197, 255, 0.68) !important;
  background: rgba(40, 96, 155, 0.92) !important;
  color: #f3f9ff !important;
}

kbd.input-glyph[data-pad-type="xbox"][data-gp-role="face-north"] {
  border-color: rgba(255, 223, 122, 0.72) !important;
  background: rgba(156, 118, 22, 0.92) !important;
  color: #fffced !important;
}

kbd.input-glyph[data-pad-type="playstation"][data-gp-role^="face"] {
  background: rgba(19, 28, 43, 0.82) !important;
}

kbd.input-glyph[data-pad-type="playstation"][data-gp-role="face-south"] {
  border-color: rgba(123, 188, 255, 0.78) !important;
  color: rgba(210, 233, 255, 0.98) !important;
}

kbd.input-glyph[data-pad-type="playstation"][data-gp-role="face-east"] {
  border-color: rgba(255, 132, 132, 0.78) !important;
  color: rgba(255, 220, 220, 0.98) !important;
}

kbd.input-glyph[data-pad-type="playstation"][data-gp-role="face-west"] {
  border-color: rgba(255, 168, 224, 0.8) !important;
  color: rgba(255, 230, 247, 0.98) !important;
}

kbd.input-glyph[data-pad-type="playstation"][data-gp-role="face-north"] {
  border-color: rgba(137, 255, 166, 0.78) !important;
  color: rgba(220, 255, 228, 0.98) !important;
}

kbd.input-glyph[data-pad-type="nintendo"][data-gp-role^="face"] {
  background: rgba(179, 49, 49, 0.94) !important;
  border-color: rgba(255, 193, 193, 0.52) !important;
  color: #fff8f8 !important;
}

kbd.input-glyph[data-input-kind="gp"][data-gp-role^="shoulder"],
kbd.input-glyph[data-input-kind="gp"][data-gp-role^="trigger"],
kbd.input-glyph[data-input-kind="gp"][data-gp-role^="system"],
kbd.input-glyph[data-input-kind="gp"][data-gp-role^="dpad"],
kbd.input-glyph[data-input-kind="gp"][data-gp-role="misc"] {
  min-width: 28px !important;
  padding: 0 7px !important;
  background: rgba(18, 29, 44, 0.78) !important;
  border-color: rgba(162, 205, 235, 0.34) !important;
  color: rgba(237, 245, 251, 0.98) !important;
  text-transform: none !important;
}

kbd.input-glyph[data-input-kind="gp"][data-gp-role^="stick"],
kbd.input-glyph[data-input-kind="gp"][data-gp-role="dpad-cluster"] {
  min-width: 42px !important;
  padding: 0 8px !important;
  background: rgba(18, 29, 44, 0.78) !important;
  border-color: rgba(162, 205, 235, 0.34) !important;
  color: rgba(237, 245, 251, 0.98) !important;
  text-transform: none !important;
}

.pause-btn-key.input-glyph {
  margin-left: 6px;
}

.gp-rebind-btn .input-glyph,
.gp-rebind-glyph .input-glyph {
  margin: 0;
}

#fpCrosshairTooltip .input-glyph {
  transform: translateY(1px);
}
@keyframes gp-rebind-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(140, 200, 255, 0.0); }
  50%      { box-shadow: 0 0 0 4px rgba(140, 200, 255, 0.15); }
}
.gp-rebind-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 12px 0 4px;
}
.gp-rebind-reset {
  appearance: none;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  padding: 4px 10px;
  font-size: 12px;
  color: rgba(225, 240, 255, 0.7);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.gp-rebind-reset:hover {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.22);
  color: rgba(225, 240, 255, 0.95);
}
.gp-rebind-hint {
  font-size: 11px;
  color: rgba(225, 240, 255, 0.55);
  font-style: italic;
}
@media (prefers-reduced-motion: reduce) {
  .gp-rebind-btn.is-listening { animation: none; }
}

/* Search results — flat list grouped by tab. */
.settings-search-results {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.settings-search-group .pause-section-title {
  margin-bottom: 6px;
}

.settings-search-empty {
  padding: 24px 8px;
  text-align: center;
  font-size: 12px;
  color: rgba(225, 240, 255, 0.6);
}

/* Hide the rail when the search-results pane takes over. */
.settings-pages.is-searching~.settings-rail,
.settings-body:has(.settings-pages.is-searching) .settings-rail {
  opacity: 0.45;
  pointer-events: none;
}

/* Reduced motion: kill page slides + indicator slides. Fade only. */
@media (prefers-reduced-motion: reduce) {
  .settings-page {
    transition: opacity 120ms ease;
    transform: none !important;
    filter: none !important;
  }

  .settings-page .pause-toggle-row,
  .settings-page .settings-keyboard-ref {
    transition: opacity 120ms ease !important;
    transform: none !important;
    transition-delay: 0ms !important;
  }

  .settings-tab-indicator {
    transition: opacity 120ms ease !important;
  }
}

/* Mobile: collapse rail to a horizontal strip. */
@media (max-width: 600px) {
  .settings-body {
    grid-template-columns: 1fr;
  }

  .settings-rail {
    flex-direction: row;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .settings-tab-indicator {
    /* Horizontal mode: the inline-driven transform still works since
       it uses translateY; in horizontal layout we hide the indicator
       to avoid jumpy rendering. The active-tab class still highlights. */
    display: none;
  }

  .settings-tab.is-active {
    background: rgba(159, 227, 255, 0.16);
    border: 1px solid rgba(159, 227, 255, 0.4);
  }
}

/* ─── Leaderboard mode tabs ──────────────────────────────────────── */
/* (.lb-tabs / .lb-tab + #fpLeaderboardList rules removed when the
   leaderboard panel was taken out of the pause menu. The shared
   leaderboard.html page defines its own equivalents.) */

.catBadge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.02em;
  padding: 1px 7px 1px 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: #cdd7e5;
  white-space: nowrap;
}

.catBadge .catEmoji {
  font-size: 11px;
  line-height: 1;
}

.catBadge .catDot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.45);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
}

/* Secret-coin badge — mirrors the inline rule in leaderboard.html so
   the dialog rows surface the same coin info as the standalone
   leaderboard. is-zero dims out, is-max gets a brighter gold tint. */
.secretBadge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.02em;
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(255, 216, 115, 0.10);
  border: 1px solid rgba(255, 216, 115, 0.32);
  color: #ffd700;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.secretBadge i {
  font-size: 11px;
  line-height: 1;
}

.secretBadge.is-zero {
  background: rgba(255, 255, 255, 0.03);
  border-color: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.55);
  opacity: 0.6;
}

.secretBadge.is-max {
  background: rgba(255, 216, 115, 0.18);
  border-color: rgba(255, 216, 115, 0.55);
  box-shadow: 0 0 0 1px rgba(255, 216, 115, 0.18) inset;
}

/* ─── Name Dialog ────────────────────────────────────────────────── */
#nameDialogOverlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 12600;
  background: rgba(3, 7, 12, 0.62);
  backdrop-filter: blur(7px);
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
}

.name-dialog-card {
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid rgba(128, 190, 232, 0.25);
  border-radius: var(--r-xl);
  padding: 24px 28px;
  max-width: 380px;
  width: 90%;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    inset 0 0 20px rgba(255, 255, 255, 0.03);
  animation: bounceIn 0.4s var(--spring) both;
  color: var(--text);
}

.name-dialog-body {
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 14px;
  color: #e8eef7;
}

.name-dialog-input-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.name-dialog-input-row input {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--r-sm);
  padding: 10px 14px;
  font-size: 14px;
  color: #fff;
  font-family: inherit;
  outline: none;
  transition: border-color 0.2s;
}

.name-dialog-input-row input:focus {
  border-color: rgba(159, 227, 255, 0.6);
  box-shadow: 0 0 0 3px rgba(159, 227, 255, 0.12);
}

.name-dialog-count {
  font-size: 10px;
  color: rgba(255, 255, 255, 0.3);
  font-variant-numeric: tabular-nums;
  display: none;
}

.name-dialog-count.on {
  display: inline;
  color: rgba(255, 255, 255, 0.5);
}

.name-dialog-count.warn {
  color: #ffbd59;
}

.name-dialog-count.max {
  color: #ff7a7a;
}

.name-dialog-hint {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.45);
  margin-bottom: 14px;
  min-height: 16px;
}

.name-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* ─── Finish Dialog ──────────────────────────────────────────────── */
/* Overlay blurs the game scene behind the dialog without darkening
   it: backdrop-filter only (no background tint). The card's own
   var(--glass) + var(--glass-blur) recipe still owns the card
   surface tint identical to .board on /leaderboard. The card sits
   on top of the overlay, so its backdrop-filter samples through the
   already-blurred output — that compounds the blur slightly but
   doesn't add any dimming, which was the previous parity issue. */
#finishDialogOverlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 12500;
  background: transparent;
  backdrop-filter: blur(8px) saturate(1.05);
  -webkit-backdrop-filter: blur(8px) saturate(1.05);
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
}

#finishDialogCard {
  width: min(94vw, 680px);
  max-height: min(92vh, 780px);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background-color: var(--glass);
  background-image: var(--glass-specular);
  /* Mirrors .board on /leaderboard so the dialog and the share page
     read as the same component: white-tinted glass border + the
     same multi-layer specular/glow stack. */
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    inset 0 0 32px 0 rgba(255, 255, 255, 0.03),
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 1px 0 rgba(0, 0, 0, 0.5);
  color: #e8eef7;
  animation: bounceIn 0.5s var(--spring) both;
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: radial-gradient(white, black);
}

#finishDialogHeader {
  padding: 16px 20px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

#finishDialogSaveHint {
  margin-top: 8px;
  min-height: 16px;
  font-size: 11px;
  /* WCAG AA needs 4.5:1 at 11px. The hint sits over the dialog's
     glass surface (which itself blurs the game scene), so the
     effective background luminance varies. Use a near-solid
     warm-white tint that clears AA against both the dialog body
     and the gold-tinted .own-current row bg. */
  color: rgba(232, 244, 255, 0.92);
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Row-attached save hint — sits directly under the editable name
   input within the same leaderboard row, so the player gets feedback
   right next to where they're typing. Shares the busy/error modifier
   classes with the top-level #finishDialogSaveHint. Color matches
   the top-level hint for AA contrast at 11px against both the
   dialog body and the gold .own-current row bg. */
.finishDialogRowSaveHint {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  min-height: 14px;
  font-size: 11px;
  line-height: 1.25;
  color: rgba(232, 244, 255, 0.92);
}

.finishDialogRowSaveHint:empty {
  display: none;
}

.finishDialogRowSaveHint.finishDialogSaveHint--busy {
  font-size: 11px;
  color: #9fe3ff;
}

.finishDialogRowSaveHint.finishDialogSaveHint--error {
  color: #ffb4b4;
}

/* Saved-success state: tint the hint green so the animated check has
   matching companion text. Picked to clear AA at 11px against both
   the dialog body and the gold .own-current row bg. */
.finishDialogRowSaveHint.finishDialogSaveHint--saved,
#finishDialogSaveHint.finishDialogSaveHint--saved {
  color: #b5f0c9;
}

/* Inside the row, the spinner can stay 12px but we shave a hair so it
   fits the smaller in-row text size. */
.finishDialogRowSaveHint .finishSavingSpinner {
  width: 11px;
  height: 11px;
}

/* While a save is in flight, lift the hint visually so the player
   notices it next to the disabled action buttons. */
#finishDialogSaveHint.finishDialogSaveHint--busy {
  font-size: 12px;
  color: #9fe3ff;
}

#finishDialogSaveHint.finishDialogSaveHint--error {
  color: #ffb4b4;
}

/* Animated saved checkmark — drawn with two stroke-dasharray passes
   (ring then tick) chained off a small pop. Replays cheaply each
   time the saved hint re-renders, which is what we want as feedback
   when a rename succeeds. The whole thing is 14×14 in the row hint
   and scales naturally inside flex. */
.finishSavedCheck {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  overflow: visible;
  animation: finishSavedCheckPop 320ms cubic-bezier(.2, .8, .2, 1.2) both;
}

#finishDialogSaveHint .finishSavedCheck {
  width: 16px;
  height: 16px;
}

.finishSavedCheck__ring {
  fill: none;
  stroke: #6cd49a;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 60;
  stroke-dashoffset: 60;
  transform-origin: center;
  animation: finishSavedCheckRingDraw 360ms ease-out 60ms forwards;
}

.finishSavedCheck__tick {
  fill: none;
  stroke: #7ce0a8;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 16;
  stroke-dashoffset: 16;
  animation: finishSavedCheckTickDraw 260ms ease-out 220ms forwards;
}

@keyframes finishSavedCheckPop {
  0% {
    transform: scale(0.4);
    opacity: 0;
  }

  60% {
    transform: scale(1.12);
    opacity: 1;
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes finishSavedCheckRingDraw {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes finishSavedCheckTickDraw {
  to {
    stroke-dashoffset: 0;
  }
}

@media (prefers-reduced-motion: reduce) {

  .finishSavedCheck,
  .finishSavedCheck__ring,
  .finishSavedCheck__tick {
    animation: none;
  }

  .finishSavedCheck__ring,
  .finishSavedCheck__tick {
    stroke-dashoffset: 0;
  }
}

.finishSavingSpinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid rgba(159, 227, 255, 0.22);
  border-top-color: rgba(159, 227, 255, 0.95);
  animation: splashSpinnerRotate 0.85s linear infinite;
  flex-shrink: 0;
}

@media (prefers-reduced-motion: reduce) {
  .finishSavingSpinner {
    animation: splashSpinnerRotate 2.4s linear infinite;
  }
}

#finishDialogSummaryGrid {
  margin-top: 2px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.finishDialogSummaryItem {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  padding: 9px 10px;
  display: grid;
  gap: 4px;
}

.finishDialogSummaryItem .k {
  font-size: 10px;
  letter-spacing: var(--type-tracking-wide);
  color: rgba(197, 225, 245, 0.78);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
}

.finishDialogSummaryItem .v {
  font-size: 14px;
  color: #eaf4ff;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
}

.finishDialogSummaryItemTime .v {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 30px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
  color: #fff;
  text-shadow: 0 1px 12px rgba(159, 227, 255, 0.16);
}

.finishDialogSummaryItemTime {
  grid-column: span 1;
}

.finishDialogSummaryItemCat {
  grid-column: 1 / -1;
}

#finishDialogCatPreview {
  display: block;
  width: 100%;
  height: 86px;
  border-radius: 9px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: radial-gradient(75% 95% at 50% 8%, rgba(178, 228, 255, 0.2), rgba(18, 26, 42, 0.42));
  overflow: hidden;
}

#finishDialogCatCanvas {
  width: 100%;
  height: 100%;
  display: block;
}

.finishDialogCatText {
  margin-top: 3px;
  font-size: 10px;
  letter-spacing: 0.03em;
  color: rgba(198, 225, 245, 0.78);
  text-transform: uppercase;
}

#finishDialogSummaryRank {
  color: #9fe3ff;
}

#finishDialogCoinBadge {
  color: var(--gold);
}

#finishDialogSummarySecret {
  color: #b8ecff;
}

@media (max-width: 760px) {
  #finishDialogSummaryGrid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .finishDialogSummaryItemTime,
  .finishDialogSummaryItemCat {
    grid-column: span 2;
  }
}

@media (max-width: 560px) {
  #finishDialogSummaryGrid {
    grid-template-columns: 1fr;
  }

  .finishDialogSummaryItemTime,
  .finishDialogSummaryItemCat {
    grid-column: span 1;
  }
}

#finishDialogBoard {
  padding: 8px 14px 0;
  overflow-y: auto;
  flex: 1;
}

#finishDialogBoard h4 {
  margin: 8px 0 6px;
  font-size: 11px;
  font-weight: var(--weight-bold);
  letter-spacing: var(--type-tracking-wide);
  color: #9fe3ff;
}

/* Mirrors the standalone /leaderboard board (.list / .row in
   leaderboard.html) so the dialog and the share page read as the
   same component: 38px rank gutter, 10px column gap, 36px row
   height, 11px side padding. Editable/pending rows extend to two
   rows so the save-hint can sit beneath the input. */
#finishDialogList {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

#finishDialogList li {
  display: grid;
  grid-template-columns: 38px 1fr auto auto auto;
  gap: 10px;
  align-items: center;
  height: 36px;
  padding: 0 11px;
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.05);
  font-size: 13px;
  line-height: 1.35;
}

/* Editable/pending rows carry a save-hint sibling that sits on a
   second grid row beneath the input column. Row 1 stays a fixed
   36px so the rank, input, cat badge, and time line up exactly
   like a non-editable row; the hint hangs below in row 2 with a
   small bottom pad so the row reads as one unit. */
#finishDialogList li:has(> .finishDialogRowSaveHint:not(:empty)) {
  height: auto;
  grid-template-rows: 36px auto;
  row-gap: 0;
  padding: 6px 11px 6px;
}

#finishDialogList li>.finishDialogRowSaveHint {
  grid-column: 2 / 3;
  grid-row: 2;
}

#finishDialogList li.own-history {
  background: rgba(255, 216, 115, 0.07);
  border-color: rgba(255, 216, 115, 0.22);
}

#finishDialogList li.own-current {
  background: rgba(255, 216, 115, 0.16);
  border-color: rgba(255, 216, 115, 0.46);
  box-shadow: 0 0 0 1px rgba(255, 216, 115, 0.15), inset 3px 0 0 rgba(255, 226, 140, 0.65);
}

#finishDialogList li .rk {
  color: var(--muted);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

#finishDialogList li .nm {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#finishDialogList li .nm.nm-edit {
  overflow: visible;
  white-space: normal;
}

#finishDialogList li .tm {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: #9fe3ff;
  /* Mono cap-height sits higher in the em-box than the body face;
     nudge down 2px so the digits visually center against the
     proportional rank/name on the same row. Matches the standalone
     /leaderboard .time rule. */
  padding-top: 2px;
}

#finishDialogList li.own-history .tm {
  color: #d9ebff;
}

#finishDialogList li.own-current .tm {
  color: var(--gold);
}

#finishDialogList li.pending {
  border-style: dashed;
}

#finishDialogList li.pending .tm {
  color: rgba(255, 216, 115, 0.85);
}

#finishDialogList li.pending .rk-pending {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.finishRankSkel {
  display: inline-block;
  width: 28px;
  height: 12px;
  border-radius: 5px;
  background: linear-gradient(90deg,
      rgba(255, 216, 115, 0.12) 0%,
      rgba(255, 216, 115, 0.42) 50%,
      rgba(255, 216, 115, 0.12) 100%);
  background-size: 200% 100%;
  animation: finishRankShimmer 1.1s linear infinite;
}

.finishRankSkel--row {
  width: 22px;
  height: 10px;
}

@keyframes finishRankShimmer {
  0% {
    background-position: 100% 0;
  }

  100% {
    background-position: -100% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .finishRankSkel {
    animation: none;
    opacity: 0.55;
  }
}

.finishDialogRowNameInput {
  width: 100%;
  min-width: 0;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 7px;
  padding: 6px 8px;
  color: #fff;
  font-size: 13px;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
}

.finishDialogRowNameInput:focus {
  border-color: rgba(159, 227, 255, 0.62);
  box-shadow: 0 0 0 3px rgba(159, 227, 255, 0.12);
}

.finishDialogRowNameInput:disabled {
  opacity: 0.72;
}

/* Save-failed state on the input itself. Red border + soft halo so
   the player sees the error attached to the field they were editing
   rather than as a disconnected note elsewhere. Cleared the moment
   they start typing again (JS removes the class on input). The halo
   is held back from the focus halo's intensity so a re-focus on the
   errored field still reads as a focused field, not an alert. */
.finishDialogRowNameInput--error,
.finishDialogRowNameInput--error:focus {
  border-color: rgba(255, 132, 132, 0.78);
  box-shadow: 0 0 0 3px rgba(255, 132, 132, 0.16);
  animation: finishDialogRowNameInputErrorShake 320ms cubic-bezier(.36, .07, .19, .97) both;
}

@keyframes finishDialogRowNameInputErrorShake {

  10%,
  90% {
    transform: translate3d(-1px, 0, 0);
  }

  20%,
  80% {
    transform: translate3d(2px, 0, 0);
  }

  30%,
  50%,
  70% {
    transform: translate3d(-3px, 0, 0);
  }

  40%,
  60% {
    transform: translate3d(3px, 0, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .finishDialogRowNameInput--error {
    animation: none;
  }
}

.finishDialogActions {
  /* Standard dialog grouping: destructive (Exit) on the far left,
     affirmative actions (Play again + Copy & share) grouped on the
     right. The flex spacer between them gives the visual separation
     that an equal 3-col grid flattens, and makes mis-tapping Exit
     when reaching for an affirmative action much harder. Buttons
     size to content so the row reads as two groups rather than
     three peers. */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px 14px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  flex-wrap: wrap;
}

/* Spacer: pushes everything after the destructive button to the
   right edge. Applied to the first affirmative button (Play again)
   via the markup. */
.finishDialogActions__spacer {
  margin-left: auto;
}

/* Sizing/layout only — surface (light tile vs deep CTA) comes from
   the shared .glass-btn--tile modifier and the .finishDlgBtn--cta
   override below. Mirrors the .pause-btn rhythm: 18px 20px / 14px. */
.finishDlgBtn {
  padding: 18px 20px;
  font-size: 14px;
  letter-spacing: 0.01em;
  border-radius: var(--r-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  white-space: nowrap;
  min-width: 0;
}

/* Disabled state for the finish-screen action buttons while the run
   save is in flight. Keep them in the layout (so nothing jumps when
   the save resolves) but make it clear they aren't clickable yet. */
.finishDlgBtn:disabled,
.finishDlgBtn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  filter: saturate(0.6);
}

.finishDlgBtn--busy {
  pointer-events: none;
}

/* Copy & share CTA — deep glass tile that reads darker than the
   sibling Exit / Play-again light tiles so it stands out as the
   primary post-run action. Keeps the same size/rhythm as the row;
   only the surface and a subtle cyan halo change. */
.finishDlgBtn--cta {
  background-color: rgba(8, 14, 26, 0.55);
  border-color: rgba(159, 227, 255, 0.32);
  color: #e6f4ff;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.24),
    0 4px 14px rgba(145, 222, 255, 0.12);
}

.finishDlgBtn--cta:hover {
  background-color: rgba(14, 22, 38, 0.62);
  border-color: rgba(159, 227, 255, 0.5);
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.12),
    0 1px 2px rgba(0, 0, 0, 0.24),
    0 6px 18px rgba(145, 222, 255, 0.2);
}

/* On narrow viewports stack the actions to keep tap targets full
   width — same breakpoint as the pause launcher's stack rule. The
   spacer collapses naturally inside a stacked column. */
@media (max-width: 520px) {
  .finishDialogActions {
    flex-direction: column;
    align-items: stretch;
  }

  .finishDialogActions__spacer {
    margin-left: 0;
  }

  .finishDlgBtn {
    width: 100%;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   MOBILE CONTROLS
   ═══════════════════════════════════════════════════════════════════ */
#mobilePad {
  display: none;
  position: fixed;
  bottom: 30px;
  left: 24px;
  width: 130px;
  height: 130px;
  z-index: 310;
}

#mobileJoystick {
  width: 100%;
  height: 100%;
  border-radius: var(--r-circle);
  border: 2px solid rgba(255, 255, 255, 0.15);
  background: rgba(0, 0, 0, 0.15);
  position: relative;
  backdrop-filter: blur(4px);
}

#mobileJoyKnob {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 46px;
  height: 46px;
  border-radius: var(--r-circle);
  background: rgba(255, 255, 255, 0.25);
  border: 2px solid rgba(255, 255, 255, 0.3);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: transform 0.08s linear;
}

#mobileJumpBtn {
  display: none;
  position: fixed;
  bottom: 40px;
  right: 24px;
  width: 72px;
  height: 72px;
  border-radius: var(--r-circle);
  background: var(--accent-glow);
  border: 2px solid rgba(145, 222, 255, 0.35);
  color: var(--accent);
  font-size: 28px;
  z-index: 310;
  cursor: pointer;
  box-shadow: 0 4px 20px rgba(145, 222, 255, 0.2);
  transition: all var(--dur-med) var(--spring);
  font-family: inherit;
}

#mobileJumpBtn:active {
  transform: scale(0.85);
  background: rgba(145, 222, 255, 0.5);
}

#mobileExitBtn {
  display: none;
  position: fixed;
  top: 16px;
  right: 60px;
  width: 40px;
  height: 40px;
  border-radius: var(--r-lg);
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  color: var(--danger);
  font-size: 18px;
  z-index: 310;
  cursor: pointer;
  transition: all var(--dur-med) var(--spring);
  font-family: inherit;
}

#mobileExitBtn:active {
  transform: scale(0.85);
}

/* ═══════════════════════════════════════════════════════════════════
   PLAY-MODE VISIBILITY
   ═══════════════════════════════════════════════════════════════════ */
body.play-mode #playDockBtn,
body.play-mode .panel,
body.play-mode .panel-fab,
body.play-mode .part-tooltip {
  display: none !important;
}

/* Hide Control Center + tooltips while the character-select modal is open
   so the picker reads as a clean full-screen UI. */
body:has(#charSelect.open) #playDockBtn,
body:has(#charSelect.open) .panel,
body:has(#charSelect.open) .panel-fab,
body:has(#charSelect.open) .part-tooltip {
  display: none !important;
}

/* Same treatment for the title splash so the room canvas peeks behind a
   clean overlay with nothing else fighting it for attention. */
body:has(#titleSplash.open) #playDockBtn,
body:has(#titleSplash.open) .panel,
body:has(#titleSplash.open) .panel-fab,
body:has(#titleSplash.open) .part-tooltip {
  display: none !important;
}

body.play-mode #fpHud {
  display: block;
}

body.play-mode #fpCrosshair {
  display: block;
}

body.play-mode #fps {
  top: 96px;
  right: 16px;
  bottom: auto;
}

/* Firefox + macOS pointer-lock fallback: we don't lock the pointer on
   that combo (Bugzilla 1417702 corrupts movementX/Y too badly to be
   playable), so the system cursor would otherwise float over the
   game. .fp-no-cursor hides it everywhere on the canvas; pause/finish
   overlays sit on top of the body and remove the rule via :has,
   restoring the cursor for buttons. */
body.fp-no-cursor,
body.fp-no-cursor canvas,
body.fp-no-cursor #fpHud,
body.fp-no-cursor #fpCrosshair {
  cursor: none !important;
}

body.fp-no-cursor:has(#fpPauseOverlay[style*="display: flex"]),
body.fp-no-cursor:has(.finish-dialog.open),
body.fp-no-cursor:has(#deathOverlay.open) {
  cursor: auto !important;
}

/* ═══════════════════════════════════════════════════════════════════
   TITLE SPLASH — liquid-glass overlay above the live three.js scene.
   Also serves as the loading screen: starts in `data-state="loading"`
   (opaque dark bg, button shows a spinner) and transitions to
   `data-state="ready"` once the first scene frame paints.
   ═══════════════════════════════════════════════════════════════════ */
#titleSplash {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 480;
  padding: 32px 20px;
  overflow: hidden;
  /* The card is a SIBLING of this element (see vite-index.html), so
     putting backdrop-filter here is now safe — it doesn't trap the
     card inside a backdrop root. This blurs the canvas across the
     whole splash area (matching the leaderboard/about iframe-bg look)
     while the card adds its own stronger blur on top. */
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(8, 10, 16, 0.55) 0%, rgba(8, 10, 16, 0) 55%),
    radial-gradient(120% 80% at 50% 100%, rgba(8, 10, 16, 0.6) 0%, rgba(8, 10, 16, 0) 55%);
  /* Subtle scrim blur, matched to #bgScrim on /leaderboard and /about
     so all three pages feel like the same world seen through the same
     piece of glass. The card on top adds the strong --glass-blur. */
  backdrop-filter: blur(2px) saturate(1.1);
  -webkit-backdrop-filter: blur(2px) saturate(1.1);
  /* Smooth crossfade from the loading-state opaque bg to the ready-state
     transparent glass once the scene is ready. */
  transition: background 0.6s ease-out;
}

#titleSplash.open {
  display: block;
}

/* Loading state — splash IS the boot screen. Uses the SAME radial-blue
   gradient as the home / about / leaderboard <body> fallback so entering
   the game from /home reads as a continuation of that atmosphere instead
   of a jarring color flash before the scene appears. The 3D scene loads
   in the background and the splash dismisses straight into character
   select, which keeps the same scrim look. */
#titleSplash[data-state="loading"] {
  background:
    radial-gradient(1200px 700px at 18% -12%, #244868 0%, transparent 58%),
    radial-gradient(900px 560px at 110% 10%, #1f3e56 0%, transparent 65%),
    linear-gradient(180deg, #122637, #07121d);
  /* Loading bg is opaque — no need to blur a scene we can't see. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* While loading, the play button can't actually start a run — show a
   progress cursor so it's obvious it's not idle. */
#titleSplash[data-state="loading"]+.title-splash-card .title-splash-play {
  cursor: progress;
}

/* ─── Splash background ───────────────────────────────────────── */
.title-splash-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* Subtle dotted grid kept for engineering-side-project flavor; sits over
   the glass so it doesn't fight with the liquid orbs. */
.title-splash-grid {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px);
  background-size: 32px 32px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, #000 30%, transparent 80%);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, #000 30%, transparent 80%);
  opacity: 0.5;
}

/* ─── Frosted-glass hero card ─────────────────────────────────── */
/* Uses the canonical liquid-glass tokens (--glass, --glass-blur,
   --glass-specular, --glass-border) shared with .panel,
   .skate-onboard-card, finish dialog, etc. so every glass surface in
   the app refracts the world the same way. The cool dark tint also
   keeps white text legible against the busy live scene behind. */
.title-splash-card {
  /* Hidden until #titleSplash gets the .open class (the sibling rule
     below switches this to flex). The card lives OUTSIDE #titleSplash
     in the DOM so its backdrop-filter samples the canvas directly —
     putting it inside the full-viewport overlay made Chromium clip the
     backdrop sample to the overlay's own paint output, killing the
     blur. See vite-index.html for the structural reasoning. */
  display: none;
  /* Position relative + parent flex centering on home.html. We used to
     use position:fixed + transform: translate(-50%, -50%) here, but
     that compound-transform centering conflicted with the shared
     pageContentIn keyframe (whose `transform: translateY(...)` would
     overwrite the centering). Now centering is done by the parent
     <main class="page"> flexbox, freeing transform for animation. */
  position: relative;
  z-index: 481;
  /* one above #titleSplash (z-index 480) so the card paints on top */
  max-width: 720px;
  width: calc(100% - 40px);
  text-align: center;
  color: #fff;
  padding: 44px 36px 36px;
  border-radius: 32px;
  background-color: var(--glass);
  background-image: var(--glass-specular);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    inset 0 0 32px 0 rgba(255, 255, 255, 0.03),
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 1px 0 rgba(0, 0, 0, 0.5);
  /* Same Linear-style fade-in used on the about / leaderboard cards
     (pageContentIn). Centering is now flex-based so we can use the
     shared keyframe directly. */
  animation: pageContentIn 0.7s cubic-bezier(0.32, 0.72, 0, 1) both;
}

/* Show the card whenever the splash is open. Sibling combinator works
   because the card is the next element after #titleSplash in the DOM. */
#titleSplash.open+.title-splash-card {
  display: block;
}

/* Top sheen — slim refractive glaze across the very top of the card. */
.title-splash-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 6%;
  right: 6%;
  height: 1px;
  background: linear-gradient(90deg,
      transparent 0%,
      rgba(255, 255, 255, 0.7) 50%,
      transparent 100%);
  border-radius: 32px 32px 0 0;
  pointer-events: none;
}

/* Big duotone title with a hot-pink offset shadow for hero punch. */
.title-splash-title {
  position: relative;
  font-size: clamp(48px, 9.5vw, 104px);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 0.92;
  /* Extra top/bottom margin so the wordmark gets real breathing room
     inside the card (the eyebrow above and the tagline below were
     crowding the trail's vertical glow). */
  margin: 28px 0 60px;
  display: inline-block;
  /* The motion trail (speed lines + text-shadow haze) only extends
     left of the wordmark, so the visual mass sits left of the
     element's geometric center. Nudge the whole inline-block rightward
     so the perceived center of "trail + word" lands on the card's
     center axis. Transform is used (not margin/padding) because the
     speed-line pseudos are positioned relative to this box \u2014 changing
     the box width would drag them along; translate moves everything
     as a rigid unit. */
  transform: translateX(clamp(18px, 3vw, 44px));
}

/* Decorative speed lines streaking past the wordmark — abstract
   horizontal blue dashes layered on the left, fading rightward. The
   wordmark stays clean and bright; these lines do the visual work of
   "this thing is screaming past you." Three lines total: two via
   pseudo-elements (cap of 2 per element), and a third via a real
   <span class="title-splash-speed-line"> in the markup. */
.title-splash-title::before,
.title-splash-title::after,
.title-splash-speed-line {
  content: "";
  position: absolute;
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg,
      transparent 0%,
      rgba(159, 227, 255, 0.0) 0%,
      rgba(159, 227, 255, 0.7) 60%,
      rgba(255, 255, 255, 0.85) 100%);
  filter: blur(1.5px);
  pointer-events: none;
}

.title-splash-title::before {
  width: 21.6%;
  top: 18%;
  left: -23%;
}

.title-splash-title::after {
  width: 16.8%;
  top: 82%;
  left: -21%;
}

.title-splash-speed-line {
  width: 12%;
  top: 48%;
  left: -10.58%;
}

.title-splash-title-main {
  position: relative;
  /* Near-white wordmark with the faintest cool tint, so it reads as
     bright/clean against the dark card. The bottom stop is cool but
     still light \u2014 a deeper blue stop here pulls the whole word toward
     icy-blue, which is what we don't want. */
  background: linear-gradient(180deg, #ffffff 0%, #f4faff 55%, #d8e8f5 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* background-clip:text only paints the gradient inside this element's
     box, clipped to glyphs. With letter-spacing: -0.04em + heavy
     weight, the rightmost glyph's ink overshoots its advance box on
     the right \u2014 those pixels render as color:transparent (= invisible)
     against the dark card. Padding-right on the same element that owns
     the gradient extends the paint region just past the trailing glyph
     so its full shape gets filled. */
  padding-right: 0.32em;
  /* Speed-line trail \u2014 lots of layers, lots of blur, long reach. The
     wordmark itself stays nearly white; the trail does the motion
     work. Layers progress from a near-solid cyan close to the letters
     to a soft deep-blue haze 140px out, mimicking how motion blur
     fades from sharp to diffuse with distance. */
  text-shadow:
    -3px 0 0 rgba(220, 240, 255, 0.55),
    -10px 0 14px rgba(159, 227, 255, 0.55),
    -22px 0 26px rgba(120, 200, 240, 0.4),
    -42px 0 44px rgba(80, 170, 220, 0.3),
    -68px 0 64px rgba(60, 140, 200, 0.22),
    -100px 0 90px rgba(50, 120, 180, 0.16),
    -140px 0 120px rgba(40, 100, 160, 0.1),
    0 3px 0 rgba(0, 0, 0, 0.3);
  transform: skewX(-7deg);
  transform-origin: left center;
  display: inline-block;
}

.title-splash-title-shadow {
  position: absolute;
  inset: 0;
  color: transparent;
  -webkit-text-stroke: 1.5px rgba(255, 102, 168, 0.55);
  transform: translate(7px, 7px);
  z-index: -1;
  pointer-events: none;
}

@media (max-width: 640px) {
  .title-splash-title-shadow {
    transform: translate(4px, 4px);
    -webkit-text-stroke-width: 1px;
  }
}

.title-splash-tagline {
  font-size: clamp(15px, 1.7vw, 17px);
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.82);
  margin: 0 auto 32px;
  max-width: 540px;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
}

.title-splash-tagline strong {
  color: #ffe4c8;
  font-weight: 600;
}

/* ─── Liquid glass play button ────────────────────────────────── */
.title-splash-cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-bottom: 28px;
}

/* ─── Splash secondary nav (Leaderboard / About) ────────────────
   A small understated row beneath the play button. The Play CTA stays
   the unmistakable hero; these are quiet routes for the Public
   leaderboard and the About page. */
.title-splash-links {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  font-weight: var(--weight-semibold);
  color: rgba(255, 255, 255, 0.78);
  letter-spacing: 0.02em;
}

.title-splash-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  color: inherit;
  text-decoration: none;
  background-color: var(--glass-soft);
  border: 1px solid var(--glass-stroke-soft, rgba(255, 255, 255, 0.08));
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  transition: border-color 0.2s, background-color 0.2s, transform 0.2s, color 0.2s;
}

.title-splash-link:hover {
  color: #fff;
  border-color: var(--glass-border-strong);
  background-color: rgba(255, 255, 255, 0.07);
  transform: translateY(-1px);
}

.title-splash-link:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.7);
  outline-offset: 3px;
}

.title-splash-link i {
  font-size: 15px;
  opacity: 0.95;
}

.title-splash-link-sep {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.32);
}

/* ─── Cross-page top-tab nav (about ↔ leaderboard) ──────────────
   Liquid-glass pill bar pinned to the top of the page. A tiny SPA
   router (inline in about.html and leaderboard.html) intercepts
   tab clicks and swaps just the <main> contents — the persistent
   <iframe id="bgFrame"> stays mounted, so the live scene never
   reloads when you switch views. */
.site-tabs {
  position: fixed;
  top: 18px;
  left: 50%;
  /* Resting (hidden) state — tabs sit just above the viewport, blurred
     and transparent. Each page's boot script flips body.tabs-revealed
     once the bg scene is up AND the page card has finished its own
     entrance, so the nav drops in cleanly after everything else. */
  transform: translate(-50%, -34px);
  opacity: 0;
  filter: blur(10px);
  pointer-events: none;
  z-index: 482;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px;
  border-radius: 999px;
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid var(--glass-border);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.32),
    inset 0 -1px 0 rgba(255, 255, 255, 0.05),
    0 18px 50px rgba(0, 0, 0, 0.45),
    0 1px 0 rgba(0, 0, 0, 0.45);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  font-family: var(--font-ui);
  /* Easing matches main.page's transition; duration is a touch longer
     so the eye reads "okay, the room is here, NOW the chrome arrives"
     rather than racing the page card. */
  transition:
    transform 0.62s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1),
    filter 0.55s cubic-bezier(0.32, 0.72, 0, 1);
}

body.tabs-revealed .site-tabs {
  transform: translate(-50%, 0);
  opacity: 1;
  filter: blur(0);
  pointer-events: auto;
}

/* Play CTA leaving — body.is-leaving is set by fullNav() in each
   page's router right before navigating to /play. Tabs lift up and
   blur out together with main.page, so the chrome clears the stage
   before the character-select bounces in on the next document.

   Exit uses ease-in (slow start, accelerate away) instead of the
   entry's ease-out-expo. The entrance settles INTO place; the exit
   does the inverse — the elements hold their position for a beat,
   then visibly accelerate off-screen. With ease-out-expo the chrome
   was perceptually "gone" by ~280ms on a 550ms duration because the
   curve front-loads the motion; ease-in keeps the elements visibly
   present across the bulk of the duration so the user actually sees
   them leave. */
body.is-leaving .site-tabs {
  transform: translate(-50%, -64px);
  opacity: 0;
  filter: blur(14px);
  pointer-events: none;
  transition:
    transform 0.7s cubic-bezier(0.55, 0, 0.78, 0.4),
    opacity 0.7s cubic-bezier(0.55, 0, 0.78, 0.4),
    filter 0.6s cubic-bezier(0.55, 0, 0.78, 0.4);
}

@media (prefers-reduced-motion: reduce) {
  .site-tabs {
    transform: translate(-50%, 0);
    opacity: 1;
    filter: none;
    pointer-events: auto;
    transition: none;
  }

  body.is-leaving .site-tabs {
    transform: translate(-50%, 0);
    opacity: 0;
    filter: none;
    transition: opacity 0.16s ease;
  }
}

html.is-bg .site-tabs {
  display: none !important;
}

html.is-bg .site-tabs-pill {
  display: none !important;
}

.site-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.78);
  text-decoration: none;
  background: transparent;
  border: 1px solid transparent;
  transition:
    color 0.22s cubic-bezier(0.32, 0.72, 0, 1),
    background-color 0.2s ease,
    border-color 0.2s ease,
    transform 0.12s cubic-bezier(0.32, 0.72, 0, 1);
}

.site-tab:hover {
  color: #fff;
  background-color: rgba(255, 255, 255, 0.06);
}

/* Tip #1 (Emil Kowalski): subtle scale-down on press makes the tab
   feel as if it is listening. */
.site-tab:active {
  transform: scale(0.97);
}

.site-tab:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.7);
  outline-offset: 2px;
}

.site-tab i {
  font-size: 15px;
  opacity: 0.92;
}

/* Each tab renders both icon variants (regular + fill); CSS picks the
   one that matches the active state. The fill glyph aligns optically
   slightly differently on some weights, so we stack them in the same
   inline slot rather than swapping classes via JS. */
.site-tab .site-tab__icon--fill {
  display: none;
}

.site-tab.is-active .site-tab__icon--regular {
  display: none;
}

.site-tab.is-active .site-tab__icon--fill {
  display: inline-block;
  opacity: 1;
}

/* Fallback active styling — used when JS fails to build the pill
   overlay. With the pill present (.site-tabs.has-pill) the bg/border/
   shadow are dropped because the pill itself provides the surface;
   only the text-color swap stays. */
.site-tab.is-active {
  color: #fff;
  background-color: rgba(255, 255, 255, 0.13);
  border-color: var(--glass-border-strong);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 1px 0 rgba(0, 0, 0, 0.25);
}

.site-tabs.has-pill .site-tab.is-active {
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* Single moving pill — a Stripe-blog style indicator that slides and
   resizes to match the active tab. JS in each page's router sets the
   pill's translateX + width from getBoundingClientRect; CSS animates
   the transform and width with the same ease-out used elsewhere. The
   pill sits behind the tabs (z-index 0) so the tab text is on top.

   We animate transform (GPU) plus width (layout). The pill is tiny so
   the layout cost is negligible; doing both keeps the rounded ends
   from distorting (which scaleX would do). */
.site-tabs-pill {
  position: absolute;
  top: 5px;
  bottom: 5px;
  left: 0;
  width: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: 999px;
  background-color: rgba(255, 255, 255, 0.13);
  border: 1px solid var(--glass-border-strong);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 1px 0 rgba(0, 0, 0, 0.25);
  /* Hidden until JS positions it on first paint, then revealed. */
  opacity: 0;
  transform: translateX(0);
  will-change: transform, width;
}

.site-tabs.has-pill .site-tab {
  /* Tabs sit above the moving pill. */
  z-index: 1;
}

.site-tabs.pill-ready .site-tabs-pill {
  opacity: 1;
  transition:
    transform 0.32s cubic-bezier(0.32, 0.72, 0, 1),
    width 0.32s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.18s ease;
}

@media (prefers-reduced-motion: reduce) {
  .site-tabs.pill-ready .site-tabs-pill {
    transition: opacity 0.18s ease;
  }
}

@media (max-width: 480px) {
  .site-tabs {
    top: 12px;
    gap: 2px;
    padding: 4px;
  }

  .site-tab {
    padding: 8px 12px;
    font-size: 12px;
  }

  .site-tab i {
    font-size: 14px;
  }

  .site-tabs-pill {
    top: 4px;
    bottom: 4px;
  }
}

/* Shared dotted-grid texture for the page-level scrim (#bgScrim on
   about / leaderboard). Mirrors `.title-splash-grid` exactly so the
   home #titleSplash and the other pages' #bgScrim show the same
   "engineering project" texture over the live three.js scene. */
.bg-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px);
  background-size: 32px 32px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, #000 30%, transparent 80%);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, #000 30%, transparent 80%);
  opacity: 0.5;
}

/* SPA-swap transition — used by the home ↔ about ↔ leaderboard router
   so only the foreground card transitions while we swap <main>. The
   iframe carrying the live scene keeps painting at full opacity, so
   the background never re-mounts or blinks.

   Each direction owns its own state class on the <main> element
   itself (not the body). This matters because the new <main> needs a
   clean "from" snapshot to transition OUT of — if we used the same
   class for both halves, the browser would fold the from/to states
   into one paint and the new content would just pop in.

   Animates ONLY opacity + transform on the OUTGOING page. The
   incoming page snaps in with no entry transition at all — and that's
   intentional. Any opacity, transform, or filter on the new <main>
   would form a stacking context, and stacking contexts break
   `backdrop-filter` on descendants: the glass cards inside would only
   be able to sample content painted earlier WITHIN <main> (i.e.
   nothing) instead of the iframe behind it. The result was the
   user-visible "blur appears one second after the page does" bug —
   cards looked clear during the entry fade, then "snapped" to blurred
   the moment opacity hit 1 and the stacking context dissolved.

   Since the iframe persists across SPA swaps, the new page snapping
   in over the same scene is barely perceptible — the eye is still
   tracking the outgoing page's fade, and the new content arrives with
   blur intact from frame 1. */
main.page {
  transition:
    opacity 220ms ease,
    transform 280ms cubic-bezier(0.22, 1, 0.36, 1),
    /* Filter is included on the base rule (not just on the
       is-leaving rule) so the 10px exit blur RAMPS BACK OFF when
       is-leaving is removed by demoteFromPlay — without this
       transition the blur snaps off in one frame, which read as a
       jarring pop on the way back from /play to /. */
    filter 0.4s cubic-bezier(0.32, 0.72, 0, 1);
}

/* SPA page swap — direction-aware horizontal slide. The router sets
   data-direction="forward" when navigating later in tab order
   (Home → Leaderboard → About → Settings) and "back" otherwise.
   The exit and entry happen sequentially: the leaving <main>
   slides + fades out, then is removed, then the incoming <main>
   slides into rest.

   TRADEOFF on the entry: any transform/opacity/filter/clip-path on
   <main> creates a stacking context, which breaks backdrop-filter
   sampling on the glass cards inside (they sample <main>'s own
   contents instead of the iframe behind <body>) until the animation
   ends and the stacking context dissolves. There's no CSS workaround.
   So we keep the entry SHORT (~140ms) and translate-only — no
   opacity fade, no filter — to minimize the duration of the
   unblurred flash. The exit is longer so the eye is still tracking
   outgoing motion when the new page lands. */
main.page.is-leaving {
  pointer-events: none;
  opacity: 0;
  transition:
    opacity 280ms ease,
    transform 320ms cubic-bezier(0.22, 1, 0.36, 1),
    filter 0.4s cubic-bezier(0.32, 0.72, 0, 1);
}

main.page.is-leaving[data-direction="forward"] {
  transform: translateX(-44px);
}

main.page.is-leaving[data-direction="back"] {
  transform: translateX(44px);
}

main.page.is-entering {
  /* Translate-only, no opacity. Short duration so the
     backdrop-filter "unblurred flash" on glass cards is at the edge
     of perceptible. */
  transition: transform 140ms cubic-bezier(0.22, 1, 0.36, 1);
}

main.page.is-entering[data-direction="forward"] {
  transform: translateX(28px);
}

main.page.is-entering[data-direction="back"] {
  transform: translateX(-28px);
}

/* Play CTA exit — body.is-leaving is set by fullNav() right before we
   navigate to /play. The tabs lift up (rule above), and the page card
   sinks slightly + blurs out so the whole foreground clears the stage
   together. The bg iframe keeps painting at full opacity, so the
   character-select on the next document bounces in over the same
   live room with no scene seam.

   Durations are intentionally generous — the promotion sequence is
   choreographed in three beats: chrome exits (~0.55s), the bedroom
   sits alone for a brief breath (~150ms), then the picker fades in.
   Faster exit blurred the boundary between phases; longer exit lets
   each beat read on its own. */
body.is-leaving main.page {
  opacity: 0;
  transform: translateY(8px);
  filter: blur(10px);
  pointer-events: none;
  transition:
    opacity 0.7s cubic-bezier(0.55, 0, 0.78, 0.4),
    transform 0.7s cubic-bezier(0.55, 0, 0.78, 0.4),
    filter 0.6s cubic-bezier(0.55, 0, 0.78, 0.4);
}

/* Intentionally no .is-entering rule — see comment above. The router
   still adds/removes the class for symmetry, but it has no visual
   effect. */

/* (Previously the home #titleSplash + .title-splash-card had their own
   keyframes that needed to be suppressed on SPA-inserted <main>s to
   prevent a double-animation blink. Now both use the shared
   pageContentIn keyframe — or no animation at all on #titleSplash —
   so that suppression is no longer needed. The animation runs cleanly
   on every entry, whether direct-load or SPA-swap.) */

/* ─── Linear-style content fade-in ─────────────────────────────────
   Each direct child of <main class="page"> fades in from blurred +
   slightly translated, with a small per-child stagger. This runs on
   BOTH first load AND SPA swap (no class gating needed — keyframes
   just fire when the element first paints).

   IMPORTANT: the animation lives on the CHILDREN, not on .page
   itself. Putting opacity / transform / filter on .page would form a
   stacking context and break `backdrop-filter` on every glass card
   inside it (the bug we just chased down for the SPA transition).
   Applying `filter: blur()` on a card itself is safe — the browser
   composites the card's own backdrop-filtered backdrop first, THEN
   filters the result, so the card's blur effect on the iframe still
   shows through. By the end of the animation `filter` is back to
   `none`, leaving no permanent stacking context. */
@keyframes pageContentIn {
  from {
    opacity: 0;
    transform: translateY(12px);
    filter: blur(8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

main.page>*:not(#titleSplash):not(.title-splash-card) {
  animation: pageContentIn 0.42s cubic-bezier(0.32, 0.72, 0, 1) both;
}

main.page>*:nth-child(1):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 0ms;
}

main.page>*:nth-child(2):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 36ms;
}

main.page>*:nth-child(3):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 72ms;
}

main.page>*:nth-child(4):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 108ms;
}

main.page>*:nth-child(5):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 144ms;
}

main.page>*:nth-child(n+6):not(#titleSplash):not(.title-splash-card) {
  animation-delay: 170ms;
}

/* SPA-inserted <main> elements use the parent slide as the entire
   transition — no need to also stagger the children fading in over
   it. Direct loads (no data-spa) still get the boot-time stagger so
   the first paint of any page on a fresh visit feels rich. */
main.page[data-spa="1"]>*:not(#titleSplash):not(.title-splash-card) {
  animation: none;
}

/* The home splash treatment (#titleSplash + .title-splash-card) keeps
   its own boot entrance — explicitly excluded above. */

@media (prefers-reduced-motion: reduce) {

  main.page,
  main.page.is-leaving,
  main.page.is-entering {
    transition: none;
    transform: none;
  }

  body.is-leaving main.page {
    transition: opacity 0.16s ease;
    transform: none;
    filter: none;
  }

  main.page>* {
    animation: none;
  }
}

/* ─── Splash play button — hero sizing on top of .glass-btn--primary ───
   The button uses the canonical .glass-btn .glass-btn--primary classes
   in markup, so its glass surface, blur, hover lift, cursor-tracked
   shine, and click-flash all match the rest of the in-game UI exactly.
   This rule only adds the hero-specific differences: pill shape, larger
   padding, bigger font, fixed min-width so the Play / Loading… / Hang
   tight… state swaps don't reflow the button. */
.title-splash-play {
  /* Fixed min-width so swapping between “Play”, “Loading…”, and
     “Hang tight…” doesn’t reflow the button. 320px comfortably fits
     the longest state copy + spinner at the larger hero sizing. */
  min-width: 320px;
  padding: 22px 64px;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: 0.02em;
  border-radius: 999px;
  /* Slightly stronger drop-shadow than a normal glass-btn since this is
     the hero CTA over a busy live scene. */
  box-shadow:
    0 16px 44px rgba(0, 0, 0, 0.4),
    0 2px 6px rgba(0, 0, 0, 0.2);
}

.title-splash-play:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.7);
  outline-offset: 4px;
}

/* Three swappable content rows inside the play button — only one is shown
   at a time based on splash data-state and the button's own .is-waiting
   class. Default visibility is set here; data-state rules toggle them. */
.title-splash-play-content {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 12px;
  position: relative;
  z-index: 1;
}

#titleSplash[data-state="loading"]+.title-splash-card .title-splash-play-content--loading {
  display: inline-flex;
}

#titleSplash[data-state="ready"]+.title-splash-card .title-splash-play-content--ready {
  display: inline-flex;
}

/* Player clicked Play before the scene was ready — keep them on the splash
   with a spinner in the button until we can drop them into char select. */
#titleSplash[data-state="ready"]+.title-splash-card .title-splash-play.is-waiting .title-splash-play-content--ready {
  display: none;
}

#titleSplash[data-state="ready"]+.title-splash-card .title-splash-play.is-waiting .title-splash-play-content--waiting {
  display: inline-flex;
}

.title-splash-play.is-waiting {
  cursor: progress;
}

/* The spinner element is no longer used on the home splash (the
   full-button shimmer replaced it). Keep the class for the legacy
   index.html splash and vite-index.html which still reference it. */
.title-splash-play-spinner {
  display: inline-block;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2.5px solid rgba(255, 255, 255, 0.22);
  border-top-color: rgba(255, 255, 255, 0.92);
  animation: splashSpinnerRotate 0.85s linear infinite;
  vertical-align: middle;
  flex-shrink: 0;
}

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

@media (prefers-reduced-motion: reduce) {
  .title-splash-play-spinner {
    animation: splashSpinnerRotate 2.4s linear infinite;
  }
}

@media (max-width: 540px) {
  .title-splash-card {
    padding: 32px 22px 26px;
    border-radius: 26px;
  }

  .title-splash-play {
    width: 100%;
    padding: 16px 24px;
  }
}

body.play-mode #titleSplash,
body.play-mode #titleSplash+.title-splash-card {
  display: none !important;
}

/* ═══════════════════════════════════════════════════════════════════
   CHARACTER SELECT — Smash Bros-style grid

   Backdrop matches the home / about / leaderboard #bgScrim treatment
   (vignette gradient + thin backdrop blur) so opening the picker reads
   as the same atmosphere as the menu pages — the live room behind
   stays visible and the .char-card glass surfaces still pop on top.
   Previously this was a flat rgba(0,0,0,0.7) + heavy 12px blur, which
   broke continuity from /home → /play.

   Open/close uses opacity + visibility (not display: none → flex) so
   transitions fire on BOTH directions. The inner card grid keeps its
   bouncy entrance but as a transition tied to .open instead of a
   one-shot keyframe, so it replays on every open.
   ═══════════════════════════════════════════════════════════════════ */
#charSelect {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  /* `safe center` keeps the panel vertically centered when it fits,
     but flips to top-aligned (no clipping) when the panel is taller
     than the viewport — required so the Start-run button at the
     bottom of the panel stays reachable on phones in portrait. */
  align-items: safe center;
  justify-content: center;
  /* Allow vertical scroll when the panel exceeds the viewport
     (mobile portrait, very short windows). Padding gives the panel
     breathing room from the edges while scrolling. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 16px 0;
  /* Vignette-only scrim — same gradient as #bgScrim on the menu pages.
     The atmospheric backdrop-blur lives on #charSelect::before below
     (NOT directly on this wrapper). A backdrop-filter on the wrapper
     itself would make .char-select-inner sample a pre-blurred layer
     and break its frosted glass. Putting the blur on a ::before makes
     it a sibling-in-paint-order to .char-select-inner, mirroring the
     #bgScrim + glass-card sibling layout used on home / about /
     leaderboard. */
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(8, 10, 16, 0.55) 0%, rgba(8, 10, 16, 0) 55%),
    radial-gradient(120% 80% at 50% 100%, rgba(8, 10, 16, 0.6) 0%, rgba(8, 10, 16, 0) 55%);
  /* Closed (resting) state. We hold opacity:1 and toggle ONLY
     visibility for the hide — if we faded the wrapper's opacity
     instead, the wrapper's own backdrop-filter glass would alpha-
     composite during the ramp, so the panel would render at
     half-strength blur for the first ~250ms of the open and then
     "snap" to full blur at opacity 1. The user sees that as the
     background visibly changing mid-open. With opacity locked at 1,
     the glass paints at full strength from frame 1; the visible
     "fade in" feel comes entirely from the per-child stagger on
     .char-select-inner > * and the wrapper's scale-up transform.

     Visibility flip is delayed 0.55s on close so the inner panel's
     scale-down (0.5s spring) and the children's per-child fade-out
     (0.5s) finish painting before the wrapper goes invisible. */
  opacity: 1;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity 0.32s var(--ease-out-expo),
    visibility 0s linear 0.55s;
}

/* Atmospheric backdrop blur — matches #bgScrim on the menu pages so the
   character-select reads as the same chrome. Painted before the inner
   panel, so .char-select-inner's own frosted backdrop-filter samples
   {live iframe + this blur} — same composition as the glass cards on
   home / about / leaderboard sampling {iframe + #bgScrim}. */
/* Atmospheric backdrop blur — matches #bgScrim on the menu pages so the
   character-select reads as the same chrome.

   IMPORTANT: this lives on body::before (gated to when the picker is
   open) instead of on #charSelect::before. If we put the blur on the
   picker wrapper, the wrapper's opacity:0→1 transition makes the blur
   ramp in over 320ms — the bedroom looks sharp for a beat right when
   the chrome fades out, then the blur snaps back in. Putting it on
   body with a :has() gate means it activates instantly the moment
   .open is added (no opacity inheritance), so the bedroom blur stays
   at constant strength as the picker fades in on top.

   The z-index sits between the scene canvas (z-auto) and #charSelect
   (z:500) so the blur samples only the canvas — the picker paints
   above this layer and isn't blurred by it. */
body:has(#charSelect.open)::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  backdrop-filter: blur(2px) saturate(1.1);
  -webkit-backdrop-filter: blur(2px) saturate(1.1);
}

#charSelect.open {
  visibility: visible;
  pointer-events: auto;
  transition: visibility 0s linear;
}

/* Body.is-leaving fade — when fullNav() / _exitToHome() set
   body.is-leaving on /play before navigating away, animate the picker
   out the same way it came in: panel scales back, children fade out
   per-child. Crucially we DO NOT fade the wrapper's opacity — doing
   that would alpha-composite both the panel's own backdrop-filter
   glass and the body::before atmospheric bedroom blur, so the user
   would see the bedroom visibly un-blur as the picker dimmed. By
   only reversing the children's opacity + the panel's scale, the
   glass paints at full strength right up until visibility:hidden
   snaps it out, and the bedroom stays consistently blurred. The
   close visually mirrors the open. */
body.is-leaving #charSelect.open .char-select-inner {
  transform: scale(0.92) translateY(20px);
}

body.is-leaving #charSelect.open .char-select-inner>* {
  opacity: 0;
  transform: translateY(12px);
  filter: blur(8px);
  /* Cancel the staggered open delays so all children fall away
     together — a stagger on the way out reads as "glitchy” for an
     element that's already on screen. */
  transition-delay: 0ms !important;
}

/* Fast close used when the player clicks Play to actually start a run.
   The normal X-button close runs at 500ms so it visually mirrors the
   open; on game-start that's just dead time before the player sees
   the room. _startGame() adds .is-starting to #charSelect immediately
   before removing .open so this rule wins, collapses the fade to
   ~80ms, drops the per-child stagger, and snaps visibility off as
   soon as the fade finishes. */
#charSelect.is-starting {
  transition:
    opacity 0.08s var(--ease-out-expo),
    visibility 0s linear 0.08s !important;
}

#charSelect.is-starting .char-select-inner {
  transition: transform 0.08s var(--ease-out-expo) !important;
}

#charSelect.is-starting .char-select-inner>* {
  transition:
    opacity 0.08s var(--ease-out-expo),
    transform 0.08s var(--ease-out-expo),
    filter 0.08s var(--ease-out-expo) !important;
  transition-delay: 0ms !important;
}

.char-select-inner {
  position: relative;
  text-align: center;
  width: min(1280px, 92vw);
  max-width: 92vw;
  /* Liquid-glass container — same surface treatment as .hero / .card on
     home / about / leaderboard so the picker reads as the same chrome
     as the menu pages. White-edged border, inset highlight + dark seam
     shadow stack, 32px radius, glass blur backdrop. The #charSelect
     scrim behind already provides the soft 2px atmosphere blur, this
     adds the heavier frosted panel on top. */
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid var(--glass-border);
  border-radius: 32px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    inset 0 0 32px 0 rgba(255, 255, 255, 0.03),
    0 30px 80px rgba(0, 0, 0, 0.5),
    0 1px 0 rgba(0, 0, 0, 0.5);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  padding: 32px 32px 28px;
  isolation: isolate;
  /* Resting state — slight scale down + lift. Mirrors the old
     bounceIn keyframe but as a transition tied to #charSelect.open,
     so the bounce replays on every open (and reverses on close).

     Held at opacity 1 for the same reason as #charSelect: a
     fading wrapper would alpha-composite the backdrop-filter
     glass and make the blur ramp visibly with the fade. The
     per-child stagger below handles the visible fade-in of the
     panel's contents. */
  transform: scale(0.92) translateY(20px);
  opacity: 1;
  transition: transform 0.5s var(--spring);
}

/* Top sheen — slim refractive glaze across the very top edge of the
   container. Matches .hero::before / .title-splash-card::before. */
.char-select-inner::before {
  content: "";
  position: absolute;
  top: 0;
  left: 6%;
  right: 6%;
  height: 1px;
  background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.7) 50%, transparent 100%);
  border-radius: 32px 32px 0 0;
  pointer-events: none;
}

#charSelect.open .char-select-inner {
  transform: scale(1) translateY(0);
}

/* Per-child entrance — same Linear-style fade in used on main.page > *
   (opacity + translateY + blur, with a small per-child stagger). The
   panel's own bounce-in starts immediately; the children begin a beat
   later (120ms initial delay) so the panel has a moment to settle into
   place before its contents bloom. Each step is 70ms (matches
   pageContentIn). On close, the .open-scoped rules drop, delays revert
   to 0, and all children fade out together with the panel. */
.char-select-inner>* {
  opacity: 0;
  transform: translateY(12px);
  filter: blur(8px);
  transition:
    opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.5s cubic-bezier(0.32, 0.72, 0, 1),
    filter 0.5s cubic-bezier(0.32, 0.72, 0, 1);
}

#charSelect.open .char-select-inner>* {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

#charSelect.open .char-select-inner>*:nth-child(1) {
  transition-delay: 120ms;
}

#charSelect.open .char-select-inner>*:nth-child(2) {
  transition-delay: 190ms;
}

#charSelect.open .char-select-inner>*:nth-child(3) {
  transition-delay: 260ms;
}

#charSelect.open .char-select-inner>*:nth-child(4) {
  transition-delay: 330ms;
}

#charSelect.open .char-select-inner>*:nth-child(n+5) {
  transition-delay: 400ms;
}

@media (prefers-reduced-motion: reduce) {

  #charSelect,
  #charSelect.open,
  .char-select-inner,
  #charSelect.open .char-select-inner {
    transition: opacity 0.18s ease, visibility 0s linear;
    transform: none;
  }

  .char-select-inner>*,
  #charSelect.open .char-select-inner>* {
    transition: opacity 0.18s ease;
    transform: none;
    filter: none;
    transition-delay: 0ms !important;
  }
}

/* Mobile-only advisory inside the character select. Desktop hides it
   entirely (still occupies a child slot in nth-child stagger but gets
   no transition-delay, since it's the 5th child where the rule is
   `n+5` 400ms — kept consistent for when it's visible on mobile). */
.char-mobile-note {
  display: none;
}

@media (max-width: 720px) {
  .char-mobile-note {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 16px 0 0;
    padding: 12px 14px;
    border-radius: var(--r-md);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.10);
    color: rgba(255, 255, 255, 0.78);
    font-size: 13px;
    line-height: 1.4;
    text-align: left;
  }

  .char-mobile-note i {
    font-size: 18px;
    color: rgba(255, 255, 255, 0.6);
    flex-shrink: 0;
    margin-top: 1px;
  }
}

@media (max-width: 720px) {
  .char-select-inner {
    width: 94vw;
    padding: 22px 20px 20px;
    border-radius: 26px;
  }

  .char-select-inner::before {
    border-radius: 26px 26px 0 0;
  }
}

.char-select-header {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  margin-bottom: 24px;
}

.char-select-inner h2 {
  color: #fff;
  font-size: 28px;
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.26);
  margin: 0;
}

.char-back {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  border-radius: var(--r-circle);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: rgba(255, 255, 255, 0.7);
  font-size: 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--dur-med) var(--spring);
  backdrop-filter: blur(8px);
}

.char-back:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  transform: translateY(-50%) scale(1.1);
}

.char-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  margin-bottom: 24px;
}

@media (max-width: 720px) {
  .char-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
  }
}

.char-card {
  /* Mostly transparent — the .char-select-inner panel behind already
     provides the glass material. We just give the cards a faint
     border + a whisper of white tint so each tile is clearly defined
     as its own surface without darkening the panel. */
  background-color: rgba(255, 255, 255, 0.025);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  padding: 14px 12px 16px;
  cursor: pointer;
  position: relative;
  /* Per-property timings so colors/borders/shadows don't ride the same
     overshoot spring as the transform (was `all <spring>`, which made
     every property wobble on hover and jitter on quick mouse sweeps).
     Transform uses a gentler overshoot than --spring so the lift settles
     cleanly within its window. */
  transition:
    transform 280ms cubic-bezier(0.22, 1.18, 0.36, 1),
    box-shadow 220ms var(--ease-out-expo),
    border-color 160ms var(--ease-in-out),
    background-color 160ms var(--ease-in-out);
  /* Slim top-edge highlight so the border catches light like the
     other glass surfaces. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  /* Fixed height so all cards match regardless of content */
  min-height: clamp(280px, 42vh, 420px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
}

.char-card:hover {
  transform: translateY(-8px) scale(1.06);
  z-index: 3;
  border-color: var(--glass-border-strong);
  background-color: rgba(255, 255, 255, 0.07);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 18px 44px rgba(0, 0, 0, 0.28);
}

.char-card:active:not(:has(.color-dot:active)) {
  transform: scale(0.96);
  transition: transform 90ms var(--ease-in-out);
}

/* Focus ring via box-shadow since outline is clipped by mask-image */
.char-card:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 164, 239, 0.45), 0 8px 24px rgba(0, 0, 0, 0.2);
  outline: none;
}

.char-card.selected {
  /* Bright accent ring + soft accent glow + a slight permanent lift,
     so the active card unmistakably reads as the one that will be
     played. The 2px border, accent-tinted top sheen, and outer glow
     stack make it pop even when the user is hovering a sibling. */
  border: 2px solid var(--accent);
  background-color: rgba(145, 222, 255, 0.14);
  background-image: linear-gradient(180deg, rgba(145, 222, 255, 0.18) 0%, rgba(145, 222, 255, 0.04) 60%);
  transform: translateY(-4px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.32),
    0 0 0 4px rgba(0, 164, 239, 0.18),
    0 0 28px rgba(145, 222, 255, 0.32),
    0 14px 36px rgba(0, 0, 0, 0.32);
  /* Compensate for the +1px border so layout doesn't shift. */
  padding: 13px 11px 15px;
}

/* Selected card stays selected-looking on hover — lift a touch more
   but keep the ring + glow so the active state remains the strongest
   signal on the grid. */
.char-card.selected:hover {
  transform: translateY(-10px) scale(1.06);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.38),
    0 0 0 4px rgba(0, 164, 239, 0.24),
    0 0 32px rgba(145, 222, 255, 0.4),
    0 22px 48px rgba(0, 0, 0, 0.36);
}

.char-card.selected::after {
  content: '✓';
  position: absolute;
  top: 8px;
  right: 10px;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-circle);
  background: var(--accent);
  color: #001a26;
  font-size: 13px;
  font-weight: 900;
  box-shadow: 0 2px 10px rgba(0, 164, 239, 0.5);
}

/* Locked Totodile card — heavily desaturated + dimmed so it's
 * unmistakably unavailable at a glance, gold-padlock chip in the
 * corner, shake animation when an attempt is made to select it. */
.char-card.locked {
  filter: grayscale(1) brightness(0.45) contrast(0.9);
  opacity: 0.55;
  cursor: not-allowed;
  border-color: rgba(255, 255, 255, 0.06);
  background-color: rgba(8, 10, 16, 0.35);
}

.char-card.locked:hover {
  transform: none;
  border-color: rgba(255, 210, 74, 0.45);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 210, 74, 0.25) inset;
  /* Brighten slightly on hover so the lock chip remains readable
     without the whole card popping back to full color. */
  filter: grayscale(1) brightness(0.6) contrast(0.9);
  opacity: 0.7;
}

.char-card.locked:active {
  transform: none;
}

.char-card.locked .char-name {
  color: rgba(255, 225, 150, 0.9);
  font-size: 13px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
}

.char-card__lock {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-circle);
  background: rgba(255, 210, 74, 0.18);
  border: 1px solid rgba(255, 210, 74, 0.55);
  color: #ffd24a;
  font-size: 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  z-index: 2;
}

.char-card.shake {
  animation: modePillShake 0.45s var(--spring) both;
}

/* 3D preview canvas */
.char-preview {
  width: 100%;
  height: clamp(160px, 26vh, 280px);
  display: block;
  border-radius: var(--r-md);
  margin-bottom: 8px;
  background: transparent;
  flex-shrink: 0;
}

.char-name {
  font-size: 16px;
  font-weight: var(--weight-bold);
  color: #fff;
  margin: 0;
  width: 100%;
  min-height: 1.25em;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1.2;
  letter-spacing: 0.01em;
}

.char-colors {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-top: 8px;
  min-height: 28px;
  visibility: visible;
}

.char-colors--spacer {
  pointer-events: none;
}

.color-dot {
  width: 24px;
  height: 24px;
  border-radius: var(--r-circle);
  border: 2px solid rgba(255, 255, 255, 0.2);
  cursor: pointer;
  transition: border-color var(--dur-med), box-shadow var(--dur-med);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  /* Easy hit target */
  padding: 0;
  margin: 0;
}

.color-dot:hover {
  border-color: rgba(255, 255, 255, 0.5);
}

.color-dot.on {
  border-color: var(--accent);
  box-shadow: 0 0 8px rgba(145, 222, 255, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3);
}

.char-start {
  padding: 16px 48px;
  font-size: 16px;
  font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
}

.char-start:active {
  transform: translateY(1px);
}

/* Movement-mode toggle (Normal / Speed / Skate) — sits between the grid and Start */
.mode-select {
  display: flex;
  gap: 12px;
  justify-content: center;
  align-items: stretch;
  margin: 4px 0 20px;
  flex-wrap: wrap;
}

.mode-pill {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 10px 34px;
  min-width: 140px;
  border-radius: 999px;
  border: 2px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.04);
  color: rgba(255, 255, 255, 0.78);
  font: inherit;
  font-weight: var(--weight-bold);
  font-size: 14px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: border-color var(--dur-med), color var(--dur-med),
    background var(--dur-med), box-shadow var(--dur-med),
    transform var(--dur-med);
}

.mode-pill i {
  font-size: 18px;
  line-height: 1;
}

.mode-pill__label {
  display: block;
  line-height: 1.1;
}

.mode-pill__sub {
  display: block;
  font-size: 11px;
  font-weight: var(--weight-med);
  letter-spacing: 0.02em;
  opacity: 0.7;
  text-transform: none;
}

.mode-pill:hover {
  border-color: rgba(255, 255, 255, 0.4);
  color: #fff;
}

.mode-pill.on {
  border-color: var(--accent);
  color: #fff;
  background: rgba(145, 222, 255, 0.12);
  box-shadow: 0 0 12px rgba(145, 222, 255, 0.35);
}

.mode-pill.on[data-mode="speed"] {
  border-color: #ffd24a;
  background: rgba(255, 210, 74, 0.14);
  box-shadow: 0 0 14px rgba(255, 179, 0, 0.45);
}

.mode-pill.on[data-mode="skate"] {
  border-color: #7af5c2;
  background: rgba(122, 245, 194, 0.14);
  box-shadow: 0 0 14px rgba(76, 230, 179, 0.4);
}

.mode-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mode-pill:active {
  transform: translateY(1px);
}

.mode-pill.locked {
  border-color: rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.025);
  color: rgba(255, 255, 255, 0.55);
  cursor: not-allowed;
  box-shadow: none;
}

.mode-pill.locked:hover {
  border-color: rgba(255, 210, 74, 0.45);
  color: rgba(255, 225, 150, 0.85);
  background: rgba(255, 210, 74, 0.05);
}

.mode-pill.locked .mode-pill__sub {
  color: #ffd24a;
  opacity: 0.95;
  font-weight: var(--weight-bold);
}

.mode-pill.locked i {
  color: rgba(255, 210, 74, 0.85);
}

@keyframes modePillShake {

  0%,
  100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-6px);
  }

  40% {
    transform: translateX(5px);
  }

  60% {
    transform: translateX(-3px);
  }

  80% {
    transform: translateX(2px);
  }
}

.mode-pill.shake {
  animation: modePillShake 0.36s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .mode-pill.shake {
    animation: none;
  }
}

body.play-mode #fpControls {
  display: block;
}

/* HUD entrance — when play-mode flips on (toggleFirstPerson enters
   FP), the top run-pill (.run-hud-center) and bottom charge bar
   (#fpChargeBar) animate in using the same Linear-style fade
   (translate + blur + opacity) used on the home / about / leaderboard
   pages — see the pageContentIn keyframe. The blur is what makes the
   motion legible: a bare opacity fade reads as "the chrome was always
   there"; the blur dropoff makes it clearly arrive.

   Both elements use `transform: translateX(-50%)` for centering, so
   the keyframes preserve that X translation while animating Y +
   opacity + filter. The class flip from absent → present re-runs the
   animation on every fresh entry into FP (re-runs included).

   The HUD pill is small relative to a hero card on the menu pages, so
   subtle Y/blur reads as "always there"; we use bigger travel (40px),
   longer duration (1.4s), and stronger blur (14px) than pageContentIn
   so the entrance lands. Top leads, bottom follows by 200ms. */
@keyframes fpHudTopIn {
  from {
    opacity: 0;
    transform: translate(-50%, -40px);
    filter: blur(14px);
  }

  to {
    opacity: 1;
    transform: translate(-50%, 0);
    filter: blur(0);
  }
}

@keyframes fpHudBottomIn {
  from {
    opacity: 0;
    transform: translate(-50%, 40px);
    filter: blur(14px);
  }

  to {
    opacity: 1;
    transform: translate(-50%, 0);
    filter: blur(0);
  }
}

body.play-mode .run-hud-center {
  animation: fpHudTopIn 1.4s 0ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

body.play-mode #fpChargeBar {
  animation: fpHudBottomIn 1.4s 200ms cubic-bezier(0.32, 0.72, 0, 1) both;
}

@media (prefers-reduced-motion: reduce) {

  body.play-mode .run-hud-center,
  body.play-mode #fpChargeBar {
    animation: none;
  }
}

/* Mobile — show touch controls in play mode */
@media (pointer: coarse) {
  body.play-mode #mobilePad {
    display: block !important;
  }

  body.play-mode #mobileJumpBtn {
    display: flex !important;
    align-items: center;
    justify-content: center;
  }

  body.play-mode #mobileExitBtn {
    display: flex !important;
    align-items: center;
    justify-content: center;
  }

  body.play-mode #fpControls {
    display: none !important;
  }

  body.play-mode .run-hud-center {
    top: 10px;
  }

  body.play-mode .run-pill {
    padding: 6px 12px;
    gap: 8px;
  }

  body.play-mode .run-pill--combo>#runTimerHud,
  body.play-mode .run-pill--combo>#coinHud {
    gap: 6px;
  }

  body.play-mode .run-pill--coins {
    font-size: 15px;
  }

  body.play-mode .run-pill--timer #runTimerText {
    font-size: 22px;
  }

  body.play-mode #mphValue {
    font-size: 20px;
  }

  body.play-mode .mph-unit {
    font-size: 9px;
  }

  body.play-mode #fps {
    top: 56px;
    right: 12px;
    font-size: 10px;
    padding: 3px 6px;
  }

  body.play-mode #fpShareRow {
    top: 56px;
    left: 12px;
    right: auto;
  }

  body.play-mode #fpChargeBar {
    width: min(94vw, 360px);
    bottom: 10px;
    padding: 9px 10px 8px;
  }

  body.play-mode #fpChargeHint {
    display: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   UTILITY — apply .bounce to trigger a bounce animation via JS
   ═══════════════════════════════════════════════════════════════════ */
.bounce {
  animation: uiBounce 0.45s var(--spring);
}

@keyframes uiBounce {
  0% {
    transform: scale(1);
  }

  35% {
    transform: scale(1.12);
  }

  70% {
    transform: scale(0.97);
  }

  100% {
    transform: scale(1);
  }
}

.shake {
  animation: uiShake 0.4s var(--spring);
}

@keyframes uiShake {

  0%,
  100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-4px);
  }

  40% {
    transform: translateX(4px);
  }

  60% {
    transform: translateX(-3px);
  }

  80% {
    transform: translateX(2px);
  }
}

.pop-in {
  animation: popIn 0.45s var(--spring) both;
}

@keyframes popIn {
  from {
    opacity: 0;
    transform: scale(0.7);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ═══════════════════════════════════════════════════════════════════
   GLASS SHINE — cursor-tracking via CSS custom properties
   JS sets --mx and --my on the button; CSS uses them in a gradient.
   No overlay divs needed — avoids backdrop-filter compositing issues.
   ═══════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════
   ACCESSIBILITY
   ═══════════════════════════════════════════════════════════════════ */

/* ─── Reduced motion ─────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ─── Focus-visible ring (keyboard-only, shared style) ───────────── */
/* Suppress all focus outlines on mouse/touch — only show on keyboard nav */
:focus:not(:focus-visible) {
  outline: none !important;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Buttons inside seg groups — inset ring to not break rounded corners */
.seg button:focus-visible {
  outline-offset: -2px;
}

/* Toggle switch focus ring — pairs the accent outline with a soft
   halo + lift so it reads clearly on the busy settings background.
   The toggle pill is small (36×20) and the surrounding rows are
   dense, so a plain 2px outline gets lost; the halo separates the
   focused row from its neighbors at a glance. */
.toggle-sw:focus-visible {
  outline: 2px solid #5cd8ff;
  outline-offset: 3px;
  box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, 0.12),
    0 0 0 6px rgba(92, 216, 255, 0.18),
    0 0 14px rgba(92, 216, 255, 0.35);
}

/* Row-level focus for controller-first settings navigation. The row
   is now the default focus stop; primary controls are entered with A
   / Enter and exited with B / Escape (for sliders). */
.pause-toggle-row:focus-visible {
  outline: 2px solid #5cd8ff;
  outline-offset: 2px;
  border-color: rgba(140, 200, 255, 0.55);
  background-color: rgba(255, 255, 255, 0.1);
}

/* Range slider focus — outline marks the track, but the real signal
   is the thumb growing + glowing so the user knows which slider will
   accept controller / arrow-key input. */
input[type=range]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.pause-range:focus-visible::-webkit-slider-thumb {
  background: #ffffff;
  border-color: rgba(92, 216, 255, 0.9);
  box-shadow:
    0 0 0 3px rgba(92, 216, 255, 0.32),
    0 0 12px rgba(92, 216, 255, 0.55),
    0 1px 4px rgba(0, 0, 0, 0.4);
  transform: scale(1.25);
}
.pause-range:focus-visible::-moz-range-thumb {
  background: #ffffff;
  border-color: rgba(92, 216, 255, 0.9);
  box-shadow:
    0 0 0 3px rgba(92, 216, 255, 0.32),
    0 0 12px rgba(92, 216, 255, 0.55),
    0 1px 4px rgba(0, 0, 0, 0.4);
  transform: scale(1.25);
}

/* Inline action buttons inside settings rows (e.g. Camera switcher).
   Match the gp-rebind-btn focus look so all interactive controls in
   the panel land on the same focus language. */
.pause-inline-btn:focus-visible {
  outline: 2px solid #5cd8ff;
  outline-offset: 2px;
  border-color: rgba(140, 200, 255, 0.55);
  background: rgba(255, 255, 255, 0.22);
}

/* Color dots — small, need tighter ring */
.color-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ─── Gamepad focus ring (always visible while pad is connected) ──
   public/spa-gamepad-nav.js drives focus via .focus({focusVisible})
   in response to controller input on the menu pages. Firefox honors
   the focusVisible option so :focus-visible matches automatically;
   Chromium ignores it and falls back to its heuristic, which can
   suppress the ring after a recent mouse click. body.has-gamepad is
   set the moment a pad is detected and lets us paint the ring on
   plain :focus too, so the controller user always sees where focus
   lives regardless of the prior input modality.

   !important is required to override the global
   :focus:not(:focus-visible) { outline:none !important } above —
   without it, Chromium's heuristic still wins on a focus() call
   that didn't originate from a real keydown. */
body.has-gamepad :focus {
  outline: 2px solid var(--accent) !important;
  outline-offset: 2px !important;
}
body.has-gamepad .seg button:focus { outline-offset: -2px !important; }
body.has-gamepad input[type=range]:focus { outline-offset: 4px !important; }
body.has-gamepad .color-dot:focus { outline-offset: 3px !important; }

/* ── Fireball / Kamehameha label swap ──────────────────────────────── */
/* The fireball hint contains two icon + label spans. Default state
   shows the 🔥 Fireball pair; when #fpChargeBar.ss-active is set, we
   hide that pair and show the ⚡ Kamehameha pair instead. The control
   bar (parent) gets the class, so we use the descendant selector. */
#fireballUnlockHint .fb-hint-icon-ss,
#fireballUnlockHint .fb-hint-label-ss {
  display: none;
}

#fpChargeBar.ss-active #fireballUnlockHint .fb-hint-icon {
  display: none;
}

#fpChargeBar.ss-active #fireballUnlockHint .fb-hint-label {
  display: none;
}

#fpChargeBar.ss-active #fireballUnlockHint .fb-hint-icon-ss {
  display: inline-block;
  font-size: 14px;
  color: #ffe680;
  filter: drop-shadow(0 0 6px rgba(255, 220, 130, 0.95));
  animation: fbHintSpark 0.9s ease-in-out infinite;
}

#fpChargeBar.ss-active #fireballUnlockHint .fb-hint-label-ss {
  display: inline;
  color: #fff4ca;
  text-shadow: 0 0 9px rgba(255, 220, 130, 0.85);
  letter-spacing: 0.04em;
}

#fpChargeBar.ss-active #fireballUnlockHint {
  color: #fff4ca;
  text-shadow: 0 0 8px rgba(255, 220, 130, 0.7);
}

#fpChargeBar.ss-active #fireballUnlockHint kbd {
  background: rgba(255, 220, 120, 0.22);
  border-color: rgba(255, 230, 150, 0.6);
  color: #fff8db;
}

@keyframes fbHintSpark {

  0%,
  100% {
    transform: scale(1) rotate(-2deg);
    filter: drop-shadow(0 0 6px rgba(255, 220, 130, 0.95));
  }

  50% {
    transform: scale(1.18) rotate(3deg);
    filter: drop-shadow(0 0 12px rgba(255, 245, 180, 1));
  }
}

/* ── Electric lightning overlay on the bottom HUD bar ──────────────── */
/* A handful of pre-positioned bolts and sparks living inside an
   absolute-positioned overlay. They're invisible by default; when the
   parent goes ss-active they fade in and run staggered flicker
   animations. Each bolt is a thin diagonal CSS gradient — cheap and
   doesn't require an SVG asset. Sparks are tiny radial blobs.

   Bolt widths are percentages of the overlay (= container) so the
   sparks scale with the bar — which itself shrinks/grows depending on
   whether skate trick hints, the SS row, etc. are visible. The
   container also clips so bolts can't shoot out the side. */
#fpBarLightning {
  position: absolute;
  inset: -8px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s ease-out;
  overflow: hidden;
  border-radius: 16px;
  z-index: 1;
}

#fpChargeBar.ss-active #fpBarLightning {
  opacity: 1;
}

/* Inner content above the lightning. The HUD layout is flow-based, so
   we lift the children above the absolute overlay by giving them a
   stacking context. */
#fpChargeBar>*:not(#fpBarLightning) {
  position: relative;
  z-index: 2;
}

/* Bolt = thin angled streak built from a clipped gradient. Yellow/gold
   to match the Super Saiyan aura. Width is set per-bolt as a percentage
   so they scale with whatever height the HUD bar happens to be. */
#fpBarLightning .bolt {
  position: absolute;
  height: 3px;
  background: linear-gradient(90deg,
      transparent 0%,
      rgba(255, 220, 110, 0.0) 8%,
      rgba(255, 240, 170, 0.95) 38%,
      #fffbe2 50%,
      rgba(255, 240, 170, 0.95) 62%,
      rgba(255, 220, 110, 0.0) 92%,
      transparent 100%);
  filter: drop-shadow(0 0 7px rgba(255, 215, 110, 0.95)) drop-shadow(0 0 14px rgba(255, 180, 60, 0.7));
  border-radius: 2px;
  opacity: 0;
  transform-origin: center;
}

/* Six bolts at different positions / angles / phases. Widths are
   percentages of the container so they always fit, with a max-width
   cap so they don't get absurdly long on wide layouts. Each uses its
   own keyframe with a different cycle length so flickers never sync. */
#fpBarLightning .bolt-1 {
  top: 8%;
  left: 2%;
  width: 38%;
  max-width: 180px;
  transform: rotate(-22deg);
  animation: hudBoltA 2.3s steps(1, end) infinite;
  animation-delay: 0s;
}

#fpBarLightning .bolt-2 {
  top: 4%;
  left: 24%;
  width: 28%;
  max-width: 140px;
  transform: rotate(18deg);
  animation: hudBoltB 1.9s steps(1, end) infinite;
  animation-delay: 0.55s;
}

#fpBarLightning .bolt-3 {
  top: 50%;
  left: 38%;
  width: 46%;
  max-width: 220px;
  transform: rotate(-8deg);
  animation: hudBoltC 2.7s steps(1, end) infinite;
  animation-delay: 1.1s;
}

#fpBarLightning .bolt-4 {
  bottom: 6%;
  right: 8%;
  width: 34%;
  max-width: 170px;
  transform: rotate(-30deg);
  animation: hudBoltD 2.1s steps(1, end) infinite;
  animation-delay: 0.3s;
}

#fpBarLightning .bolt-5 {
  bottom: 10%;
  left: 8%;
  width: 24%;
  max-width: 120px;
  transform: rotate(28deg);
  animation: hudBoltE 1.7s steps(1, end) infinite;
  animation-delay: 1.25s;
}

#fpBarLightning .bolt-6 {
  top: 12%;
  right: 4%;
  width: 42%;
  max-width: 200px;
  transform: rotate(12deg);
  animation: hudBoltF 2.45s steps(1, end) infinite;
  animation-delay: 0.75s;
}

/* Sparks = small radial dots that pop and fade like static crackle. */
#fpBarLightning .spark {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: radial-gradient(circle,
      #fffbe2 0%,
      rgba(255, 240, 170, 0.95) 35%,
      rgba(255, 200, 80, 0.5) 65%,
      transparent 100%);
  filter: drop-shadow(0 0 6px rgba(255, 215, 110, 0.9));
  opacity: 0;
}

#fpBarLightning .spark-a {
  top: 6%;
  left: 36%;
  animation: hudSparkA 1.8s steps(1, end) infinite;
  animation-delay: 0.1s;
}

#fpBarLightning .spark-b {
  bottom: 8%;
  right: 28%;
  animation: hudSparkB 2.4s steps(1, end) infinite;
  animation-delay: 0.85s;
}

#fpBarLightning .spark-c {
  top: 38%;
  right: 6%;
  animation: hudSparkC 2.05s steps(1, end) infinite;
  animation-delay: 1.4s;
}

/* Each bolt gets its own irregular flicker pattern. Different cycle
   lengths + different on-times make the whole thing feel chaotic. */
@keyframes hudBoltA {

  0%,
  11% {
    opacity: 0;
  }

  12%,
  16% {
    opacity: 1;
  }

  17%,
  38% {
    opacity: 0;
  }

  39%,
  41% {
    opacity: 0.85;
  }

  42%,
  73% {
    opacity: 0;
  }

  74%,
  77% {
    opacity: 0.95;
  }

  78%,
  100% {
    opacity: 0;
  }
}

@keyframes hudBoltB {

  0%,
  24% {
    opacity: 0;
  }

  25%,
  27% {
    opacity: 1;
  }

  28%,
  55% {
    opacity: 0;
  }

  56%,
  59% {
    opacity: 0.9;
  }

  60%,
  100% {
    opacity: 0;
  }
}

@keyframes hudBoltC {

  0%,
  7% {
    opacity: 0;
  }

  8%,
  13% {
    opacity: 1;
  }

  14%,
  47% {
    opacity: 0;
  }

  48%,
  51% {
    opacity: 0.8;
  }

  52%,
  82% {
    opacity: 0;
  }

  83%,
  86% {
    opacity: 1;
  }

  87%,
  100% {
    opacity: 0;
  }
}

@keyframes hudBoltD {

  0%,
  18% {
    opacity: 0;
  }

  19%,
  22% {
    opacity: 0.95;
  }

  23%,
  64% {
    opacity: 0;
  }

  65%,
  68% {
    opacity: 0.85;
  }

  69%,
  91% {
    opacity: 0;
  }

  92%,
  95% {
    opacity: 1;
  }

  96%,
  100% {
    opacity: 0;
  }
}

@keyframes hudBoltE {

  0%,
  33% {
    opacity: 0;
  }

  34%,
  37% {
    opacity: 1;
  }

  38%,
  70% {
    opacity: 0;
  }

  71%,
  74% {
    opacity: 0.9;
  }

  75%,
  100% {
    opacity: 0;
  }
}

@keyframes hudBoltF {

  0%,
  4% {
    opacity: 0;
  }

  5%,
  9% {
    opacity: 0.85;
  }

  10%,
  42% {
    opacity: 0;
  }

  43%,
  46% {
    opacity: 1;
  }

  47%,
  79% {
    opacity: 0;
  }

  80%,
  83% {
    opacity: 0.9;
  }

  84%,
  100% {
    opacity: 0;
  }
}

@keyframes hudSparkA {

  0%,
  28% {
    opacity: 0;
    transform: scale(0.6);
  }

  29%,
  33% {
    opacity: 1;
    transform: scale(1.3);
  }

  34%,
  67% {
    opacity: 0;
    transform: scale(0.8);
  }

  68%,
  71% {
    opacity: 0.85;
    transform: scale(1.1);
  }

  72%,
  100% {
    opacity: 0;
    transform: scale(0.6);
  }
}

@keyframes hudSparkB {

  0%,
  41% {
    opacity: 0;
    transform: scale(0.5);
  }

  42%,
  46% {
    opacity: 1;
    transform: scale(1.4);
  }

  47%,
  88% {
    opacity: 0;
    transform: scale(0.7);
  }

  89%,
  92% {
    opacity: 0.9;
    transform: scale(1.2);
  }

  93%,
  100% {
    opacity: 0;
    transform: scale(0.6);
  }
}

@keyframes hudSparkC {

  0%,
  14% {
    opacity: 0;
    transform: scale(0.6);
  }

  15%,
  19% {
    opacity: 0.95;
    transform: scale(1.25);
  }

  20%,
  56% {
    opacity: 0;
    transform: scale(0.7);
  }

  57%,
  60% {
    opacity: 1;
    transform: scale(1.35);
  }

  61%,
  100% {
    opacity: 0;
    transform: scale(0.6);
  }
}

/* Subtle whole-bar gold shimmer on top of the existing SS glow. */
#fpChargeBar.ss-active::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(120deg,
      transparent 30%,
      rgba(255, 230, 130, 0.08) 50%,
      transparent 70%);
  opacity: 0;
  animation: hudCrackle 3.1s steps(1, end) infinite;
  z-index: 1;
}

@keyframes hudCrackle {

  0%,
  19% {
    opacity: 0;
  }

  20%,
  23% {
    opacity: 1;
  }

  24%,
  47% {
    opacity: 0;
  }

  48%,
  50% {
    opacity: 0.7;
  }

  51%,
  81% {
    opacity: 0;
  }

  82%,
  85% {
    opacity: 0.9;
  }

  86%,
  100% {
    opacity: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   OVERCHARGE DEATH OVERLAY — appears when player holds the red
   kamehameha past the danger window. Same frosted-glass language as
   the pause overlay but with a red eyebrow + glow to read as fatal.
   ═══════════════════════════════════════════════════════════════════ */
#fpDeathOverlay {
  display: none;
  position: fixed;
  inset: 0;
  background:
    radial-gradient(ellipse at center, rgba(120, 10, 0, 0.45) 0%, rgba(0, 0, 0, 0.55) 60%);
  z-index: 460;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(14px) saturate(0.85);
  animation: fadeIn 0.35s var(--ease-out-expo) both;
}

.death-card {
  background-color: var(--glass);
  background-image: var(--glass-specular);
  border: 1px solid rgba(255, 90, 60, 0.35);
  border-radius: var(--r-xl);
  padding: 32px 40px 30px;
  max-width: 480px;
  text-align: center;
  box-shadow:
    0 20px 60px rgba(0, 0, 0, 0.55),
    0 0 80px rgba(220, 30, 10, 0.25);
  backdrop-filter: blur(18px) saturate(1.05);
  animation: deathCardIn 0.45s var(--ease-out-expo) both;
}

@keyframes deathCardIn {
  from {
    opacity: 0;
    transform: scale(0.92) translateY(8px);
  }

  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

.death-eyebrow {
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #ff6b4d;
  font-weight: 700;
  margin-bottom: 6px;
  text-shadow: 0 0 12px rgba(255, 60, 30, 0.6);
}

.death-title {
  margin: 0 0 10px;
  font-size: 28px;
  line-height: 1.2;
  color: #fff;
  letter-spacing: 0.01em;
  text-shadow: 0 2px 12px rgba(255, 70, 40, 0.35);
}

.death-sub {
  margin: 0 0 22px;
  color: rgba(255, 230, 220, 0.82);
  font-size: 15px;
  line-height: 1.45;
}

.death-btns {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: stretch;
}

.death-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  font-size: 15px;
  font-weight: 600;
}

.death-btn--primary {
  /* Slight red tint over the standard primary glass button. */
  box-shadow: 0 0 24px rgba(255, 90, 50, 0.35);
}

@media (max-width: 480px) {
  .death-card {
    padding: 24px 22px 22px;
    max-width: 92vw;
  }

  .death-title {
    font-size: 22px;
  }
}