:root {
  --bg: #07070b;
  --bg-2: #0f0f17;
  --ink: #e8e8f0;
  --ink-strong: #ffffff;
  --dim: #6c6c80;
  --accent: #ffd84d;
  --grid-bg: #101019;
  --grid-border: #1a1a24;
  --tile-bg: #18181f;
  --tile-blank-bg: #0c0c12;
  --logo-cell: #d8d8e2;
  --panel-bg: #11111a;
  --panel-border: #2a2a38;
  --row-bg: #1a1a26;
  --hover-bg: #1a1a26;
  --kbd-bg: #1a1a26;
  --kbd-system-bg: #11111c;
  --overlay-bg: rgba(7, 7, 11, 0.78);
  --scene-overlay-bg: rgba(7, 7, 11, 0.82);
  --pause-btn-bg: rgba(7, 7, 11, 0.6);
  --pause-btn-border: rgba(255, 255, 255, 0.18);
  /* Scrollbar tokens — used by .mp-chat-body and any other scrollable
     panel that wants theme-aware bars. Light pip on dark theme. */
  --scrollbar-thumb: rgba(255, 255, 255, 0.18);
  --scrollbar-thumb-hover: rgba(255, 255, 255, 0.32);
  --cols: 95;
  --rows: 95;
  --tile: min(
    calc((100vw - 32px) / var(--cols)),
    calc((100dvh - 110px) / var(--rows))
  );
}

body.theme-light {
  --bg: #f6f6f9;
  --bg-2: #ffffff;
  --ink: #1a1a26;
  --ink-strong: #07070b;
  --dim: #6c6c80;
  --accent: #b88c00;
  --grid-bg: #e9e9ee;
  --grid-border: #cfcfd6;
  --tile-bg: #ffffff;
  --tile-blank-bg: #ededf2;
  --panel-bg: #ffffff;
  --panel-border: #d4d4dc;
  --row-bg: #f0f0f4;
  --hover-bg: #ededf2;
  --kbd-bg: #ededf2;
  --kbd-system-bg: #f5f5fa;
  --overlay-bg: rgba(246, 246, 249, 0.85);
  --scene-overlay-bg: rgba(246, 246, 249, 0.9);
  --pause-btn-bg: rgba(255, 255, 255, 0.85);
  --pause-btn-border: rgba(0, 0, 0, 0.18);
  --logo-cell: #1f1f2c;
  /* Dark pip on light theme — flips the scrollbar contrast direction
     so it stays visible against pale backgrounds. */
  --scrollbar-thumb: rgba(0, 0, 0, 0.22);
  --scrollbar-thumb-hover: rgba(0, 0, 0, 0.40);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

body.playing {
  overflow: hidden;
  position: fixed;
  inset: 0;
}
body.playing #home {
  display: none;
}
/* During play we keep the topbar visible (brand + theme toggle stay in
   reach) and dock the stage right below it so the canvas takes up
   every remaining pixel. */

#app {
  height: 100vh;
  height: 100dvh;
  display: flex; flex-direction: column;
}

#topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 18px;
  height: 53px;
  border-bottom: 1px solid var(--grid-border);
  flex-shrink: 0;
  box-sizing: border-box;
  overflow: hidden;
}
#brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: inherit;
  min-width: 0;
}
.logo {
  width: 28px;
  height: 28px;
  display: block;
  flex-shrink: 0;
}
/* Two-variant logo: `logo-light.svg` has dark tiles (used on light
 * theme), `logo-dark.svg` has white tiles (used on dark theme). The
 * theme system only adds `theme-light` to <body> — dark is the
 * default-styled absence of that class. Show the dark-tile logo by
 * default, swap to the light-tile logo when `theme-light` is set.
 * Both <img>s render at the same size so the brand row doesn't
 * reflow on theme change. */
.logo-light { display: none; }
.logo-dark  { display: block; }
body.theme-light .logo-light { display: block; }
body.theme-light .logo-dark  { display: none; }
#brand:hover .logo,
#brand:focus-visible .logo {
  /* Subtle lift on hover so the brand still reacts to focus now
   * that the logo no longer has a coloured accent tile. */
  filter: brightness(1.08);
}
#theme-toggle {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--dim);
  width: 35px;
  height: 35px;
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  flex-shrink: 0;
}
#theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
#theme-toggle svg { width: 18px; height: 18px; display: block; }
#theme-toggle .icon-sun { display: none; }
#theme-toggle .icon-moon { display: block; }
body.theme-light #theme-toggle .icon-sun { display: block; }
body.theme-light #theme-toggle .icon-moon { display: none; }
#topbar h1 {
  margin: 0;
  font-size: 16px;
  letter-spacing: 0.18em;
  font-weight: 600;
  color: var(--ink-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#home {
  flex: 1;
  display: flex; flex-direction: column;
  padding: 0;
  gap: 0;
  min-height: 0;
  position: relative; /* anchor for #grid-wants overlay */
}
#home[hidden] { display: none; }
#grid-canvas {
  flex: 1;
  width: 100%;
  height: 100%;
  display: block;
  background: var(--bg);
  cursor: pointer;
  min-height: 0;
}

#grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), var(--tile));
  grid-template-rows: repeat(var(--rows), var(--tile));
  gap: 1px;
  /* The empty rows have no DOM — this color shows through where the
     grid is unpopulated, plus through the 1px gaps between real tiles. */
  background: var(--tile-blank-bg);
  padding: 4px;
  border: 1px solid var(--grid-border);
  border-radius: 4px;
  overflow: visible;
}

.tile {
  width: var(--tile);
  height: var(--tile);
  background-color: var(--tile-bg);
  background-image: var(--atlas-url);
  background-repeat: no-repeat;
  background-size: calc(var(--atlas-cols) * var(--tile)) calc(var(--atlas-rows) * var(--tile));
  background-position: calc(var(--col, 0) * var(--tile) * -1) calc(var(--row, 0) * var(--tile) * -1);
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  cursor: pointer;
  border: 0;
  padding: 0;
  position: relative;
  transition: transform 80ms ease, outline-color 80ms ease, opacity 120ms ease, filter 120ms ease;
  outline: 1px solid transparent;
  outline-offset: -1px;
  opacity: 0.18;
  filter: grayscale(0.7) brightness(0.7);
  text-decoration: none;
}
.tile.real {
  opacity: 1;
  filter: none;
  box-shadow: 0 0 0 1px rgba(255, 216, 77, 0.65), 0 0 8px rgba(255, 216, 77, 0.35);
  z-index: 1;
  animation: realPulse 2.4s ease-in-out infinite;
}
body.theme-light .tile { opacity: 0.55; filter: grayscale(0.4); }
body.theme-light .tile.real { opacity: 1; filter: none; }
.tile.blank:hover {
  transform: none;
  opacity: 0.32;
  outline-color: transparent;
}
@keyframes realPulse {
  0%, 100% { box-shadow: 0 0 0 1px rgba(255, 216, 77, 0.55), 0 0 6px rgba(255, 216, 77, 0.25); }
  50%      { box-shadow: 0 0 0 1px rgba(255, 216, 77, 0.85), 0 0 12px rgba(255, 216, 77, 0.55); }
}
.tile:hover {
  transform: scale(1.8);
  z-index: 3;
  outline-color: var(--accent);
  opacity: 1;
  filter: none;
}

#hint {
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-align: center;
  flex-shrink: 0;
  padding: 4px 0;
}

#tooltip {
  position: fixed;
  background: var(--panel-bg);
  color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 4px 8px;
  font-size: 12px;
  border-radius: 3px;
  pointer-events: none;
  z-index: 50;
  white-space: nowrap;
  transform: translate(-50%, calc(-100% - 8px));
}
#tooltip .num { color: var(--dim); margin-left: 6px; }
#tooltip.real .name { color: var(--accent); }

/* Game stage — an in-flow flex child that fills the rest of #app
   below the topbar + game-hud. Was previously position:fixed which on
   mobile Safari left a gap between the topbar and the HUD; making
   stage and game-hud both regular flex children stacks them with no
   browser-chrome surprises. */
#stage {
  flex: 1;
  min-height: 0;
  position: relative;
  background: var(--bg);
  overflow: hidden;
}
#stage[hidden] { display: none; }

#stage-host {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
#stage-host canvas {
  display: block;
  max-width: 100%;
  max-height: 100%;
  touch-action: none;
}

/* Party "wants-to-play" overlay — DOM siblings positioned absolutely
 * over individual game tiles on the home grid. Each .gw-tile sits on
 * top of one cell, draws a coloured ring around the card, and stacks
 * up to 3 avatars in the bottom-right corner (extra count collapses
 * into a "+N" badge). Pointer-events stay disabled so clicks fall
 * through to the underlying canvas hit-test. */
#grid-wants {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}
.gw-tile {
  position: absolute;
  border-radius: 8px;
  pointer-events: none;
  box-sizing: border-box;
  /* Soft glow + crisp 2px border in the wanter's colour. */
  box-shadow: 0 0 0 2px var(--gw-color, #fff),
              0 0 18px 2px var(--gw-glow, rgba(255, 255, 255, 0.45));
}
.gw-avatars {
  position: absolute;
  right: 4px;
  bottom: 4px;
  display: flex;
  flex-direction: row-reverse; /* newest first */
  align-items: flex-end;
}
.gw-avatars .mp-avatar {
  width: 22px;
  height: 22px;
  font-size: 10px;
  margin-left: -7px;          /* overlap stack */
  border: 2px solid var(--bg);
}
.gw-avatars .gw-more {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--dim);
  color: var(--bg);
  font: 10px ui-monospace, "SF Mono", Menlo, monospace;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: -7px;
  border: 2px solid var(--bg);
}
/* On desktop the home tiles are much bigger, so the 22-px mobile
   avatar disappears in the corner. Double the badge + matching
   stack overlap so the "X wants to play this" cue actually reads
   across the room. Pointer is presumed when the viewport is wide
   AND coarse-pointer is absent, so we also gate on min-width to
   avoid blowing the chip up on a large phone in landscape. */
@media (min-width: 720px) {
  .gw-avatars { right: 8px; bottom: 8px; }
  .gw-avatars .mp-avatar,
  .gw-avatars .gw-more {
    width: 44px;
    height: 44px;
    font-size: 16px;
    margin-left: -12px;
    border-width: 3px;
  }
}

/* In-game HUD — an absolutely-positioned, transparent overlay that
   floats above the game canvas. The game's own background shows
   through the row, so the HUD reads against whatever the game is
   The HUD itself lives entirely on the Phaser canvas — no DOM, no
   SVG, no per-chip elements. The framework draws score / mult / level
   / phase / lives / timer at fixed positions in the game's own scene,
   and centre overlays the same way. CSS for the HUD is gone. */
@media (max-width: 540px) {
  #game-hud { gap: 6px; padding: 0 6px; }
  #hud-right { gap: 4px; }
}

/* Canvas-overlay text zones (gameCenterTopHUD / gameCenterCenterHUD /
   gameCenterBottomHUD). Sit over the Phaser canvas, do NOT consume
   layout space — used for big banners / phase callouts / multiplier
   pops / "TAP NOW" calls. Each zone has a `.canvas-text-main` line
   plus an optional `.canvas-text-sub` line below. */
.canvas-text {
  position: absolute;
  left: 0; right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 105;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  letter-spacing: 0.06em;
  text-align: center;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
  transition: opacity 220ms ease;
}
.canvas-text-main { font: inherit; line-height: 1.05; }
.canvas-text-sub  {
  margin-top: 6px;
  font-size: 0.45em;
  font-weight: 500;
  letter-spacing: 0.10em;
  opacity: 0.85;
  text-transform: uppercase;
}
.canvas-text-sub:empty { display: none; }
#canvas-top    { top: 8%;    font-size: 28px; font-weight: 700; }
#canvas-center { top: 50%;   font-size: 40px; font-weight: 800; transform: translateY(-50%); }
#canvas-bottom { bottom: 6%; font-size: 16px; font-weight: 500; }
.canvas-text[hidden] { display: none; }
.canvas-text.size-small  { font-size: 14px; }
.canvas-text.size-medium { font-size: 22px; }
.canvas-text.size-large  { font-size: 36px; }
.canvas-text.size-huge   { font-size: 48px; font-weight: 800; }
.canvas-text.size-mega   { font-size: 72px; font-weight: 900; letter-spacing: 0.04em; }

/* Animation effects for multiplier-style canvas text. Games stack any
   combination via the `effects` field in setOptions(). */
.canvas-text.pulse        { animation: canvasTextPulse 0.7s ease-in-out infinite; }
.canvas-text.bounce       { animation: canvasTextBounce 0.6s ease-in-out infinite; }
.canvas-text.shake        { animation: canvasTextShake 0.18s ease-in-out infinite; }
.canvas-text.pop          { animation: canvasTextPop 0.45s cubic-bezier(.2,.9,.3,1.4) forwards; }
.canvas-text.glow         { text-shadow: 0 0 6px currentColor, 0 0 14px currentColor, 0 2px 4px rgba(0,0,0,0.4); }
.canvas-text.rainbow      { animation: canvasTextRainbow 1.6s linear infinite; }
.canvas-text.tilt-shimmer .canvas-text-main {
  background-clip: text; -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  background-image: linear-gradient(110deg, #ff5f6f, #ffae73, #ffd84d, #5fcf5f, #6cd0ff, #9d8df1, #ff5f6f);
  background-size: 300% 100%;
  animation: canvasTextShimmer 2.4s linear infinite;
}
/* Center zone keeps its translate(-50%) y-centering — combine its
   per-state transform animation with the centering using composition. */
#canvas-center.pulse  { animation: canvasTextPulseCentered 0.7s ease-in-out infinite; }
#canvas-center.bounce { animation: canvasTextBounceCentered 0.6s ease-in-out infinite; }
#canvas-center.pop    { animation: canvasTextPopCentered 0.45s cubic-bezier(.2,.9,.3,1.4) forwards; }

@keyframes canvasTextPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}
@keyframes canvasTextBounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}
@keyframes canvasTextShake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-4px); }
  75%      { transform: translateX( 4px); }
}
@keyframes canvasTextPop {
  0%   { transform: scale(0.4); opacity: 0; }
  60%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes canvasTextRainbow {
  0%   { color: #ff5f6f; }
  16%  { color: #ffae73; }
  33%  { color: #ffd84d; }
  50%  { color: #5fcf5f; }
  66%  { color: #6cd0ff; }
  83%  { color: #9d8df1; }
  100% { color: #ff5f6f; }
}
@keyframes canvasTextShimmer {
  0%   { background-position: 0% 50%; }
  100% { background-position: 300% 50%; }
}

/* Centered variants: keep the -50% Y offset that #canvas-center uses. */
@keyframes canvasTextPulseCentered {
  0%, 100% { transform: translateY(-50%) scale(1); }
  50%      { transform: translateY(-50%) scale(1.08); }
}
@keyframes canvasTextBounceCentered {
  0%, 100% { transform: translateY(-50%); }
  50%      { transform: translateY(calc(-50% - 10px)); }
}
@keyframes canvasTextPopCentered {
  0%   { transform: translateY(-50%) scale(0.4); opacity: 0; }
  60%  { transform: translateY(-50%) scale(1.20); opacity: 1; }
  100% { transform: translateY(-50%) scale(1); opacity: 1; }
}

/* Topbar action cluster (pause + global settings + theme). Pause is
   hidden on the home grid and revealed during play via body.playing;
   the global-settings gear is the inverse — visible on home only.
   They sit in the same slot visually but never both at once. */
#topbar-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
#pause-btn,
#reset-btn,
#global-settings-btn {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--dim);
  width: 35px;
  height: 35px;
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
/* The `display: inline-flex` rule above out-specifies the
   user-agent's `[hidden] { display: none }` default (id vs.
   attribute selector), which made setting `pauseBtn.hidden = true`
   in JS a no-op — the button stayed visible on the home grid even
   though main.js had hidden it on every unmount. Restore the
   expected hidden-attribute behaviour with a matching-specificity
   rule. */
#pause-btn[hidden],
#reset-btn[hidden],
#global-settings-btn[hidden] { display: none; }
#pause-btn:hover,
#reset-btn:hover,
#global-settings-btn:hover { color: var(--accent); border-color: var(--accent); }
#pause-btn svg,
#reset-btn svg,
#global-settings-btn svg { width: 18px; height: 18px; display: block; }

/* Home-only visibility for the gear. body.playing toggles on at game
   mount (see main.js `mountGame`) so this rule hides the button the
   moment a game starts and brings it back on unmount. */
body.playing #global-settings-btn { display: none; }

/* MP boot loader — shown while we wait for the first authoritative
 * state from the server after mounting/refreshing into a multiplayer
 * game. Without this, the player briefly sees the game's local-default
 * world (e.g. the play area for Scoop) before the server state arrives
 * and "snaps" to the real situation (e.g. shop overlay). Showing the
 * brand mark with a wave of pulsing grid cells fills that gap.
 *
 * Sits above the canvas and any controls; below scene-overlay (110)
 * so a game-over modal could in principle render on top, but in
 * practice this disappears as soon as the first state arrives. */
.mp-boot-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  background: var(--scene-overlay-bg, rgba(7, 7, 11, 0.96));
  color: var(--ink);
  z-index: 109;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  animation: mp-boot-fade-in 200ms ease-out both;
}
@keyframes mp-boot-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.mp-boot-overlay .mp-boot-logo {
  width: 180px;
  height: 180px;
  /* The wordmark is built from many small squares. Apply an alpha
   * mask that's mostly dim with a brighter diagonal band, then
   * animate the mask-position so the band sweeps across the artwork.
   * Each pixel-square of the logo brightens as the band crosses it,
   * giving the same "wave of pulses" feel as the old separate grid
   * but using the logo's own structure. */
  -webkit-mask-image: linear-gradient(135deg,
    rgba(0,0,0,0.20) 0%,
    rgba(0,0,0,0.20) 35%,
    rgba(0,0,0,1.00) 50%,
    rgba(0,0,0,0.20) 65%,
    rgba(0,0,0,0.20) 100%);
          mask-image: linear-gradient(135deg,
    rgba(0,0,0,0.20) 0%,
    rgba(0,0,0,0.20) 35%,
    rgba(0,0,0,1.00) 50%,
    rgba(0,0,0,0.20) 65%,
    rgba(0,0,0,0.20) 100%);
  -webkit-mask-size: 260% 260%;
          mask-size: 260% 260%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  animation: mp-boot-logo-wave 1.8s ease-in-out infinite;
}
@keyframes mp-boot-logo-wave {
  from { -webkit-mask-position:   0%   0%; mask-position:   0%   0%; }
  to   { -webkit-mask-position: 100% 100%; mask-position: 100% 100%; }
}
.mp-boot-overlay .mp-boot-label {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: lowercase;
  color: var(--dim, #8a8aa0);
  margin: 0;
}

/* ---------------------------------------------------------------------
 * Fullscreen "tap to start" intro splash, shown on every game launch
 * (SP + MP) as a pause-gate. A tap / click / key anywhere dismisses it
 * and starts (or resumes) the game. Sits above everything game-related,
 * including the MP boot loader. The hero is the game's own animated
 * home-grid icon (drawn by showIntroOverlay); bespoke per-game intro
 * animations can replace it game-by-game later.
 * ------------------------------------------------------------------- */
.eg-intro {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--scene-overlay-bg, rgba(7, 7, 11, 0.96));
  color: var(--ink);
  z-index: 120;
  cursor: pointer;
  overflow: hidden;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  animation: mp-boot-fade-in 220ms ease-out both;
  -webkit-user-select: none;
          user-select: none;
  touch-action: none;
}
/* Hero: the game's own animated home-grid icon, rendered large above the
 * title by showIntroOverlay's rAF loop. The default (non-custom) intro
 * shows it at 2× the original size. */
.eg-intro-hero {
  width: clamp(240px, 52vmin, 400px);
  height: clamp(240px, 52vmin, 400px);
  margin-bottom: 4px;
  image-rendering: auto;
  pointer-events: none;
}
.eg-intro-content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  padding: 28px;
  max-width: min(620px, 94vw);
}
.eg-intro-title {
  margin: 0;
  font-size: clamp(28px, 7vmin, 52px);
  letter-spacing: 0.04em;
  color: var(--ink-strong, #fff);
}
.eg-intro-howto {
  margin: 0;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dim, #8a8aa0);
}
.eg-intro-controls {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}
.eg-intro-ctrl {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
}
/* The keys column — a right-aligned cluster of key-caps / input icons
 * (built by renderControlKeys). */
.eg-intro-keys {
  flex: 0 0 auto;
  min-width: 42%;
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
}
.eg-intro-act {
  flex: 1 1 auto;
  text-align: left;
  font-size: clamp(14px, 2.4vmin, 18px);
  color: var(--ink, #e8e8f0);
}
/* Intro-sized key-caps + input icons — a larger take on the settings-menu
 * `.gs-key` styling (which is scoped to `.gs-controls`). */
.eg-intro .gs-ctrl-keys {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
}
.eg-intro kbd.gs-key,
.eg-intro .gs-key {
  background: var(--kbd-bg);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 4px 9px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: clamp(13px, 2vmin, 17px);
  color: var(--ink);
  letter-spacing: 0.04em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  min-height: 30px;
  line-height: 1;
  box-shadow: inset 0 -1.5px 0 var(--panel-border);
}
.eg-intro .gs-key-icon { padding: 4px 7px; }
.eg-intro .gs-key-icon svg { display: block; width: 20px; height: 20px; color: var(--ink); }
.eg-intro .gs-key-mouse svg { width: 17px; height: 20px; }
.eg-intro .gs-ctrl-sep { color: var(--dim); font-size: 14px; padding: 0 2px; }
.eg-intro-start {
  margin: 8px 0 0;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: clamp(13px, 2.2vmin, 16px);
  letter-spacing: 0.2em;
  text-transform: lowercase;
  color: var(--ink, #e8e8f0);
  animation: eg-intro-pulse 1.4s ease-in-out infinite;
}
@keyframes eg-intro-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}

/* shared overlay used by Phaser scenes (DOM, sits over the canvas) */
.scene-overlay {
  position: absolute;
  inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 10px;
  background: var(--scene-overlay-bg);
  color: var(--ink);
  text-align: center;
  padding: 24px;
  /* Higher than #stage-controls (108) so the touch dpad can't render
   * over the resume / again buttons even if it didn't get hidden. */
  z-index: 110;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

/* Vortex post-wave upgrade chooser. Three cards across desktop, stacks
 * vertically on narrow phones. Above the touch controls (z-index 108)
 * so the dpad can't eat clicks on the cards. */
.vortex-chooser {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--scene-overlay-bg, rgba(7,7,11,0.82));
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: 115;
  padding: 24px;
}
.vortex-chooser-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: min(900px, 96vw);
}
.vortex-chooser-title {
  margin: 0;
  font-size: 22px;
  letter-spacing: 0.15em;
  color: var(--ink-strong, #fff);
}
.vortex-chooser-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  width: 100%;
}
.vortex-chooser-card {
  background: var(--panel-bg, #14141f);
  border: 2px solid var(--panel-border, #2a2a38);
  border-radius: 12px;
  padding: 18px 16px;
  color: var(--ink, #e8e8f0);
  text-align: left;
  cursor: pointer;
  font: inherit;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: transform 120ms ease, box-shadow 140ms ease;
}
.vortex-chooser-card:hover,
.vortex-chooser-card:focus-visible {
  transform: translateY(-3px);
  outline: none;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.vortex-chooser-icon {
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 2px;
  /* Soft glow so the icon stays legible on the dark panel without
   * fighting the card's coloured border. The shadow follows the
   * SVG's stroke colour (currentColor) so it reads as a halo in the
   * tree's tint, not a flat grey blob. */
  filter: drop-shadow(0 0 6px currentColor);
}
.vortex-chooser-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.vortex-chooser-tree {
  font-size: 11px;
  letter-spacing: 0.20em;
  text-transform: uppercase;
}
.vortex-chooser-label {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-strong, #fff);
}
.vortex-chooser-desc {
  font-size: 13px;
  color: var(--dim, #9a9aae);
  line-height: 1.4;
}
.scene-overlay h2 { margin: 0; font-size: 22px; letter-spacing: 0.08em; }
.scene-overlay p { margin: 0; color: var(--dim); font-size: 13px; max-width: 32em; }
.scene-overlay .scene-score { font-size: 14px; color: var(--ink); letter-spacing: 0.06em; margin-top: 4px; }
.scene-overlay .scene-score.new-best { color: var(--accent); font-weight: 600; }

/* Multiplayer party breakdown inside the game-over modal — one row per
 * player, sorted best-first, crown on #1, local player's row highlighted. */
.scene-overlay-scores {
  display: flex; flex-direction: column;
  gap: 6px;
  width: min(340px, 86vw);
  margin: 4px 0 2px;
}
.scene-score-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 12px 7px 8px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
  text-align: left;
}
body.theme-light .scene-score-row {
  background: rgba(0, 0, 0, 0.045);
  border-color: rgba(0, 0, 0, 0.08);
}
.scene-score-row.is-top {
  /* gold-tinted standout for the leader of the round */
  background: rgba(255, 216, 77, 0.12);
  border-color: rgba(255, 216, 77, 0.4);
}
.scene-score-row.is-mine {
  box-shadow: inset 0 0 0 1px var(--accent, #ffd84d);
}
.scene-score-color { width: 4px; height: 26px; border-radius: 2px; flex-shrink: 0; }
.scene-score-rank {
  width: 24px; flex-shrink: 0; text-align: center;
  font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums;
  color: var(--dim);
}
.scene-score-row.is-top .scene-score-rank { font-size: 18px; }
.scene-overlay .scene-score-avatar.mp-avatar.sm { width: 30px; height: 30px; flex-shrink: 0; }
.scene-score-name {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 14px; font-weight: 600; color: var(--ink);
}
.scene-score-val {
  font-weight: 700; font-variant-numeric: tabular-nums;
  font-size: 16px; white-space: nowrap;
  color: var(--ink-strong, #fff);
}
body.theme-light .scene-score-val { color: var(--ink); }
.scene-overlay button {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--accent);
  padding: 10px 18px;
  font: inherit;
  letter-spacing: 0.08em;
  cursor: pointer;
  border-radius: 4px;
  font-size: 14px;
}
.scene-overlay button:hover { background: var(--accent); color: var(--bg); }
.scene-overlay .scene-overlay-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
.scene-overlay .scene-overlay-again,
.scene-overlay .scene-overlay-random,
.scene-overlay .scene-overlay-resume {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.scene-overlay .scene-overlay-again svg,
.scene-overlay .scene-overlay-random svg,
.scene-overlay .scene-overlay-resume svg {
  width: 16px;
  height: 16px;
  display: block;
}

/* Dig Deep night shop — fullscreen modal with a scrollable inner panel. */
/* Fullscreen repair shop: a scrollable content column with a sticky descend
 * footer pinned to the bottom. */
.digdeep-shop {
  padding: 0;
  text-align: left;
  align-items: stretch;
  justify-content: flex-start;
}
.digdeep-shop-panel {
  width: 100%;
  max-width: min(900px, 96vw);
  margin: 0 auto;
  flex: 1 1 auto;
  min-height: 0;          /* allow the flex child to actually scroll */
  overflow-y: auto;
  padding: 14px 18px 18px;
  display: flex;
  flex-direction: column;
}
.digdeep-shop-panel h2 { font-size: 20px; }
.digdeep-shop-foot {
  width: 100%;
  max-width: min(900px, 96vw);
  margin: 0 auto;
  flex: 0 0 auto;
  padding: 10px 18px calc(10px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  background: var(--scene-overlay-bg);
}
/* Ship-bodies view: a vertical list of chassis rows (image left, price right). */
.digdeep-body-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 8px 0;
  padding: 8px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.digdeep-body-row.equipped {
  border-color: rgba(95, 207, 95, 0.6);
  box-shadow: inset 0 0 0 1px rgba(95, 207, 95, 0.4);
}
.digdeep-body-pic {
  flex: 0 0 auto;
  width: 120px;
  height: 90px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: radial-gradient(circle at 50% 38%, #1b2440, #0a0f1e);
}

/* touch controls (DOM overlay over canvas) */
#stage-controls {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 108;
}
#stage-controls.active { pointer-events: none; }
#stage-controls .stick {
  position: absolute;
  bottom: max(20px, env(safe-area-inset-bottom));
  left: max(18px, env(safe-area-inset-left));
  width: 130px; height: 130px;
  border-radius: 50%;
  background: rgba(255,255,255,0.06);
  border: 2px solid rgba(255,255,255,0.18);
  pointer-events: auto;
  touch-action: none;
}
/* Stick thumb — white in dark theme, black in light theme. Same
   silhouette either way; just the fill colour swaps so the dpad
   reads against whichever page background is showing. */
#stage-controls .stick .thumb {
  position: absolute;
  left: 50%; top: 50%;
  width: 50px; height: 50px;
  margin: -25px 0 0 -25px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  border: 2px solid rgba(255, 255, 255, 0.9);
  pointer-events: none;
  transition: background 100ms;
}
#stage-controls .stick.active .thumb { background: rgba(255, 255, 255, 0.85); }
body.theme-light #stage-controls .stick .thumb {
  background: rgba(0, 0, 0, 0.45);
  border-color: rgba(0, 0, 0, 0.75);
}
body.theme-light #stage-controls .stick.active .thumb {
  background: rgba(0, 0, 0, 0.7);
}
#stage-controls .buttons {
  position: absolute;
  bottom: max(28px, env(safe-area-inset-bottom));
  right: max(18px, env(safe-area-inset-right));
  display: flex;
  flex-direction: row-reverse;
  gap: 14px;
  pointer-events: none;
}
#stage-controls .btn {
  width: 70px; height: 70px;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  border: 2px solid rgba(255,255,255,0.22);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.08em;
  pointer-events: auto;
  touch-action: none;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}
#stage-controls .btn.active { background: rgba(255, 216, 77, 0.4); border-color: var(--accent); color: #000; }
#stage-controls .btn.large { width: 90px; height: 90px; font-size: 13px; }
/* Cooldown overlay — a clockwise conic-gradient pie wipe driven by
   the `--cd` custom property (0..1, 1 = just fired). Sits ABOVE the
   icon but below the active highlight so the sweep reads even when
   the player keeps tapping. `cd-active` enables the overlay; when
   removed, the pseudo-element is hidden so a fully-ready button has
   no extra rendering cost. */
#stage-controls .btn {
  --cd: 0;
  position: relative;
  overflow: hidden;
}
#stage-controls .btn::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(
    from -90deg,
    rgba(0,0,0,0.62) calc(var(--cd) * 360deg),
    transparent 0deg
  );
  opacity: 0;
  transition: opacity 80ms linear;
}
#stage-controls .btn.cd-active::after { opacity: 1; }
/* Disabled / locked button — washed-out fill, dimmed icon, no taps. */
#stage-controls .btn.disabled,
#stage-controls .btn:disabled {
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.35);
  opacity: 0.55;
  cursor: not-allowed;
}
#stage-controls .btn.disabled svg,
#stage-controls .btn:disabled svg { opacity: 0.6; }
/* Icon-only buttons: centre the SVG and let it scale with the button. */
#stage-controls .btn.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
#stage-controls .btn.btn-icon svg {
  width: 56%;
  height: 56%;
  display: block;
  pointer-events: none;
}

/* hide touch overlay when the game opted out */
#stage-controls[hidden] { display: none; }

/* loading shimmer for lazy game module */
.scene-loading {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--dim);
  font-size: 13px;
  letter-spacing: 0.16em;
  background: var(--bg);
  z-index: 106;
}

/* pause + settings overlay */
.settings-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay-bg);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 16px;
}
.settings-panel {
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: 24px 26px;
  min-width: min(420px, 92vw);
  max-width: 92vw;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
@media (min-width: 720px) {
  .settings-panel {
    min-width: 560px;
    padding: 32px 36px;
  }
}
@media (min-width: 1024px) {
  .settings-panel {
    min-width: 640px;
    padding: 36px 40px;
  }
}
/* Pause-panel header: large square animated hero canvas on top, then
 * title + subtitle left-aligned underneath. The hero shares its
 * 32-unit drawHoverIcon coordinate space with the home grid so it
 * reads as the same tile the player launched the game from. */
.gs-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  border-bottom: 1px solid var(--row-bg);
  padding-bottom: 18px;
  margin-bottom: 16px;
}
/* Corner close (X) for the global settings panel — replaces the old
 * full-width "Done" action button. Sits in the panel's top-right so
 * the panel is dismissable on mobile (where the sheet fills the
 * screen and there's no backdrop to tap). */
.gs-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--row-bg);
  color: var(--ink);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  cursor: pointer;
  z-index: 3;
}
.gs-close svg { width: 18px; height: 18px; }
.gs-close:hover { background: var(--hover-bg); border-color: var(--dim); }
/* The settings-panel must be a positioning context for the absolute
 * close button. */
.settings-panel { position: relative; }
@media (max-width: 720px) {
  .gs-close {
    top: max(12px, env(safe-area-inset-top));
  }
}
/* Brand mark tucked into the top-left corner of every settings / confirm
 * modal (global settings, the in-game pause panel, the reset-game
 * dialog, …). Painted as a background image so it swaps with the theme
 * the same way the .logo-light / .logo-dark <img>s do — the dark-tile
 * logo on dark theme, the light-tile logo on light theme. */
.settings-panel::before {
  content: '';
  position: absolute;
  top: 14px;
  left: 16px;
  width: 26px;
  height: 26px;
  background: url(/logo-dark.svg) center / contain no-repeat;
  opacity: 0.92;
  pointer-events: none;
  z-index: 3;
}
body.theme-light .settings-panel::before {
  background-image: url(/logo-light.svg);
}
@media (max-width: 720px) {
  .settings-panel::before {
    top: max(14px, env(safe-area-inset-top));
  }
}
/* Gear hero for the global settings panel — a large cog above the
 * "Settings" title, replacing the old brand-logo hero (the logo now
 * lives in the corner via .settings-panel::before). Spins slowly so the
 * hero has a touch of life, matching the animated hero in the per-game
 * pause panel. */
.gs-gear {
  width: 84px;
  height: 84px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-strong);
  flex-shrink: 0;
}
.gs-gear svg {
  width: 100%;
  height: 100%;
  display: block;
  animation: gs-gear-spin 14s linear infinite;
}
@keyframes gs-gear-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .gs-gear svg { animation: none; }
}
.gs-hero {
  display: block;
  /* Square hero, capped at 320px. Capping width AND height keeps the
   * aspect-ratio honoured — without `max-width` a 400-px panel was
   * stretching the canvas to 400×320 (max-height clamped the height
   * but the width kept growing to fill 100%, breaking the 1:1 ratio).
   */
  width: min(100%, 320px);
  aspect-ratio: 1 / 1;
  height: auto;
  max-height: 320px;
  max-width: 320px;
  align-self: center;
  border-radius: 12px;
  border: 1px solid var(--panel-border);
  background: var(--row-bg);
  /* Hero is rendered into a 256-px buffer; let the browser downscale
   * smoothly rather than picking the nearest pixel. */
  image-rendering: auto;
}
/* Row icon — small glyph beside each setting label. Fixed box keeps
 * every label at the same x regardless of which SVG is inside. */
.gs-row .gs-icon {
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--dim);
  flex: none;
}
.gs-row .gs-icon svg {
  width: 18px;
  height: 18px;
  display: block;
}
.gs-title-wrap {
  min-width: 0;
  text-align: left;
}
.gs-title {
  margin: 0;
  font-size: 26px;
  letter-spacing: 0.06em;
  color: var(--ink-strong);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
/* MP pill next to the game name when the settings panel opens inside
 * a multiplayer round. Same accent treatment as the chooser-active
 * upgrade chips so it reads as a status badge, not a button. */
.gs-mp-pill {
  display: inline-flex;
  align-items: center;
  background: var(--accent);
  color: var(--bg);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  line-height: 1;
}
.gs-desc {
  margin: 6px 0 0;
  color: var(--dim);
  font-size: 13px;
  letter-spacing: 0.04em;
  line-height: 1.5;
}
.gs-scoreboard {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 18px;
}
/* SP-only "best" scoreboard — single block instead of two-column,
 * since the current-score column was removed. */
.gs-scoreboard-solo {
  grid-template-columns: 1fr;
}
.gs-score-block {
  background: var(--row-bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.gs-score-label {
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.gs-score-value {
  color: var(--ink-strong);
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.gs-controls {
  margin-bottom: 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--row-bg);
}
.gs-section-title {
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.18em;
  margin-bottom: 8px;
  text-transform: uppercase;
}

/* ---- Music libraries manager (global settings) ----------------- */
.gs-music-libs {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--row-bg);
}
.gs-music-libs-hint {
  margin: 0 0 10px;
  font-size: 11px;
  line-height: 1.5;
}
.gs-music-lib-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 10px;
}
.gs-music-lib-row {
  display: flex;
  align-items: center;
  gap: 9px;
  background: var(--row-bg);
  border-radius: 6px;
  padding: 6px 10px;
}
/* Round "+" add button living in the Libraries header row — filled
 * accent disc with a white glyph. */
.gs-music-lib-add-mini {
  justify-self: end;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 999px;
  cursor: pointer;
  padding: 0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.gs-music-lib-add-mini svg { width: 18px; height: 18px; }
.gs-music-lib-add-mini:hover { filter: brightness(1.08); }
.gs-music-lib-add-mini:active { transform: translateY(1px); }
/* Per-entry track count, just left of the link button. */
.gs-music-lib-count {
  flex: 0 0 auto;
  min-width: 26px;
  text-align: center;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-radius: 999px;
  padding: 2px 7px;
}
/* Open-on-github link button (matches the remove button's footprint). */
.gs-music-lib-link {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--dim);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
}
.gs-music-lib-link:hover { color: var(--accent); border-color: var(--accent); }
.gs-music-lib-link svg { width: 13px; height: 13px; }
.gs-music-lib-toggle {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  cursor: pointer;
}
.gs-music-lib-path {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gs-music-lib-remove {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--dim);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
}
.gs-music-lib-remove:hover { color: #ff5f6f; border-color: rgba(255,95,111,0.55); }
.gs-music-lib-remove svg { width: 13px; height: 13px; }
.gs-music-lib-add {
  display: flex;
  gap: 8px;
}
.gs-music-lib-input {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--row-bg);
  color: var(--ink);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
  font-size: 13px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.gs-music-lib-input:focus { outline: none; border-color: var(--accent); }
.gs-music-lib-addbtn {
  flex: 0 0 auto;
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 8px 16px;
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.gs-music-lib-addbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.gs-music-lib-addbtn svg { width: 14px; height: 14px; }
.gs-music-lib-addbtn:hover { background: var(--accent); color: var(--bg); }
.gs-music-lib-err {
  display: block;
  color: #ff5f6f;
  font-size: 11px;
  margin-top: 6px;
  min-height: 14px;
}

/* ---- Add-library picker modal ---------------------------------- */
.gs-libpicker-overlay { z-index: 210; }
.gs-libpicker {
  position: relative;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: 22px 24px;
  width: min(460px, 94vw);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.gs-libpicker .gs-header { margin-bottom: 12px; }
.gs-libpicker-search {
  width: 100%;
  background: var(--row-bg);
  color: var(--ink);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 10px 12px;
  font: inherit;
  font-size: 14px;
  margin-bottom: 12px;
}
.gs-libpicker-search:focus { outline: none; border-color: var(--accent); }
.gs-libpicker-results {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 120px;
  max-height: 46vh;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.gs-libpicker-status {
  color: var(--dim);
  font-size: 13px;
  text-align: center;
  padding: 28px 8px;
}
.gs-libpicker-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  background: var(--row-bg);
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 9px 12px;
  cursor: pointer;
  color: var(--ink);
  font: inherit;
}
.gs-libpicker-row:hover { border-color: var(--panel-border); }
.gs-libpicker-row.selected { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--row-bg)); }
.gs-libpicker-row.is-added { opacity: 0.5; cursor: default; }
.gs-libpicker-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.gs-libpicker-row-path {
  font-size: 13px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gs-libpicker-row-sub { font-size: 11px; color: var(--dim); }
.gs-libpicker-row-right { flex: 0 0 auto; display: inline-flex; align-items: center; }
/* Repo (navigational) rows: a drill-in chevron on the right. */
.gs-libpicker-chev { color: var(--dim); }
.gs-libpicker-chev svg { width: 16px; height: 16px; }
.gs-libpicker-row.is-repo:hover .gs-libpicker-chev { color: var(--accent); }
/* Informational note row (e.g. "no mp3 / wav files here") — not a button. */
.gs-libpicker-row.is-note { background: transparent; cursor: default; opacity: 0.7; }
.gs-libpicker-row-count {
  flex: 0 0 auto;
  min-width: 34px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-radius: 999px;
  padding: 3px 8px;
}
.gs-libpicker-footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 14px;
}
.gs-libpicker-confirm {
  background: var(--accent);
  color: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 9px 22px;
  font: inherit;
  font-size: 14px;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.gs-libpicker-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
.gs-libpicker-confirm:not(:disabled):hover { filter: brightness(1.08); }
/* Controls table: one outer grid so the keys column auto-sizes to the
 * widest key-set, keeping the action column aligned across rows. Each
 * row uses display: contents so its two children (keys + action) sit
 * directly in the parent grid. */
.gs-controls-table {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 16px;
  row-gap: 6px;
  align-items: center;
}
.gs-ctrl-row { display: contents; }
.gs-ctrl-keys {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.gs-ctrl-sep {
  color: var(--dim);
  font-size: 11px;
  padding: 0 1px;
}
.gs-ctrl-action {
  color: var(--dim);
  font-size: 12px;
}
.gs-ctrl-note {
  grid-column: 1 / -1;
  color: var(--dim);
  font-size: 11px;
  font-style: italic;
  padding-left: 4px;
}
/* Key cap — the same look for typed letters, named keys (Esc, Space, …),
 * and the small pointer icons. */
.gs-controls kbd.gs-key,
.gs-controls .gs-key {
  background: var(--kbd-bg);
  border: 1px solid var(--panel-border);
  border-radius: 4px;
  padding: 2px 6px;
  font: inherit;
  font-size: 11px;
  color: var(--ink);
  letter-spacing: 0.04em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  min-height: 20px;
  line-height: 1;
  box-shadow: inset 0 -1px 0 var(--panel-border);
}
.gs-controls .gs-key-icon {
  padding: 2px 5px;
}
.gs-controls .gs-key-icon svg {
  display: block;
  width: 14px;
  height: 14px;
  color: var(--ink);
}
.gs-controls .gs-key-mouse svg {
  width: 12px;
  height: 14px;
}
.gs-ctrl-system .gs-key {
  background: var(--kbd-system-bg);
  color: var(--dim);
}

.gs-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 18px;
}
.gs-row {
  display: grid;
  grid-template-columns: 22px 1fr 160px;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  color: var(--ink);
}
/* Read-only row (member view of a non-personal MP setting): the value
 * still reads at a glance, but the input is dimmed + cursor flips to
 * not-allowed so it's obvious the leader owns the choice. */
.gs-row-readonly { opacity: 0.78; }
.gs-row-readonly .gs-row-label { color: var(--dim, #9a9aae); }
.gs-row-readonly .gs-toggle,
.gs-row-readonly .gs-select,
.gs-row-readonly .gs-slider-wrap input[type=range] {
  cursor: not-allowed;
  filter: saturate(0.4);
}
.gs-row-readonly .gs-toggle:hover,
.gs-row-readonly .gs-select:hover { background: var(--row-bg, #1a1a26); }
/* Highlight applied when a peer's settings:update arrives and we
 * re-render the row. Pulses an accent-tinted backdrop + outline so
 * the user sees WHICH setting moved, instead of reading a toast. */
.gs-row-flash {
  animation: gs-row-flash-anim 1.5s ease-out 1;
  border-radius: 8px;
}
@keyframes gs-row-flash-anim {
  0%   { background: rgba(255, 216, 77, 0.32); box-shadow: 0 0 0 2px rgba(255, 216, 77, 0.65) inset; }
  40%  { background: rgba(255, 216, 77, 0.18); box-shadow: 0 0 0 2px rgba(255, 216, 77, 0.40) inset; }
  100% { background: transparent;               box-shadow: 0 0 0 2px transparent inset; }
}
.gs-row-label {
  color: var(--ink);
  letter-spacing: 0.04em;
  text-align: left;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gs-select {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 6px 12px;
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.04em;
  cursor: pointer;
  border-radius: 6px;
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  text-align: left;
}
/* Toggle switch — pill-shaped track + sliding knob. */
.gs-toggle {
  --toggle-w: 44px;
  --toggle-h: 24px;
  --toggle-pad: 2px;
  appearance: none;
  -webkit-appearance: none;
  background: var(--row-bg);
  border: 1px solid var(--panel-border);
  cursor: pointer;
  padding: 0;
  margin: 0;
  position: relative;
  width: var(--toggle-w);
  height: var(--toggle-h);
  border-radius: 999px;
  justify-self: end;
  transition: background-color 120ms ease, border-color 120ms ease;
  flex: none;
}
.gs-toggle:hover { background: var(--hover-bg); }
.gs-toggle.on {
  background: var(--accent);
  border-color: var(--accent);
}
.gs-toggle-knob {
  position: absolute;
  top: var(--toggle-pad);
  left: var(--toggle-pad);
  width: calc(var(--toggle-h) - var(--toggle-pad) * 2 - 2px);
  height: calc(var(--toggle-h) - var(--toggle-pad) * 2 - 2px);
  background: var(--ink-strong, #fff);
  border-radius: 999px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  transition: transform 140ms ease, background-color 120ms ease;
}
.gs-toggle.on .gs-toggle-knob {
  transform: translateX(calc(var(--toggle-w) - var(--toggle-h)));
  background: #fff;
}
.gs-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.gs-select-value {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gs-select-caret {
  width: 12px;
  height: 12px;
  color: var(--dim);
  flex: none;
}
.gs-toggle.on { border-color: var(--accent); color: var(--accent); }
.gs-toggle:hover, .gs-select:hover { background: var(--hover-bg); }
/* gear + dropdown share the control cell (3rd grid column) */
.gs-select-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
.gs-select-wrap .gs-select { flex: 1; min-width: 0; }
.gs-select-gear {
  flex: none;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  background: transparent;
  color: var(--ink, #e8e8f0);
  cursor: pointer;
}
.gs-select-gear:hover { background: var(--hover-bg); color: var(--accent); }
.gs-select-gear svg { width: 18px; height: 18px; }
.gs-slider-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.gs-slider-wrap input[type=range] {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
}
.gs-slider-out {
  font-size: 12px;
  color: var(--dim);
  min-width: 32px;
  text-align: right;
}
.gs-info-value { color: var(--ink); font-size: 13px; text-align: right; }

/* Popover dropdown for select rows. */
.gs-select-menu {
  position: fixed;
  z-index: 220;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  padding: 4px;
  display: flex;
  flex-direction: column;
  max-height: 280px;
  overflow-y: auto;
}
.gs-select-item {
  background: transparent;
  border: 0;
  color: var(--ink);
  text-align: left;
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.04em;
  padding: 7px 12px;
  border-radius: 4px;
  cursor: pointer;
}
.gs-select-item:hover,
.gs-select-item.highlight,
.gs-select-item:focus {
  background: var(--hover-bg);
  outline: none;
}
.gs-select-item.selected {
  color: var(--accent);
}
.gs-select-item.selected::before {
  content: '✓';
  display: inline-block;
  width: 14px;
  margin-right: 4px;
  color: var(--accent);
}
.gs-select-item:not(.selected)::before {
  content: '';
  display: inline-block;
  width: 14px;
  margin-right: 4px;
}
.gs-actions {
  display: flex;
  gap: 10px;
  border-top: 1px solid var(--row-bg);
  padding-top: 16px;
}
/* Footer wraps the action buttons + (optionally) the music transport
 * so they pin to the panel bottom as one block. On desktop it's just
 * a stack at the end of the panel. */
.gs-footer {
  display: flex;
  flex-direction: column;
}

/* ---- Now-playing music transport (settings panel footer) -------- */
.gs-music {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--row-bg);
}
/* The whole player is ONE strip: a swap zone on the left (song title
 * by default, seek/progress on hover) + transport buttons & volume
 * on the right. */
.gs-music-bar {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* Swap zone — title and progress are stacked in the same box; only
 * one is visible at a time (see the :hover rule below). */
.gs-music-main {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  height: 32px;
}
.gs-music-ticker,
.gs-music-progress {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  transition: opacity 0.15s ease;
}
.gs-music-ticker {
  overflow: hidden;
  white-space: nowrap;
}
/* Edge fade ONLY while the title is actually marqueeing. For a short
 * title that fits without scrolling, the left fade would clip the
 * first character — so the mask is applied only when the JS adds
 * `.gs-music-scroll` (i.e. the text overflows and is animating). */
.gs-music-ticker:has(.gs-music-scroll) {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
}
.gs-music-progress {
  gap: 6px;
  opacity: 0;
  pointer-events: none;
}
/* Swap: interacting with the player (hover/move on desktop, tap on
 * touch — both flip the JS `gs-music-active` class) reveals the seek
 * bar and hides the title; after a few idle seconds it reverts to
 * the marqueeing title. `gs-music-seeking` (set while dragging) keeps
 * the progress up even if the pointer leaves the bar mid-drag, and
 * `:focus-within` keeps it up for keyboard users. We deliberately do
 * NOT key off `:hover` so a stationary cursor still times out back
 * to the title. */
.gs-music:focus-within .gs-music-ticker,
.gs-music.gs-music-seeking .gs-music-ticker,
.gs-music.gs-music-active .gs-music-ticker { opacity: 0; }
.gs-music:focus-within .gs-music-progress,
.gs-music.gs-music-seeking .gs-music-progress,
.gs-music.gs-music-active .gs-music-progress {
  opacity: 1;
  pointer-events: auto;
}
.gs-music-track {
  display: inline-block;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--ink);
}
/* Marquee — applied (via JS) when the title overflows its zone.
 * Scrolls the text fully off the left then wraps. */
.gs-music-track.gs-music-scroll {
  padding-left: 100%;
  animation: gs-marquee 9s linear infinite;
}
@keyframes gs-marquee {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-100%); }
}
.gs-music-time {
  flex: 0 0 auto;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
  min-width: 28px;
  text-align: center;
}
.gs-music-seek {
  flex: 1 1 auto;
  min-width: 40px;
  height: 16px;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
/* Track — a thin rail with an accent-filled portion driven by the
 * `--p` percentage var the JS updates each frame. */
.gs-music-seek::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg,
    var(--accent) 0 calc(var(--p, 0) * 1%),
    var(--row-bg) calc(var(--p, 0) * 1%) 100%);
}
.gs-music-seek::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--row-bg);
}
.gs-music-seek::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}
.gs-music-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  margin-top: -4px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--panel-bg);
  cursor: pointer;
}
.gs-music-seek::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--panel-bg);
  cursor: pointer;
}
.gs-music-btn {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--row-bg);
  color: var(--ink);
  border: 1px solid var(--panel-border);
  border-radius: 7px;
  cursor: pointer;
  padding: 0;
}
.gs-music-btn:hover { background: var(--hover-bg); border-color: var(--dim); }
.gs-music-btn svg { width: 18px; height: 18px; }
.gs-music-play { border-color: var(--accent); color: var(--accent); }

/* Volume: a button that opens a vertical-slider popover above it. */
.gs-music-vol-wrap {
  position: relative;
  flex: 0 0 auto;
}
.gs-music-vol-pop {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 12px 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 20;
  display: flex;
  justify-content: center;
}
.gs-music-vol-pop[hidden] { display: none; }
/* Vertical volume slider — modern writing-mode approach (min at the
 * bottom, max at the top). */
.gs-music-vol {
  writing-mode: vertical-lr;
  direction: rtl;
  width: 8px;
  height: 96px;
  accent-color: var(--accent);
  cursor: pointer;
}
/* Mobile: the pause / settings panel is too tall for short viewports
 * (Laser Grid + similar games have a lot of rows). Behave like a
 * sheet:
 *   - panel anchors to the TOP of the screen (no centring) and
 *     stretches the full height, so the user always sees the panel
 *     header even before scrolling.
 *   - panel becomes the scroll container — the long list of settings
 *     rows scrolls vertically inside it.
 *   - .gs-actions (resume / reset) is `position: sticky; bottom: 0`
 *     so it stays pinned to the visible bottom edge regardless of
 *     scroll position.
 * Desktop layout (centred card) is untouched. */
@media (max-width: 720px) {
  .settings-overlay {
    align-items: flex-start;
    padding: 0;
  }
  /* On mobile the music player must fit one strip — drop the
   * current/total time labels (they eat horizontal room); the seek
   * bar alone conveys progress + still allows scrubbing. */
  .gs-music-time { display: none; }
  .settings-panel {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    max-height: 100dvh;
    min-height: 100dvh;
    height: 100dvh;
    border-radius: 0;
    border: none;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 16px 18px 0;
    /* Reserve room for the fixed footer (actions [+ music]) at the
     * bottom so the last settings row isn't hidden behind it when
     * scrolled to the end. Two sizes: actions-only vs. actions+music. */
    padding-bottom: calc(96px + env(safe-area-inset-bottom));
    box-shadow: none;
    /* Honour the device's safe-area inset (notch / home indicator)
     * so the title doesn't slide under the iOS notch. */
    padding-top: max(16px, env(safe-area-inset-top));
  }
  /* When the music transport is present the footer is taller — give
   * the scroll content more bottom room so its last row clears it. */
  .settings-panel:has(.gs-music) {
    padding-bottom: calc(200px + env(safe-area-inset-bottom));
  }
  /* Anchor the footer (Resume/Reset + music transport) to the very
   * bottom of the screen, regardless of scroll position. `position:
   * sticky` only sits at the bottom while the panel's own bottom edge
   * is in view; on a short panel that leaves it floating mid-screen.
   * `position: fixed` against the viewport always pins it. The
   * reserved `padding-bottom` above keeps the last setting row
   * scrollable into view without being covered. */
  .gs-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--panel-bg);
    margin: 0;
    padding: 14px 18px;
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    z-index: 10;
    border-top: 1px solid var(--row-bg);
    box-shadow: 0 -8px 18px -10px rgba(0, 0, 0, 0.45);
  }
  /* The footer already has a top border; drop the action row's own so
   * they don't stack into a double rule. */
  .gs-footer .gs-actions {
    border-top: none;
    padding-top: 0;
  }
  .gs-footer .gs-music {
    border-top-color: var(--panel-border);
  }

  /* Confirm modals (reset / leave): ONLY an icon + title + blurb +
   * two buttons. The full-height sheet looks empty with content at
   * the top, so centre the prompt vertically. The action bar is
   * already `position: fixed` at the bottom (above), so we don't
   * need margin tricks here. */
  .gs-confirm.settings-panel {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

/* Confirm modal layout (reset / leave dialogs) — applies on ALL
 * viewports so the prompt + reset glyph read as centred whether the
 * dialog is a small desktop card or a full-height mobile sheet. */
.gs-confirm {
  text-align: center;
}
.gs-confirm .gs-title,
.gs-confirm .gs-desc {
  margin-left: auto;
  margin-right: auto;
  max-width: 34ch;
}
/* `.gs-title` is inline-flex (for the MP pill in game settings) —
 * justify-content centres it within the centred text flow here. */
.gs-confirm .gs-title {
  justify-content: center;
}
.gs-confirm-icon {
  display: flex;
  justify-content: center;
  margin: 2px 0 18px;
}
.gs-confirm-icon svg {
  width: 168px;
  height: 168px;
  color: var(--accent);
}
.gs-actions button {
  flex: 1;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 12px 14px;
  font: inherit;
  font-size: 14px;
  letter-spacing: 0.08em;
  cursor: pointer;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.gs-actions button svg { width: 18px; height: 18px; flex-shrink: 0; }
.gs-resume { border-color: var(--accent) !important; color: var(--accent) !important; }
.gs-resume:hover { background: var(--accent); color: var(--bg) !important; }
.gs-home:hover { background: var(--hover-bg); border-color: var(--dim); }
/* Reset button — warning red so an accidental click reads as "stop". */
.gs-reset { border-color: rgba(255, 95, 111, 0.55) !important; color: #ff5f6f !important; }
.gs-reset:hover { background: #ff5f6f; color: var(--bg) !important; border-color: #ff5f6f !important; }

/* ---------------------------------------------------------------- */
/* Multiplayer UI                                                   */
/* ---------------------------------------------------------------- */

/* Topbar user button */
.mp-user-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 1px solid var(--panel-border, rgba(255,255,255,0.12));
  color: var(--ink, #e8e8f0);
  padding: 6px 10px;
  border-radius: 999px;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.mp-user-btn:hover { border-color: var(--accent, #ffd84d); }
.mp-user-btn svg { width: 16px; height: 16px; }
.mp-user-btn.signed-in { border-color: var(--accent, #ffd84d); }
.mp-user-name { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mp-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent, #ffd84d);
  color: var(--bg, #07070b);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0;
}
.mp-avatar.lg { width: 36px; height: 36px; font-size: 14px; }
/* Party member-count chip in the user button. Shows the count of OTHER
 * members in your current party — e.g. "+3" when you and 3 friends are
 * grouped up. */
.mp-party-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent, #ffd84d);
  color: var(--bg, #07070b);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  margin-left: 2px;
}
.mp-user-btn.in-party { border-color: var(--accent, #ffd84d); }

/* Modal overlay (auth) */
.mp-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.72);
  display: flex; align-items: center; justify-content: center;
  z-index: 9999;
}
.mp-panel {
  position: relative;
  background: var(--bg-2, #0f0f17);
  border: 1px solid var(--panel-border, rgba(255,255,255,0.12));
  border-radius: 14px;
  padding: 24px;
  width: min(420px, 92vw);
  max-height: 92vh;
  overflow: auto;
  color: var(--ink);
}
.mp-close {
  position: absolute; top: 8px; right: 12px;
  background: transparent; border: 0; color: var(--ink);
  font-size: 24px; line-height: 1; cursor: pointer;
  padding: 4px 8px;
}

/* Auth-modal intro banner — surfaces invite context ("X has invited
 * you to play. Create your account.") when an anonymous visitor lands
 * on a party-link URL. Sits above the sign-in / sign-up tabs. */
.mp-auth-intro {
  margin: -6px -4px 14px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--accent, #ffd84d);
  background: rgba(255, 216, 77, 0.10);
  color: var(--ink, #e8e8f0);
  font-size: 14px;
  line-height: 1.4;
}
.mp-auth-intro p { margin: 0; }
.mp-auth-intro p + p { margin-top: 4px; color: var(--dim); font-size: 12px; }
.mp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--panel-border); margin-bottom: 14px; }
.mp-tab {
  background: transparent; border: 0;
  color: var(--dim, #6c6c80);
  padding: 10px 14px; cursor: pointer; font: inherit;
  border-bottom: 2px solid transparent;
}
.mp-tab.active { color: var(--ink); border-bottom-color: var(--accent); }

.mp-form { display: grid; gap: 12px; }
.mp-form h2 { margin: 0 0 4px; font-size: 18px; }
.mp-form label { display: grid; gap: 4px; font-size: 12px; color: var(--dim); }
.mp-form input {
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  color: var(--ink);
  padding: 9px 10px;
  border-radius: 6px;
  font: inherit;
}
.mp-form input:focus { border-color: var(--accent); outline: none; }
.mp-form button[type=submit] {
  background: var(--accent); color: var(--bg);
  border: 0; padding: 10px; border-radius: 6px;
  font: inherit; font-weight: 700; cursor: pointer;
}
.mp-error { color: #ff5f6f; font-size: 13px; margin: 0; }

/* Two-step auth modal */
.mp-form-inner { display: grid; gap: 12px; }
.mp-auth-sub { margin: 0; font-size: 13px; color: var(--dim); line-height: 1.4; }
.mp-form-info { line-height: 1.5; }
.mp-form-info p { margin: 0; font-size: 14px; }
.mp-form-info strong { color: var(--ink); }
/* Envelope graphic above the magic-link "login link sent" message.
 * Sized larger than the rest of the body text so it reads as the
 * focal point — gold accent stroke ties it to the brand. */
.mp-letter-art {
  display: flex; justify-content: center;
  color: var(--accent);
  margin: 10px 0 14px;
}
.mp-auth-panel { padding: 36px 32px; }
.mp-tos-panel  { padding: 48px 40px; }
.mp-invite-panel { padding: 36px 32px; }
/* Header row: inviter avatar + the "X has invited you to play!" copy.
 * Avatar sits left at a comfortable size; heading runs adjacent and
 * wraps if the username is long. */
.mp-invite-header {
  display: flex; align-items: center; gap: 14px;
  margin: 0 0 18px;
}
.mp-invite-header h2 {
  margin: 0;
  font-size: 18px;
  line-height: 1.3;
  font-weight: 600;
}
.mp-invite-header .mp-avatar {
  width: 56px; height: 56px;
  font-size: 22px;
  flex-shrink: 0;
}
.mp-avatar-lg { width: 56px; height: 56px; font-size: 22px; }
.mp-guest-form { margin-bottom: 4px; }
/* Guest avatar visual — gray fill + dim text + dashed border so they
 * stand out as ephemeral. Applies to any `.mp-avatar` marked
 * `.mp-avatar-guest` (set by `renderAvatarInto` when the user record's
 * `guest` flag is true). */
.mp-avatar-guest {
  background: rgba(255,255,255,0.06) !important;
  color: var(--dim) !important;
  border: 1px dashed var(--panel-border);
}
/* Breathing room between the modal heading ("Log in", "Welcome…",
 * etc.) and the first field below it. Applies inside both the auth
 * flow and the ToS modal. */
.mp-auth-panel .mp-form-inner h2,
.mp-tos-panel  h2 {
  margin: 0 0 18px;
}
/* Back-as-icon, slotted to the left of the step-2 header.
 * The `.mp-auth-header` row arranges them on one baseline so the
 * chevron sits flush with the "Log back in" / "Register" text. */
.mp-auth-header {
  display: flex; align-items: center; gap: 10px;
  margin: 0;
}
.mp-auth-header h2 { margin: 0; }
.mp-back {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; color: var(--dim);
  padding: 6px;
  /* Negative top margin lifts the chevron 16 px so it visually aligns
   * with the optical centre of the heading text rather than its
   * baseline — the SVG was sitting flush with the bottom of the H2. */
  margin: -16px -2px 0 -6px;
  font: inherit; line-height: 0;
  cursor: pointer; border-radius: 6px;
  transition: color 0.12s, background 0.12s;
}
.mp-back:hover { color: var(--ink); background: rgba(255,255,255,0.06); }
/* Read-only email field on step 2 — same visual chrome as a regular
 * input but desaturated so it reads as "informational". */
.mp-form input[readonly] {
  color: var(--dim);
  background: rgba(255,255,255,0.02);
  cursor: default;
}
.mp-or {
  text-align: center; color: var(--dim); font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase;
  position: relative; margin: 4px 0;
}
.mp-or span { background: var(--bg-2, #0f0f17); padding: 0 10px; position: relative; z-index: 1; }
.mp-or::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: 1px; background: var(--panel-border);
}
.mp-social-row { display: flex; gap: 10px; justify-content: center; }
.mp-social {
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  color: var(--ink);
  border-radius: 999px; cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
.mp-social:hover { border-color: var(--accent); background: rgba(255,216,77,0.08); }
.mp-secondary-row {
  display: flex; flex-wrap: wrap; gap: 0;
  justify-content: center; align-items: baseline;
  margin-top: 4px;
}
.mp-link {
  background: transparent; border: 0; color: var(--dim);
  padding: 0; font: inherit; font-size: 12px;
  cursor: pointer; text-decoration: underline;
  text-decoration-color: rgba(108,108,128,0.5);
}
.mp-link:hover { color: var(--accent); text-decoration-color: var(--accent); }
/* Dot separator between sibling links. */
.mp-secondary-row .mp-link + .mp-link::before {
  content: '·';
  margin: 0 8px;
  color: var(--dim);
  text-decoration: none;
  display: inline-block;
  pointer-events: none;
}
/* Suppress the dot when the preceding sibling(s) are hidden — without
 * this, the only visible link in sign-up mode (the toggle, which sits
 * after two hidden links) would render a leading "·". CSS lacks a
 * "previous-visible-sibling" combinator, so we chain explicitly for
 * the up-to-three-link case. */
.mp-secondary-row .mp-link[hidden] + .mp-link::before,
.mp-secondary-row .mp-link[hidden] + .mp-link[hidden] + .mp-link::before {
  display: none;
}
.mp-secondary-row .mp-link[hidden] { display: none; }
/* Hidden form rows (confirm password in sign-in mode) — keep
 * `[hidden]` authoritative even though the `.mp-form label` rule
 * sets `display: grid`. */
.mp-form label[hidden] { display: none; }
.mp-confirm-row { transition: opacity 0.15s; }
.mp-form-inner button[type=submit], .mp-form-inner .mp-primary {
  /* Matches the in-game Resume button (`.gs-resume`): gold border with
   * white text, inverting to gold-fill + dark text on hover. */
  background: transparent; color: var(--ink);
  border: 1px solid var(--accent);
  padding: 10px; border-radius: 6px;
  font: inherit; font-weight: 700; cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.mp-form-inner button[type=submit]:hover,
.mp-form-inner .mp-primary:hover {
  background: var(--accent);
  color: var(--bg);
}

/* ToS modal */
.mp-overlay-blocking { background: rgba(0,0,0,0.72); }
.mp-tos-panel { max-width: 420px; }
.mp-tos-panel h2 { margin: 0 0 6px; font-size: 18px; }
.mp-tos-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--ink); line-height: 1.4;
}
.mp-tos-row input[type="checkbox"] {
  width: auto; margin: 0;
  accent-color: var(--accent);
}
.mp-tos-row a { color: var(--accent); }

/* Drawer */
.mp-drawer {
  position: fixed; right: 0; top: 0; bottom: 0;
  width: min(380px, 92vw);
  background: var(--bg-2, #0f0f17);
  border-left: 1px solid var(--panel-border, rgba(255,255,255,0.12));
  z-index: 9998;
  display: flex; flex-direction: column;
  color: var(--ink);
  box-shadow: -8px 0 24px rgba(0,0,0,0.4);
}
.mp-drawer-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--panel-border);
}
.mp-me { display: inline-flex; align-items: center; gap: 10px; }
.mp-me-name { display: grid; line-height: 1.15; }
.mp-me-name small { color: var(--dim); font-size: 11px; }
.mp-drawer-tabs { padding: 0 16px; }
.mp-pane { padding: 16px; flex: 1; overflow: auto; }
.mp-drawer-footer { padding: 12px 16px; border-top: 1px solid var(--panel-border); }
.mp-signout {
  background: transparent; color: var(--dim);
  border: 1px solid var(--panel-border);
  padding: 8px 12px; border-radius: 6px;
  font: inherit; font-size: 12px; cursor: pointer;
}
.mp-signout:hover { color: #ff5f6f; border-color: #ff5f6f; }

.mp-h4 { margin: 16px 0 8px; font-size: 12px; color: var(--dim); letter-spacing: 0.08em; text-transform: uppercase; }
.mp-empty { color: var(--dim); font-size: 13px; padding: 8px 0; }
.mp-hint  { color: var(--dim); font-size: 13px; }

.mp-primary, .mp-pane button[type=submit] {
  background: var(--accent); color: var(--bg);
  border: 0; padding: 9px 14px; border-radius: 6px;
  font: inherit; font-weight: 700; cursor: pointer;
}
.mp-secondary {
  background: transparent; color: var(--dim);
  border: 1px solid var(--panel-border);
  padding: 8px 12px; border-radius: 6px;
  font: inherit; cursor: pointer;
}

.mp-divider { text-align: center; color: var(--dim); font-size: 11px; margin: 12px 0; }

.mp-join-form, .mp-friend-search {
  display: flex; gap: 8px; align-items: stretch;
}
.mp-join-form input, .mp-friend-search input {
  flex: 1;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  color: var(--ink);
  padding: 8px 10px; border-radius: 6px; font: inherit;
}
.mp-join-form button, .mp-friend-search button {
  background: var(--accent); color: var(--bg);
  border: 0; padding: 0 12px; border-radius: 6px;
  font: inherit; cursor: pointer;
}

.mp-party-header { display: flex; gap: 12px; font-size: 13px; color: var(--dim); margin-bottom: 8px; }
.mp-party-header strong { color: var(--ink); }
.mp-party-link { display: flex; gap: 8px; margin-bottom: 12px; }
.mp-party-link input {
  flex: 1; background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  color: var(--ink); padding: 6px 8px; border-radius: 6px;
  font: 11px ui-monospace, monospace;
}
.mp-party-link button {
  background: transparent; color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 6px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 12px;
}

.mp-members { list-style: none; padding: 0; margin: 0 0 12px; display: grid; gap: 6px; }
.mp-member {
  display: flex; align-items: center; gap: 10px;
  padding: 8px; border-radius: 8px;
  background: rgba(255,255,255,0.03);
  transition: opacity 140ms ease, filter 140ms ease;
}
/* Member is in the disconnect-grace window — their tab dropped or
   they backgrounded long enough to stop the heartbeat. Server still
   holds their slot; we dim the row + flag the chip so the leader
   knows at a glance without losing the layout. */
.mp-member.is-offline {
  opacity: 0.55;
  filter: grayscale(0.4);
}
.mp-member-offline-tag {
  font-size: 9px;
  letter-spacing: 0.08em;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(255,255,255,0.07);
  color: var(--dim);
  margin-left: 4px;
  text-transform: lowercase;
}
/* Vertical colour bar matching the player's per-party palette slot.
 * Hugged to the left edge of the member row so it reads as "this is
 * your team colour across every MP game". */
.mp-member-swatch {
  display: inline-block;
  width: 4px;
  align-self: stretch;
  border-radius: 3px;
  flex-shrink: 0;
  margin-right: -4px;
  box-shadow: 0 0 6px rgba(255,255,255,0.05);
}
.mp-member-name { flex: 1; }
.mp-tag.host {
  margin-left: 6px;
  background: var(--accent); color: var(--bg);
  font-size: 9px; padding: 2px 6px; border-radius: 999px; letter-spacing: 0.08em;
}
.mp-host-actions { display: flex; gap: 8px; margin-bottom: 8px; }
.mp-host-actions button {
  flex: 1;
  background: transparent; color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 8px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 12px;
}
.mp-host-actions button:hover { border-color: var(--accent); }

.mp-friend-row, .mp-search-results > div {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0; border-bottom: 1px dashed rgba(255,255,255,0.05);
  transition: opacity 140ms ease, filter 140ms ease;
}
.mp-friend-row:last-child { border-bottom: 0; }
.mp-friend-row.is-offline {
  opacity: 0.48;
  filter: grayscale(0.45);
}
.mp-friend-row.is-offline:hover {
  opacity: 0.72;
  filter: grayscale(0.2);
}
/* Friend has pending unread messages — lift the offline-row dim so the
   chat icon + badge stay legible. CSS opacity creates a stacking
   context that would otherwise drag the entire row (chat button
   included) down to 48% alpha; the only way to keep a child fully
   opaque is to lift the parent's opacity entirely. The presence dot
   still indicates offline status via its colour. */
.mp-friend-row.is-offline.has-unread,
.mp-friend-row.is-offline.has-unread:hover {
  opacity: 1;
  filter: none;
}
.mp-friend-row span:nth-child(2), .mp-search-results > div span:nth-child(2) { flex: 1; font-size: 13px; }
.mp-friend-row small { color: var(--dim); font-size: 11px; }
.mp-friend-row button {
  background: transparent; color: var(--ink);
  border: 1px solid var(--panel-border);
  padding: 5px 9px; border-radius: 6px; font: inherit; font-size: 11px; cursor: pointer;
}
/* Default is monochrome ink; hover lights the icon + border to the
   accent yellow so the affordance reads. The remove (×) button is
   the destructive action, so it gets a red hover instead. */
.mp-friend-row button:hover {
  border-color: var(--accent); color: var(--accent);
}
.mp-friend-row .mp-friend-remove:hover {
  border-color: #ff5f6f; color: #ff5f6f;
}
.mp-friend-row button:disabled { opacity: 0.5; cursor: default; }

.mp-presence-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 5px;
  border-radius: 999px;
  vertical-align: middle;
  border: 1px solid rgba(255,255,255,0.22);
}
.mp-presence-dot.online {
  background: #5fcf5f;
  box-shadow: 0 0 8px rgba(95,207,95,0.45);
}
.mp-presence-dot.offline {
  background: var(--dim);
  opacity: 0.75;
}

.mp-history { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; }
.mp-history-row {
  background: rgba(255,255,255,0.03);
  border-radius: 8px; padding: 10px 12px;
}
.mp-history-row header { font-size: 13px; margin-bottom: 6px; }
.mp-history-row header small { color: var(--dim); font-size: 11px; margin-left: 6px; }
.mp-history-scores { list-style: none; padding: 0; margin: 0; font-size: 12px; }

/* Toast (incoming party invite) */
.mp-toast {
  position: fixed; bottom: 16px; right: 16px;
  background: var(--bg-2, #0f0f17);
  border: 1px solid var(--accent, #ffd84d);
  border-radius: 10px;
  padding: 12px 16px;
  z-index: 10000;
  max-width: 320px;
  color: var(--ink);
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.mp-toast p { margin: 0 0 8px; font-size: 13px; }
.mp-toast-actions { display: flex; gap: 8px; }
.mp-toast-actions button {
  flex: 1;
  background: var(--accent); color: var(--bg);
  border: 0; padding: 6px 10px; border-radius: 6px;
  font: inherit; font-weight: 700; cursor: pointer; font-size: 12px;
}
.mp-toast-actions button.mp-secondary {
  background: transparent; color: var(--dim);
  border: 1px solid var(--panel-border);
  font-weight: 400;
}

/* "Host paused" banner */
.mp-host-paused {
  position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
  background: rgba(0,0,0,0.7);
  color: var(--ink); padding: 10px 18px;
  border-radius: 999px;
  font: inherit; font-size: 13px;
  letter-spacing: 0.05em;
  z-index: 9000;
}

/* Multiplayer scoreboard inside the in-stage overlay */
.scene-overlay .mp-scoreboard {
  list-style: none; padding: 0; margin: 8px 0 16px;
  display: grid; gap: 6px;
  font-size: 14px;
}
.scene-overlay .mp-scoreboard li {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 6px 12px;
  background: rgba(255,255,255,0.05);
  border-radius: 6px;
}

/* ---------------- Notification badge / confirm modal / info toast ---------------- */

.mp-user-btn { position: relative; }
.mp-badge {
  position: absolute;
  top: -5px; right: -5px;
  min-width: 18px; height: 18px;
  padding: 0 5px;
  background: #ff5f6f;
  color: #ffffff;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0;
  line-height: 1;
  border-radius: 999px;
  border: 2px solid var(--bg, #07070b);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.2);
}

/* Per-friend "invite to party" button */
.mp-friend-invite {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
  padding: 4px 9px;
  border-radius: 6px;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  margin-right: 4px;
}
.mp-friend-invite:hover,
.mp-friend-chat:hover,
.mp-friend-remove:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: transparent;
}
.mp-friend-invite:disabled {
  /* When the friend is offline we want this button to read the same
     as the row's chat / × icons — i.e. inherit the row's offline
     dim (`.mp-friend-row.is-offline { opacity: 0.48 }`) and the
     standard monochrome ink colour we use for the other icons in
     this row. The old !important overrides forced accent yellow +
     a separate 0.4 opacity, which stacked with the row's 0.48 to
     bury the button at ~19% alpha and tinted it differently from
     its siblings. Just disable interaction here.
   */
  cursor: default;
  background: transparent;
}

/* Confirmation panel (remove friend, etc.) */
.mp-confirm-panel { width: min(360px, 90vw); }
.mp-confirm-panel h2 { margin: 0 0 6px; font-size: 16px; }
.mp-confirm-actions { display: flex; gap: 8px; margin-top: 16px; }
.mp-confirm-actions button { flex: 1; padding: 9px; border-radius: 6px; font: inherit; cursor: pointer; }
.mp-danger {
  background: #ff5f6f;
  color: #ffffff;
  border: 0;
  font-weight: 700;
}
.mp-danger:hover { background: #ff3848; }

/* Transient info toast — bottom-center, fades in/out */
.mp-info-toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translate(-50%, 8px);
  background: rgba(0,0,0,0.82);
  color: var(--ink, #ffffff);
  padding: 8px 16px;
  border-radius: 999px;
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.04em;
  z-index: 10001;
  opacity: 0;
  transition: opacity 0.18s ease, transform 0.18s ease;
  pointer-events: none;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
.mp-info-toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ---------------- Leader / promote / crown ---------------- */
.mp-crown {
  width: 14px;
  height: 14px;
  margin-right: 4px;
  vertical-align: -2px;
  color: #ffd84d;       /* gold */
  filter: drop-shadow(0 0 2px rgba(255, 216, 77, 0.4));
}
.mp-member-name { display: inline-flex; align-items: center; gap: 2px; }

/* Shared 14px monochrome icon used in kick / invite / remove / copy /
   disband buttons. Inherits text colour so the icon recolours with
   the button's :hover / :disabled state. */
.mp-icon { width: 14px; height: 14px; flex: 0 0 auto; color: currentColor; }

/* Latency chip in the local player's party row. Pushed to the right
   edge of the row via flex auto-margin so it sits opposite the
   avatar regardless of whether the row carries other action
   buttons. Mono font + small monochrome text so it reads as
   secondary info, not a control. */
.mp-self-latency {
  margin-left: auto;
  font: 11px ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.04em;
  color: var(--dim);
  padding: 2px 6px;
  border-radius: 4px;
  white-space: nowrap;
}
.mp-self-latency.mp-latency-good { color: #5fcf5f; }
.mp-self-latency.mp-latency-ok   { color: var(--accent); }
.mp-self-latency.mp-latency-bad  { color: #ff5f6f; }

/* Buttons with the icon + label combo. Used by invite + remove on the
   friends row. */
.mp-friend-invite,
.mp-friend-remove {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
}
.mp-friend-invite .mp-icon,
.mp-friend-remove .mp-icon { color: inherit; }

/* Party-member row icon button (chat / promote / kick). Visually
   matches the friends-list `.mp-icon-btn` (square, panel-bordered,
   transparent bg) so the two lists feel like the same vocabulary.
   Hover swaps both border and icon colour to the accent — same
   affordance the friends row uses. */
.mp-member-action {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink);
}
.mp-member-action:hover { border-color: var(--accent); color: var(--accent); }
/* Kick (the destructive leader action) keeps the same shape but
   shifts to red on hover so it's distinguishable from the neutral
   icons next to it. */
.mp-member-action[data-act="kick"]:hover {
  border-color: #ff5f6f; color: #ff5f6f;
}
/* The promote button reuses the leader-crown SVG, but the .mp-crown
   class on that SVG forces gold (#ffd84d) + a yellow glow filter —
   which is correct for the marker next to the current leader's name
   but makes the promote button look like a permanently-active state
   rather than a neutral icon. Strip both overrides when the crown
   sits inside a `.mp-member-action` so it inherits the button's
   dim/hover colour ladder like every other icon. The trailing
   leader marker (.mp-member-crown .mp-crown) is unaffected and
   stays gold. */
.mp-member-action .mp-crown {
  color: inherit;
  filter: none;
  margin: 0;
}
/* Chat button hosts the unread badge — needs to be the positioning
   ancestor for the absolutely-positioned pill. */
.mp-member-chat { position: relative; }
/* The leader-marker crown lives outside the name span (sits in the
   row's trailing icon strip, just right of the chat button). Strip the
   trailing margin baked into `.mp-crown` so it doesn't compound with
   the row's flex `gap`. */
.mp-member-crown { display: inline-flex; align-items: center; }
.mp-member-crown .mp-crown { margin: 0; }

/* Icon-only buttons (copy, deny/remove on pending rows). Square-ish
   so they sit cleanly at the end of an input/row. */
.mp-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink);
}
.mp-icon-btn:hover { border-color: var(--accent); color: var(--accent); }

/* Disband / leave button — icon next to label. */
.mp-leave-with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }

/* ---------------- Pending join + approve/deny + crown ---------------- */

/* Member name is now a flex line: text on the left, crown on the right. */
.mp-member-name {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0; /* allow truncation */
}
.mp-member-name .mp-name-text {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mp-member-name .mp-crown {
  /* Inline next to the name text, not pushed to the row edge — the
     crown reads as a status badge on the leader's name rather than a
     row-level icon. */
  margin: 0 0 0 4px;
  flex: 0 0 auto;
}

/* Pending member row — desaturated avatar + pending tag */
.mp-member-pending { opacity: 0.85; }
.mp-avatar-muted   { background: var(--dim, #6c6c80) !important; color: var(--bg, #07070b) !important; }
.mp-pending-tag {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(255, 216, 77, 0.18);
  color: var(--accent, #ffd84d);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
/* Inline variant — chip sits directly to the left of the deny
 * button instead of hugging the username, so it reads as "this is
 * the state of THAT action button". */
.mp-pending-tag.mp-pending-tag-inline {
  margin-left: 0;
  margin-right: 6px;
}

.mp-approve {
  background: transparent;
  color: #5fcf5f;
  border: 1px solid #5fcf5f;
  padding: 4px 9px;
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  margin-right: 4px;
  min-width: 28px;
}
.mp-approve:hover { background: #5fcf5f; color: var(--bg); }

.mp-deny {
  padding: 4px 9px;
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  min-width: 28px;
}

/* Waiting-to-join pane (the pending joiner's view) */
.mp-pending-wait {
  text-align: center;
  padding: 24px 8px;
  display: grid;
  gap: 6px;
  justify-items: center;
}
.mp-pending-wait h3 { margin: 8px 0 0; font-size: 16px; }
.mp-pending-wait .mp-pending-code { color: var(--dim); font-size: 12px; margin: 4px 0 0; }
.mp-pending-wait .mp-pending-code strong {
  color: var(--ink);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: 0.04em;
}
.mp-spinner {
  width: 28px; height: 28px;
  border: 3px solid var(--panel-border, rgba(255,255,255,0.12));
  border-top-color: var(--accent, #ffd84d);
  border-radius: 50%;
  animation: mp-spin 0.9s linear infinite;
}
@keyframes mp-spin { to { transform: rotate(360deg); } }

/* ---------------- Toast system ----------------
 *
 *  Toasts live in a stack pinned to the bottom-right of the viewport.
 *  Each toast shows a coloured outline drawn AROUND ITS WHOLE PERIMETER
 *  on the first frame, which then erases over the toast's TTL — when
 *  the outline is gone, the toast fades and is removed.
 */

#mp-toast-stack {
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 100000;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-end;
  pointer-events: none;
  max-height: calc(100vh - 32px);
  overflow: visible;
}
#mp-toast-stack .mp-toast { pointer-events: auto; }

.mp-toast {
  position: relative;
  background: var(--bg-2, #0f0f17);
  border: 1px solid var(--panel-border, rgba(255,255,255,0.12));
  border-radius: 10px;
  padding: 14px 16px;
  max-width: 320px;
  color: var(--ink);
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  /* IMPORTANT: NOT overflow:hidden — the SVG outline strokes on the
     edge and any clipping would shave a half-pixel off it. */
  animation: mp-toast-in 200ms ease-out;
}
@keyframes mp-toast-in {
  from { transform: translateY(8px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
.mp-toast-leaving {
  transform: translateY(8px);
  opacity: 0;
  transition: transform 180ms ease-in, opacity 180ms ease-in;
}

.mp-toast .mp-toast-body p { margin: 0; font-size: 13px; line-height: 1.4; }
.mp-toast .mp-toast-actions { display: flex; gap: 8px; margin-top: 10px; }
.mp-toast .mp-toast-actions button {
  flex: 1;
  background: var(--accent); color: var(--bg);
  border: 0; padding: 6px 10px; border-radius: 6px;
  font: inherit; font-weight: 700; cursor: pointer; font-size: 12px;
}
.mp-toast .mp-toast-actions button.mp-secondary {
  background: transparent; color: var(--dim);
  border: 1px solid var(--panel-border);
  font-weight: 400;
}

/* Animated perimeter outline — one SVG <rect> tracing the toast's
   border. Starts fully drawn, erases over the toast's TTL.
   pathLength=100 normalises the perimeter so stroke-dashoffset goes
   linearly from 0 (fully visible) to 100 (fully erased) regardless
   of the toast's actual size. */
.mp-toast-outline {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* `overflow: visible` so the stroke doesn't get clipped. */
  overflow: visible;
}
.mp-toast-outline rect {
  /* SVG2 lets <rect> read width/height/x/y/rx from CSS. */
  x: 1px;
  y: 1px;
  width:  calc(100% - 2px);
  height: calc(100% - 2px);
  rx: 10;
  ry: 10;
  fill: none;
  stroke-width: 2px;
  stroke-linecap: round;
  stroke-dasharray: 100 100;
  stroke-dashoffset: 0;
  /* The dashoffset animation is set inline (per-toast TTL). */
}

/* Variant colours apply to the SVG stroke. */
.mp-toast-info    .mp-toast-outline rect { stroke: var(--accent, #ffd84d); }
.mp-toast-invite  .mp-toast-outline rect { stroke: #6cd0ff; }
.mp-toast-warn    .mp-toast-outline rect { stroke: #ff5f6f; }
.mp-toast-success .mp-toast-outline rect { stroke: #5fcf5f; }

@keyframes mp-toast-outline-shrink {
  from { stroke-dashoffset: 0;   }
  to   { stroke-dashoffset: 100; }
}

/* Outstanding-invite list shown in the party tab when not yet in any party */
.mp-invite-list { list-style: none; padding: 0; margin: 0 0 12px; }
.mp-invite-list li { padding: 6px 0; border-bottom: 1px dashed rgba(255,255,255,0.05); }
.mp-invite-list li:last-child { border-bottom: 0; }

/* ---------------- Drawer sections ---------------- */

.mp-drawer .mp-content {
  flex: 1;
  overflow: auto;
  padding: 8px 0;
}

.mp-section {
  border-top: 1px solid var(--panel-border, rgba(255,255,255,0.08));
}
.mp-section:first-child { border-top: 0; }

.mp-section-header {
  display: flex;
  align-items: center;
  padding: 10px 14px;
  gap: 6px;
}

.mp-section-toggle {
  flex: 1;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 0;
  color: var(--ink, #e8e8f0);
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  cursor: pointer;
  padding: 0;
}
.mp-section-toggle:hover { color: var(--accent, #ffd84d); }
.mp-chevron {
  width: 12px; height: 12px;
  color: var(--dim, #6c6c80);
  transition: transform 150ms ease;
}
.mp-section.open .mp-chevron { transform: rotate(90deg); }

.mp-section-add {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  width: 24px; height: 24px;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.mp-section-add:hover { color: var(--accent); border-color: var(--accent); }
/* Copy-invite-link button — same shape as `.mp-section-add` but
 * carries the copy SVG instead of a glyph. Sits to the LEFT of the
 * action button when the viewer is in a party. */
.mp-section-copy {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--dim);
  width: 24px; height: 24px;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.mp-section-copy svg { width: 14px; height: 14px; }
.mp-section-copy:hover { color: var(--accent); border-color: var(--accent); }
/* Header [flex: 1 chevron+label] | copy | × — the section header's
 * native gap already spaces the trailing buttons; no extra margin. */
/* Leave-party variant of the party-section action button: same shape,
 * danger-red hover so the destructive intent reads instantly. */
.mp-section-add.mp-section-leave:hover {
  color: var(--danger, #ff5f6f);
  border-color: var(--danger, #ff5f6f);
}

.mp-section-body {
  display: none;
  padding: 0 14px 14px;
}
.mp-section.open .mp-section-body { display: block; }

.mp-section-sub {
  margin: 12px 0 6px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim, #6c6c80);
}

/* Inline friend search reuses the .mp-friend-search styling. */
.mp-friend-search[hidden] { display: none; }

/* The leave button in the party section sits at the bottom of the body. */
.mp-leave { margin-top: 12px; width: 100%; }

/* Tighten the friends list inside the section */
.mp-section ul.mp-friends,
.mp-section ul.mp-requests,
.mp-section ul.mp-members,
.mp-section ul.mp-pending,
.mp-section ul.mp-invite-list { list-style: none; padding: 0; margin: 0; }

/* Rejoin button spacing — inside the party section body */
.mp-rejoin {
  width: 100%;
  margin: 0 0 12px;
  padding: 10px;
}
.mp-game-locked-panel { width: min(420px, 92vw); }

/* Friend-row invite button — outstanding-invite state.
 * Same icon, same position; an animated dashed accent ring + soft
 * pulse tells the user "this invite is in flight" without us
 * spending horizontal space on a "PENDING" chip. The dashes march
 * via `background-position` on a layered conic-style border so the
 * motion stays smooth without a JS timer. */
.mp-friend-invite-pending {
  position: relative;
  cursor: default;
  opacity: 0.95;
  color: var(--accent, #ffd84d) !important;
  border-color: transparent !important;
  /* Layered dashed gradients fake a four-sided dashed border that
   * animates around the button via background-position. We can't
   * animate `border-style: dashed` directly, so we paint our own
   * dashes with `background-image` and let the corners be sharp. */
  background-image:
    repeating-linear-gradient(90deg,  var(--accent, #ffd84d) 0 6px, transparent 6px 12px),
    repeating-linear-gradient(90deg,  var(--accent, #ffd84d) 0 6px, transparent 6px 12px),
    repeating-linear-gradient(0deg,   var(--accent, #ffd84d) 0 6px, transparent 6px 12px),
    repeating-linear-gradient(0deg,   var(--accent, #ffd84d) 0 6px, transparent 6px 12px);
  background-size: 100% 1.5px, 100% 1.5px, 1.5px 100%, 1.5px 100%;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: no-repeat;
  border-radius: 8px;
  animation: mp-invite-pending-march 1.2s linear infinite,
             mp-invite-pending-pulse 1.6s ease-in-out infinite;
}
.mp-friend-invite-pending:disabled { pointer-events: none; }
@keyframes mp-invite-pending-march {
  /* Slide the dash pattern by exactly one cell (12px) so the loop
   * is seamless. Top + bottom slide opposite ways; left + right the
   * same — the four tracks compose into a clockwise march. */
  0%   { background-position: 0    0,    0    100%, 0   0,    100% 0;    }
  100% { background-position: 12px 0,    -12px 100%, 0   -12px, 100% 12px; }
}
@keyframes mp-invite-pending-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 216, 77, 0.45); }
  50%      { box-shadow: 0 0 0 4px rgba(255, 216, 77, 0.0); }
}

/* ===================== Chat dock + window ===================== */

/* Friend-row chat button — same icon-btn style + a small unread badge
   anchored to the top-right corner. */
.mp-friend-chat { position: relative; }
.mp-friend-unread {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  font-size: 10px; font-weight: 700; line-height: 16px;
  color: var(--bg, #07070b);
  background: #ff5f6f;
  border-radius: 999px;
  text-align: center;
  pointer-events: none;
  box-shadow: 0 0 0 1.5px var(--panel-bg, #11111a);
}

/* The dock itself — fixed to the bottom-right corner. The rail is a
   vertical accordion of avatar bubbles; the window opens to its left. */
.mp-chat-dock {
  position: fixed;
  right: 12px;
  bottom: 12px;
  /* Sit ABOVE the multiplayer drawer (9998) and its overlay (9999)
     so the chat window stays visible when the panel is open. */
  z-index: 10002;
  pointer-events: none;
  display: flex; flex-direction: row; align-items: flex-end;
  gap: 8px;
}

/* Chat window — flex column with a header, scrollable body, and an
   inline send form. Sized to a comfortable column on desktop and to
   nearly full screen on mobile. */
.mp-chat-window {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  width: 320px;
  height: min(420px, 70vh);
  background: var(--panel-bg, #11111a);
  border: 1px solid var(--panel-border, #2a2a38);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
}
/* `display: flex` above overrides the browser-default [hidden] rule,
   which would otherwise leave an empty styled box hanging around
   after a chat is closed. Force-hide it. */
.mp-chat-window[hidden] { display: none !important; }

.mp-chat-head {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--panel-border, #2a2a38);
  background: rgba(255, 255, 255, 0.03);
}
.mp-chat-head .mp-avatar {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--row-bg, #1a1a26);
  font-size: 11px; font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--ink-strong, #ffffff);
}
.mp-chat-name { flex: 1; font-size: 14px; font-weight: 600; }
.mp-chat-head .mp-icon-btn { width: 26px; height: 26px; }

.mp-chat-body {
  flex: 1;
  overflow-y: auto;
  padding: 8px 10px;
  display: flex; flex-direction: column;
  background: var(--bg, #07070b);
  /* Theme-aware scrollbar — light pip on dark theme, dark pip on light
     theme. Firefox uses `scrollbar-color`; WebKit/Blink use the
     ::-webkit-scrollbar pseudo-elements below. */
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}
.mp-chat-body::-webkit-scrollbar { width: 8px; }
.mp-chat-body::-webkit-scrollbar-track { background: transparent; }
.mp-chat-body::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 4px;
}
.mp-chat-body::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
}
.mp-chat-loadmore {
  align-self: center;
  font-size: 11px; color: var(--dim, #6c6c80);
  padding: 4px 0 8px;
  letter-spacing: 0.06em;
}
.mp-chat-msgs {
  list-style: none;
  margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
}

.mp-chat-msg {
  display: flex; flex-direction: column;
  max-width: 80%;
  padding: 6px 10px;
  font-size: 13px;
  line-height: 1.35;
  border-radius: 10px;
  word-wrap: break-word;
  overflow-wrap: anywhere;
}
.mp-chat-msg.me {
  align-self: flex-end;
  background: var(--accent, #ffd84d);
  color: var(--bg, #07070b);
  border-bottom-right-radius: 3px;
}
.mp-chat-msg.them {
  align-self: flex-start;
  background: var(--row-bg, #1a1a26);
  color: var(--ink, #e8e8f0);
  border: 1px solid var(--panel-border, #2a2a38);
  border-bottom-left-radius: 3px;
}
.mp-chat-bubble-body { white-space: pre-wrap; }
.mp-chat-ts {
  font-size: 10px;
  color: var(--dim, #6c6c80);
  margin-top: 2px;
  align-self: flex-end;
}
.mp-chat-msg.them .mp-chat-ts { align-self: flex-start; }

.mp-chat-send {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 8px;
  border-top: 1px solid var(--panel-border, #2a2a38);
  background: rgba(255, 255, 255, 0.02);
}
.mp-chat-send input {
  flex: 1;
  background: var(--row-bg, #1a1a26);
  border: 1px solid var(--panel-border, #2a2a38);
  color: var(--ink, #e8e8f0);
  font: inherit; font-size: 13px;
  padding: 6px 10px; border-radius: 6px;
}
.mp-chat-send input:focus { outline: none; border-color: var(--accent, #ffd84d); }
.mp-chat-send button {
  background: var(--accent, #ffd84d);
  color: var(--bg, #07070b);
  border: 0;
  padding: 6px 10px;
  border-radius: 6px;
  font-weight: 700;
  cursor: pointer;
}
.mp-chat-send button:hover { filter: brightness(1.08); }

/* Mobile: stack the chat window above the dock and widen it. */
@media (max-width: 540px) {
  .mp-chat-dock { flex-direction: column-reverse; align-items: flex-end; gap: 6px; right: 8px; bottom: 8px; }
  .mp-chat-window { width: min(360px, calc(100vw - 16px)); height: min(420px, 60vh); }
}

/* ============================================================
 * Avatar enhancements + profile page + avatar editor
 * ============================================================ */

/* mp-avatar with an image or canvas inside — the existing initials
 * styles cover the empty span; once a child is added we hide the text
 * background colour so the picture isn't shadowed by a yellow disc. */
.mp-avatar.has-image {
  background: transparent;
  overflow: hidden;
  padding: 0;
}
.mp-avatar.has-image .mp-avatar-img,
.mp-avatar.has-image .mp-avatar-canvas {
  width: 100%; height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
.mp-avatar.clickable { cursor: pointer; }
.mp-avatar.clickable:hover { filter: brightness(1.1); transform: scale(1.04); }
.mp-avatar { transition: transform 80ms ease, filter 120ms ease; }

/* ---- Profile page ---- */

/* Modal overlay backdrop — sits above the MP drawer / chat dock (whose
 * avatars open the profile) but below the global toast stack. */
.profile-overlay {
  position: fixed; inset: 0;
  background: var(--overlay-bg);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex; align-items: center; justify-content: center;
  z-index: 10010;
  padding: 16px;
}
/* Centered modal panel — same surface language as the settings modal. */
.profile-modal {
  position: relative;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
  color: var(--ink, #e8e8f0);
  width: min(680px, 96vw);
  max-height: min(86vh, 760px);
  overflow-y: auto;
  padding: clamp(20px, 3vw, 32px);
  display: flex; flex-direction: column; gap: 20px;
}
/* Reserve room on the right so a long username never runs under the
 * corner close button. */
.profile-header { display: flex; align-items: flex-start; gap: 16px; padding-right: 40px; }
/* Corner close (X) — mirrors the settings modal's .gs-close. */
.profile-close {
  position: absolute;
  top: 12px; right: 12px;
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--row-bg); color: var(--ink);
  border: 1px solid var(--panel-border); border-radius: 8px;
  cursor: pointer; z-index: 3;
}
.profile-close:hover { background: var(--hover-bg); border-color: var(--dim); }
.profile-close svg { width: 18px; height: 18px; }
.profile-id { display: flex; align-items: center; gap: 16px; flex: 1; min-width: 0; }
.profile-avatar-slot {
  position: relative;
  width: 96px; height: 96px;
  display: inline-flex;
}
.profile-avatar-slot.clickable { cursor: pointer; }
.profile-avatar-slot.clickable:hover .profile-avatar { filter: brightness(1.08); }
.profile-avatar-slot.clickable::after {
  content: '';
  position: absolute; inset: -2px;
  border: 2px dashed transparent;
  border-radius: 50%;
  pointer-events: none;
}
.profile-avatar-slot.clickable:hover::after { border-color: var(--accent, #ffd84d); }
.profile-avatar {
  width: 96px !important; height: 96px !important;
  font-size: 32px;
  letter-spacing: 0;
}
.profile-meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.profile-name { margin: 0; font-size: clamp(20px, 4vw, 32px); }
.profile-tag { margin: 0; color: var(--dim, #9a9aae); font-size: 13px; }

.profile-tabs {
  display: flex; gap: 4px;
  border-bottom: 1px solid var(--panel-border, #2a2a38);
}
.profile-tab {
  background: transparent; border: 0; color: var(--dim, #9a9aae);
  padding: 10px 18px; cursor: pointer; font: inherit; font-size: 14px;
  border-bottom: 2px solid transparent;
}
.profile-tab:hover { color: var(--ink, #e8e8f0); }
.profile-tab.active {
  color: var(--ink, #e8e8f0);
  border-bottom-color: var(--accent, #ffd84d);
}

.profile-tab-panel { padding: 12px 4px; }
.profile-stats {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  max-width: 640px;
}
.profile-stat {
  background: var(--row-bg, #1a1a26);
  border: 1px solid var(--panel-border, #2a2a38);
  border-radius: 12px;
  padding: 16px;
  display: flex; flex-direction: column; gap: 4px;
}
.profile-stat-value { font-size: 28px; font-weight: 800; color: var(--accent, #ffd84d); }
.profile-stat-label { font-size: 12px; color: var(--dim, #9a9aae); letter-spacing: 0.06em; text-transform: uppercase; }

.profile-empty { color: var(--dim, #9a9aae); }

.profile-top { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.profile-top-row {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  gap: 12px;
  padding: 10px 14px;
  background: var(--row-bg, #1a1a26);
  border: 1px solid var(--panel-border, #2a2a38);
  border-radius: 8px;
  align-items: center;
}
.profile-top-name { font-weight: 600; }
.profile-top-plays, .profile-top-best, .profile-top-wins {
  font-size: 12px; color: var(--dim, #9a9aae);
}

.profile-history { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.profile-history-row {
  display: grid;
  grid-template-columns: 1fr auto auto auto auto;
  gap: 12px;
  padding: 8px 12px;
  background: var(--row-bg, #1a1a26);
  border: 1px solid transparent;
  border-radius: 6px;
  align-items: center;
  font-size: 13px;
}
.profile-history-row.won { border-color: rgba(255, 216, 77, 0.4); }
.profile-history-mode, .profile-history-when { color: var(--dim, #9a9aae); font-size: 12px; }
.profile-history-score { font-weight: 700; color: var(--accent, #ffd84d); }
.profile-history-result { color: var(--accent, #ffd84d); font-size: 12px; }

/* ---- Avatar editor modal ---- */

/* Avatar editor opens from inside the profile modal (z-index 10010), so
 * it must sit above it (still below the toast stack). */
.avatar-editor-overlay { z-index: 10020; }
.avatar-editor-panel {
  width: min(520px, calc(100vw - 24px));
  max-height: calc(100vh - 40px);
  overflow: auto;
  padding: 24px;
  display: flex; flex-direction: column; gap: 18px;
}
.avatar-editor-panel h2 { margin: 0; font-size: 20px; }
.avatar-editor-preview { display: flex; justify-content: center; }
.avatar-editor-preview-circle {
  width: 88px !important; height: 88px !important;
  font-size: 28px;
}

.avatar-editor-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.avatar-editor-cell {
  background: var(--row-bg, #1a1a26);
  border: 2px solid var(--panel-border, #2a2a38);
  border-radius: 12px;
  padding: 8px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  cursor: pointer;
  color: var(--ink, #e8e8f0);
  font: inherit;
}
.avatar-editor-cell:hover { border-color: var(--accent, #ffd84d); }
.avatar-editor-cell.selected { border-color: var(--accent, #ffd84d); background: rgba(255, 216, 77, 0.08); }
.avatar-editor-cell-art { width: 56px !important; height: 56px !important; }
.avatar-editor-cell-label { font-size: 11px; color: var(--dim, #9a9aae); text-align: center; }
.avatar-editor-cell-camera {
  background: var(--panel-border, #2a2a38);
  color: var(--ink, #e8e8f0);
}
.avatar-editor-cell-camera svg { width: 28px; height: 28px; }

.avatar-editor-actions, .avatar-editor-crop-actions {
  display: flex; justify-content: flex-end; gap: 8px;
}
/* Accent-colour picker — sits between the avatar grid and the
 * Cancel/Save row. Eight palette swatches; the user's pick gets
 * applied globally as `--accent`. */
.avatar-editor-accent {
  display: flex; flex-direction: column; gap: 6px;
}
.avatar-editor-accent-label {
  font-size: 12px; color: var(--dim, #9a9aae);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.avatar-editor-accent-row {
  display: flex; gap: 8px; flex-wrap: wrap;
}
.avatar-editor-accent-swatch {
  width: 28px; height: 28px; border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  outline-offset: 2px;
  transition: transform 0.08s ease, border-color 0.08s ease;
}
.avatar-editor-accent-swatch:hover { transform: scale(1.08); }
.avatar-editor-accent-swatch.selected {
  border-color: var(--ink-strong, #fff);
  transform: scale(1.08);
}
/* "Default" swatch — yellow base with a hatch overlay so it reads
 * as "no override" vs. a literal yellow pick. */
.avatar-editor-accent-swatch-default {
  background:
    repeating-linear-gradient(
      45deg,
      rgba(0, 0, 0, 0.35) 0 3px,
      transparent 3px 6px
    ),
    #ffd84d !important;
}
.avatar-editor-crop { display: flex; flex-direction: column; gap: 10px; }
.avatar-editor-crop[hidden] { display: none; }
.avatar-editor-crop-help { color: var(--dim, #9a9aae); font-size: 12px; margin: 0; text-align: center; }
.avatar-editor-crop-stage {
  position: relative;
  width: 256px; height: 256px;
  margin: 0 auto;
  border-radius: 12px;
  overflow: hidden;
  touch-action: none;
  cursor: grab;
  user-select: none;
}
.avatar-editor-crop-stage:active { cursor: grabbing; }
.avatar-editor-crop-canvas {
  display: block; width: 100%; height: 100%;
  background: #0a0a14;
}
/* Darken everything outside the crop circle so the contained area
 * reads as the actual output. SVG mask isn't worth the complexity
 * here — a 50%-stop radial gradient gives a clean edge. */
.avatar-editor-crop-mask {
  position: absolute; inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at center, transparent 0 49.5%, rgba(7, 7, 11, 0.72) 50% 100%);
  border-radius: 12px;
}
/* Crisp 1-px outline of the crop circle so the user can see exactly
 * what will be captured. Drawn over the mask via inset shadow on a
 * transparent circle clipped to the stage. */
.avatar-editor-crop-ring {
  position: absolute;
  left: 50%; top: 50%;
  width: 256px; height: 256px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.35),
    inset 0 0 0 1px rgba(0, 0, 0, 0.4);
}
/* Zoom slider row. The − / + glyphs flank the range input so the
 * affordance reads without leaning on the label. */
.avatar-editor-crop-zoom {
  display: flex; align-items: center; gap: 10px;
  margin: 0 auto;
  width: 256px;
}
.avatar-editor-crop-zoom-icon {
  color: var(--dim, #9a9aae);
  font-size: 16px; font-weight: 700;
  width: 14px; text-align: center;
}
.avatar-editor-crop-zoom-input {
  flex: 1;
  -webkit-appearance: none; appearance: none;
  background: transparent;
  height: 18px;
}
.avatar-editor-crop-zoom-input::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px;
  background: var(--panel-border, #2a2a38);
}
.avatar-editor-crop-zoom-input::-moz-range-track {
  height: 4px; border-radius: 2px;
  background: var(--panel-border, #2a2a38);
}
.avatar-editor-crop-zoom-input::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--accent, #ffd84d);
  border: 0;
  margin-top: -5px;
  cursor: pointer;
}
.avatar-editor-crop-zoom-input::-moz-range-thumb {
  width: 14px; height: 14px; border: 0;
  border-radius: 50%;
  background: var(--accent, #ffd84d);
  cursor: pointer;
}

/* Mobile: smaller grid + tighter cells. */
@media (max-width: 540px) {
  .avatar-editor-grid { gap: 8px; }
  .avatar-editor-cell { padding: 6px; }
  .avatar-editor-cell-art { width: 44px !important; height: 44px !important; }
}

/* ============================================================
 * Multiplayer scoreboard — vertical roster pinned at the top-
 * left of the stage. Replaces the single-score chip in MP. One
 * row per party member, leader-at-top by score descending.
 * Per-row tier classes drive the per-player score effects
 * (colour, glow, lightning, fire) mirroring the shared
 * multiplier-badge ladder.
 * ============================================================ */
.mp-scoreboard {
  position: absolute;
  top: 10px;
  left: 10px;
  display: none;
  flex-direction: column;
  gap: 4px;
  z-index: 30;
  pointer-events: none;
  font-family: "Inter", system-ui, sans-serif;
}
.mp-scoreboard.mp-scoreboard-visible { display: flex; }
.mp-scoreboard-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 6px;
  border-radius: 8px;
  background: rgba(7, 7, 11, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--ink, #e8e8f0);
  font-size: 14px;
  line-height: 1;
  min-width: 140px;
  transition: background 0.15s ease, border-color 0.15s ease;
}
/* Light theme: the dark scrim that reads as a subtle bezel against
 * dark game canvases turns into a hard black band on a light page —
 * swap to a soft translucent grey so the row blends with the light
 * background while staying legible over busy game art. */
body.theme-light .mp-scoreboard-row {
  background: rgba(232, 232, 238, 0.85);
  border-color: rgba(0, 0, 0, 0.08);
}
/* No accent ring around your own row — your colour-tinted name is
 * how you find yourself in the list. */
.mp-scoreboard-color {
  width: 4px;
  height: 22px;
  border-radius: 2px;
  flex-shrink: 0;
}
.mp-scoreboard .mp-avatar.sm {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}
.mp-scoreboard-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 110px;
  color: var(--ink, #e8e8f0);
  font-weight: 500;
}
.mp-scoreboard-score {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong, #ffffff);
  font-size: 16px;
  white-space: nowrap;
}
.mp-scoreboard-mult {
  font-weight: 700;
  font-size: 13px;
  opacity: 0.9;
  margin-left: 4px;
}
/* "Picked" badge — pulsing ring around the avatar slot while a
 * teammate is waiting on others to pick during the reward phase. */
.mp-scoreboard-row.mp-scoreboard-picked .mp-avatar.sm {
  box-shadow: 0 0 0 2px var(--accent, #ffd84d), 0 0 12px 0 rgba(255, 216, 77, 0.6);
  animation: mpScoreboardPulse 1.2s ease-in-out infinite;
}
/* "Pending" indicator — a thin spinner orbiting the avatar while the
 * game is waiting on that player's move (set per-row via `pending`).
 * Generic across games: turn-based "their turn", simultaneous-round
 * "still to strike", etc. */
.mp-scoreboard-row.mp-scoreboard-pending .mp-avatar.sm {
  position: relative;
}
.mp-scoreboard-row.mp-scoreboard-pending .mp-avatar.sm::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid transparent;
  border-top-color: var(--accent, #ffd84d);
  border-right-color: rgba(255, 216, 77, 0.35);
  animation: mpScoreboardSpin 0.9s linear infinite;
  pointer-events: none;
}
@keyframes mpScoreboardSpin {
  to { transform: rotate(360deg); }
}

/* Mobile / narrow viewports — the scoreboard would otherwise eat a
 * big chunk of the playfield. Drop the usernames (avatars still
 * identify each row), tighten padding + the minimum width so we
 * fit just the avatar and the compact "1.42M"-style score. */
@media (max-width: 600px) {
  .mp-scoreboard-row {
    min-width: 0;
    padding: 4px 8px 4px 4px;
    gap: 6px;
  }
  .mp-scoreboard-name { display: none; }
  .mp-scoreboard-color { display: none; }
  .mp-scoreboard-score { font-size: 14px; }
  .mp-scoreboard .mp-avatar.sm { width: 24px; height: 24px; }
}
@keyframes mpScoreboardPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}
/* ------- Multiplier tiers on the per-row score number -------
 * Tier 1 (×10+): green glow.
 * Tier 2 (×20+): blue, brighter glow.
 * Tier 3 (×30+): yellow with a fast flicker — the "lightning"
 *                analogue for a DOM element.
 * Tier 4 (×40+): orange with a warm cycling glow.
 * Tier 5 (×50+): red, hot.
 * Each tier nudges the score size up by a couple of px so the
 * row visually grows with the streak. */
.mp-scoreboard-tier-1 .mp-scoreboard-score,
.mp-scoreboard-tier-1 .mp-scoreboard-mult {
  color: #5fcf5f;
  text-shadow: 0 0 8px rgba(95, 207, 95, 0.85);
}
.mp-scoreboard-tier-2 .mp-scoreboard-score,
.mp-scoreboard-tier-2 .mp-scoreboard-mult {
  color: #aef0ff;
  font-size: 18px;
  text-shadow:
    0 0 6px  rgba(62,  197, 245, 0.95),
    0 0 14px rgba(174, 240, 255, 0.7),
    0 0 22px rgba(255, 255, 255, 0.35);
  animation: mpScoreboardIce 1.6s ease-in-out infinite;
}
@keyframes mpScoreboardIce {
  0%, 100% {
    text-shadow:
      0 0 6px  rgba(62,  197, 245, 0.95),
      0 0 14px rgba(174, 240, 255, 0.7),
      0 0 22px rgba(255, 255, 255, 0.35);
    filter: brightness(1.0);
  }
  50% {
    text-shadow:
      0 0 10px rgba(120, 220, 255, 1.00),
      0 0 18px rgba(255, 255, 255, 0.85),
      0 0 28px rgba(174, 240, 255, 0.45);
    filter: brightness(1.18);
  }
}
.mp-scoreboard-tier-3 .mp-scoreboard-score,
.mp-scoreboard-tier-3 .mp-scoreboard-mult {
  color: #ffd84d;
  font-size: 18px;
  text-shadow:
    0 0 8px  rgba(255, 216, 77, 0.95),
    0 0 18px rgba(255, 255, 255, 0.55);
  animation: mpScoreboardLightning 0.18s steps(2, end) infinite;
}
@keyframes mpScoreboardLightning {
  0%   { text-shadow: 0 0 8px  rgba(255, 216, 77, 0.95), 0 0 18px rgba(255, 255, 255, 0.85), 1px 0 0 #ffffff; }
  50%  { text-shadow: 0 0 14px rgba(255, 216, 77, 1.00), 0 0 24px rgba(255, 255, 255, 0.55), -1px 0 0 #ffffff; }
  100% { text-shadow: 0 0 8px  rgba(255, 216, 77, 0.95), 0 0 18px rgba(255, 255, 255, 0.85); }
}
.mp-scoreboard-tier-4 .mp-scoreboard-score,
.mp-scoreboard-tier-4 .mp-scoreboard-mult {
  color: #ffffff;
  font-size: 19px;
  text-shadow:
    0 0 6px  rgba(255, 138, 60, 1),
    0 0 14px rgba(255, 95,  111, 0.9),
    0 0 22px rgba(255, 174, 115, 0.7);
  animation: mpScoreboardFire 0.45s ease-in-out infinite;
}
.mp-scoreboard-tier-5 .mp-scoreboard-score,
.mp-scoreboard-tier-5 .mp-scoreboard-mult {
  color: #ffffff;
  font-size: 20px;
  text-shadow:
    0 0 8px  rgba(255, 48,  48, 1),
    0 0 16px rgba(255, 95,  111, 1),
    0 0 26px rgba(255, 255, 255, 0.7);
  animation: mpScoreboardFireHot 0.30s ease-in-out infinite;
}
@keyframes mpScoreboardFire {
  0%, 100% { transform: translateY(0);    filter: hue-rotate(0deg); }
  50%      { transform: translateY(-1px); filter: hue-rotate(-8deg) brightness(1.15); }
}
@keyframes mpScoreboardFireHot {
  0%, 100% { transform: translateY(0);    filter: hue-rotate(0deg)  brightness(1.0); }
  50%      { transform: translateY(-1px); filter: hue-rotate(-12deg) brightness(1.3); }
}
