:root {
  color-scheme: dark;
  --bg: #07111d;
  --panel: rgba(7, 16, 30, 0.95);
  --line: rgba(255, 255, 255, 0.15);
  --blue: #4a9eff;
  --red: #ff5a5a;
  --gold: #ffcc2e; /* Neutral Flag's single flag, matches client.js's COLORS.neutral */
  --teal: #2dd4bf;
  --violet: #a78bfa;
  --slate: #94a3b8;
  --green: #4ade80; /* Ultimate Frisbee's mode-card accent — an open-field green, distinct from every other mode's accent */
  --orange: #fb923c; /* Soccer's mode-card accent — a soccer-ball/cleats energy, distinct from Ultimate's own field green */
  --rose: #fb7185; /* Tournaments' mode-card accent — a championship-ribbon energy, distinct from --red (already the Survivor card's own accent) */
  --text: #e8eaee;
}
* { box-sizing: border-box; }
/* .hud/.footer/etc. set `display` explicitly, which otherwise beats the
   browser's default `[hidden] { display: none }` UA-stylesheet rule (author
   CSS always wins over UA styles) — without this override, toggling the
   `hidden` attribute in JS would silently do nothing on those elements. */
[hidden] { display: none !important; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 22px;
  background: radial-gradient(circle at top, #14314f 0%, var(--bg) 55%, #050913 100%);
  color: var(--text);
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.frame {
  width: min(1040px, 100%);
  border-radius: 22px;
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
  background: var(--panel);
  position: relative; /* positioning context for #vgsMenu, which now lives at frame level so comms work on lobby screens too */
}

/* Waiting-room / draft-screen chat feed for VGS comms (the V-key menu
   works on those screens now, not just in-game). */
.roomChatFeed { margin-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 10px; }
.roomChatHint { color: var(--text-dim); font-size: 0.78rem; margin-bottom: 6px; }
.roomChatHint kbd { border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 4px; padding: 0 5px; font-size: 0.72rem; }
.roomChatLines { display: flex; flex-direction: column; gap: 3px; min-height: 20px; max-height: 120px; overflow-y: auto; font-size: 0.85rem; }
.roomChatLine .chatName { font-weight: 700; }
.roomChatLine.blue .chatName { color: var(--blue); }
.roomChatLine.red .chatName { color: var(--red); }

/* Fixed to the viewport (not `.frame`) so it stays in the same spot
   regardless of which screen is showing, including the borderless
   fullscreen game view (see .frame.gameMode). */
.audioControls {
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 6px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(7, 16, 30, 0.72);
  backdrop-filter: blur(6px);
}
/* Shown while a genuine network drop is auto-reconnecting mid-match (see
   public/client.js's "disconnect" handler) — the game screen stays up
   underneath so a successful reconnect snaps right back with no
   lobby-then-game flicker; this is just a heads-up that something's
   happening, not a full loading-screen takeover. */
.reconnectingOverlay {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  padding: 8px 16px;
  border-radius: 999px;
  border: 1px solid rgba(255, 200, 90, 0.4);
  background: rgba(30, 20, 5, 0.85);
  backdrop-filter: blur(6px);
  color: #ffcf6b;
  font-size: 0.9rem;
  font-weight: 600;
}
.audioBtn {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 0.95rem;
  cursor: pointer;
  line-height: 1;
}
.audioBtn:hover { background: rgba(255, 255, 255, 0.12); }
.audioBtn.muted { opacity: 0.4; }
.audioTrackLabel {
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.65);
  transition: max-width 0.4s ease, margin-left 0.4s ease;
}
.audioTrackLabel.show { max-width: 160px; margin-left: 4px; }
.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.045);
}
.brandMark { display: flex; align-items: center; gap: 10px; }
.brandIcon { width: 34px; height: 34px; display: block; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.4)); }
.brandText { display: flex; flex-direction: column; line-height: 1.15; }
.brandTitle { font-size: 1.1rem; font-weight: 800; letter-spacing: 0.01em; }
.brandSubtitle { font-size: 0.66rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255, 255, 255, 0.45); }
.topbarRight { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
.accountBar { display: flex; align-items: center; gap: 8px; }
.accountBar .secondary { width: auto; margin-top: 0; padding: 7px 12px; font-size: 0.85rem; }
.accountSignedIn { display: flex; align-items: center; gap: 8px; }
.accountAvatar { width: 26px; height: 26px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.25); }
#accountName { font-size: 0.9rem; font-weight: 600; }
/* Signed-in account dropdown: the name/avatar is the trigger; Daily Tasks,
   Shop, Settings, Sign out live in the list. */
.accountMenu { position: relative; }
.accountMenuBtn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 10px 5px 6px; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.07);
  color: inherit; font: inherit; cursor: pointer;
}
.accountMenuBtn:hover, .accountMenu.open .accountMenuBtn { background: rgba(255, 255, 255, 0.14); }
.accountMenuCaret { font-size: 0.75rem; opacity: 0.7; }
.accountMenuList {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  min-width: 190px; padding: 6px;
  display: flex; flex-direction: column; gap: 2px;
  border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(10, 20, 36, 0.98); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.accountMenuItem {
  display: block; width: 100%; text-align: left;
  padding: 9px 12px; border-radius: 8px; border: 0; background: transparent;
  color: rgba(255, 255, 255, 0.9); font: inherit; font-size: 0.9rem; cursor: pointer;
}
.accountMenuItem:hover { background: rgba(255, 255, 255, 0.1); }
.accountMenuSep { height: 1px; margin: 4px 6px; background: rgba(255, 255, 255, 0.12); }
/* Discord CTA — brand "blurple", the look people recognize from every
   community site, so it stands out from the neutral nav buttons. */
.discordBtn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 14px; border-radius: 8px;
  background: #5865F2; color: #fff; text-decoration: none;
  font-weight: 700; font-size: 0.85rem; border: 1px solid #5865F2;
  box-shadow: 0 2px 10px rgba(88, 101, 242, 0.35);
}
.discordBtn:hover { background: #4752C4; border-color: #4752C4; }
.discordLogo { width: 18px; height: 14px; display: block; }
.hud { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.pill {
  padding: 8px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.07);
  font-size: 0.95rem;
}
.pill.blue strong { color: var(--blue); }
.pill.red strong { color: var(--red); }
.pill.you-blue { border-color: rgba(74, 158, 255, 0.55); color: var(--blue); }
.pill.you-red { border-color: rgba(255, 90, 90, 0.55); color: var(--red); }
.pill.you-spectate { opacity: 0.75; }
button.reset {
  padding: 8px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  font-size: 0.88rem;
  cursor: pointer;
}
button.reset:hover { background: rgba(255, 255, 255, 0.13); }
.stage { position: relative; }
canvas { display: block; width: 100%; height: auto; background: #12151a; }

/* Fullscreen-style in-game view (see showScreen() in client.js) — canvas
   fills essentially the whole browser window, with only a thin transparent
   overlay strip top-left (leave/status pills, flipped there via
   flex-direction so the SAME .topbar/.hud markup used everywhere else
   doesn't need duplicating) and the fixed #audioControls owning the true
   top-right corner. Real TagPro's layout (exit/status top-left, mute/
   settings top-right, playing field filling everything else) is the model.
   The game's logical camera viewport stays a fixed 1360x800 (shared/camera.js/
   server fog-of-war depend on that number matching) — this only changes
   how large that fixed-resolution canvas is DRAWN on screen, scaled up via
   object-fit rather than the map itself becoming resolution-dependent. */
.frame.gameMode {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  border: none;
  box-shadow: none;
  display: flex;
  flex-direction: column;
  z-index: 10;
}
.frame.gameMode .topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  /* .brandMark below is display:none, leaving .hud as the only flex item —
     flex-start pins it to the LEFT edge (the whole point: leave the true
     top-right corner free for #audioControls). */
  justify-content: flex-start;
  border-bottom: none;
  background: linear-gradient(rgba(5, 9, 19, 0.75), transparent);
  padding: 10px 14px;
}
/* The brand is an <a> since frontpage v2 — the old `> div:first-of-type`
   selector (meant for it) was instead hiding .topbarRight, which killed
   the ENTIRE in-game HUD row (you-pill, ability-cooldown pill, Reset/
   Leave) in every mode. Owner noticed the missing cooldown pill in
   Hockey; explicit selectors now. */
.frame.gameMode .brandMark { display: none; }
.frame.gameMode .accountBar { display: none; }
.frame.gameMode .hud { gap: 8px; }
.frame.gameMode .footer { display: none; }
.frame.gameMode .stage {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
}
.frame.gameMode canvas {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Touch controls (mobile) — a virtual joystick + 2 buttons overlaid on
   the canvas, shown ONLY on coarse-pointer (touch) devices. A laptop
   with a touchscreen that also has a keyboard/mouse never sees these —
   `pointer: coarse` only matches the device's PRIMARY pointer, and stays
   hidden here rather than trying to feature-detect "has touch," which
   would wrongly show it on such laptops too. See client.js's touch-input
   block for how these feed the exact same up/down/left/right/ability/
   detonate booleans the keyboard already produces. */
.touchControls {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none; /* only the joystick base + buttons themselves opt back in */
  user-select: none;
  -webkit-user-select: none;
}
@media (pointer: coarse) {
  .touchControls { display: block; }
}
.touchJoystick {
  position: absolute;
  left: 22px;
  bottom: 22px;
  width: 124px;
  height: 124px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  border: 2px solid rgba(255, 255, 255, 0.28);
  pointer-events: auto;
  touch-action: none; /* stop the page from panning/zooming while dragging */
}
.touchJoystickThumb {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 52px;
  height: 52px;
  margin: -26px 0 0 -26px;
  border-radius: 50%;
  background: rgba(74, 158, 255, 0.55);
  border: 2px solid rgba(74, 158, 255, 0.9);
  pointer-events: none; /* the base circle owns the drag, not the thumb graphic */
}
.touchButtons {
  position: absolute;
  right: 20px;
  bottom: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  pointer-events: none;
}
.touchBtn {
  pointer-events: auto;
  touch-action: none;
  width: 68px;
  height: 68px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  font-size: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.touchBtn.active { background: rgba(74, 158, 255, 0.4); border-color: rgba(74, 158, 255, 0.9); }
/* Meter rings (owner, 2026-09-29: "use the control indicator to also
   indicate amount ... a ring around the circle to indicate the amount
   remaining"): Turbo around the ability button, Heat around shoot. Hockey
   only; client.js updateTouchMeterRings sets --ring (0-1) and --ringColor. */
.touchBtn { position: relative; }
.touchBtn.meterRing::before {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: 50%;
  background: conic-gradient(var(--ringColor, #7cc8ff) calc(var(--ring, 1) * 360deg), rgba(255, 255, 255, 0.13) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 5px));
  pointer-events: none;
}
.touchBtn.meterRing.ringFull::before { animation: meterRingPulse 0.5s ease-in-out infinite alternate; }
@keyframes meterRingPulse { from { opacity: 0.55; } to { opacity: 1; } }
.touchBtnAbility.onCooldown { opacity: 0.4; } /* mirrors the powerup pill's "X.Xs cooldown" text, at a glance without reading it */

/* Controls legend (owner request 2026-08-24, Hockey-only originally;
   generalized 2026-08-28 to every mode) — a quiet reference card along the
   left edge, desktop only. Touch devices already have their own on-screen
   buttons (.touchControls above), so this hides itself there rather than
   doubling up. */
.controlsLegend {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 10px 12px;
  border-radius: 10px;
  background: rgba(7, 16, 30, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(3px);
  max-width: 170px;
}
@media (pointer: coarse) {
  .controlsLegend { display: none; }
}
.controlsLegendRow { display: flex; align-items: center; gap: 8px; font-size: 0.78rem; color: rgba(255, 255, 255, 0.82); }
.controlsLegendKey {
  /* A gamepad action can have more than one button bound (owner: "both
     LB and Y... can both be pass") — that label can run long enough to
     need real width. flex-shrink:0 here used to mean it NEVER shrank or
     wrapped, so a long one just overflowed the panel's own rounded box
     onto the playfield underneath (owner report 2026-08-28, live
     screenshot). min-width used to be small enough that even "WASD" (one
     of the shortest, most common keyboard labels) wrapped to "WAS"/"D"
     on two lines (owner report 2026-09-02) — raised to comfortably fit
     WASD on one line; flex-shrink:1 + overflow-wrap below still lets a
     genuinely long gamepad combo shrink/wrap past that floor exactly like
     before. */
  flex-shrink: 1;
  min-width: 52px;
  max-width: 100%;
  overflow-wrap: break-word;
  padding: 2px 6px;
  text-align: center;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(255, 255, 255, 0.08);
  font-weight: 700;
  font-size: 0.7rem;
  font-family: ui-monospace, monospace;
}

/* "Rotate your device" prompt — shown ONLY while actively in a match
   (.frame.gameMode) on a touch device currently held in portrait.
   Landscape is the only supported orientation for real play: the game's
   camera viewport is a fixed 1360x800 (~16:9) logical resolution (see
   .frame.gameMode canvas above), which letterboxes down to an unplayable
   sliver in portrait — confirmed visually before adding this prompt.
   Pure CSS, no JS: rotating the phone changes which media query matches,
   so this shows/hides itself automatically with zero event-handling code. */
.rotatePrompt { display: none; }
@media (pointer: coarse) and (orientation: portrait) {
  .frame.gameMode .rotatePrompt {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 100; /* above .touchControls (15) and everything else in gameMode */
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    background: rgba(5, 9, 19, 0.96);
    color: var(--text);
    text-align: center;
    padding: 32px;
  }
}
.rotateIcon { font-size: 3rem; animation: rotateHint 1.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .rotateIcon { animation: none; } }
@keyframes rotateHint { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-90deg); } }
.rotatePromptTitle { font-size: 1.15rem; font-weight: 800; }
.rotatePromptCopy { font-size: 0.9rem; color: rgba(255, 255, 255, 0.65); max-width: 280px; }
kbd {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.08);
  font-family: inherit;
  font-size: 0.85em;
}
/* Perf readout (FPS/ping) — same spot/style real TagPro uses (plain small
   text overlaying the canvas, not a pill). Positioning now lives on its
   parent, .hudStack (see that rule's own comment). */
.perfStats {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.55);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* --- top wide message HUD (Tribes-1-style ticker): system events, match
   countdown, and VGS chat lines, replacing the old floating speech bubbles
   drawn over the ball (they overlapped play) --- */
.hudTicker {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: min(94%, 640px);
  max-height: 60px; /* ~3 lines at this font, matching HUD_TICKER_MAX_LINES (client.js) — trimmed from 4 lines/80px 2026-09-08, 6 lines/118px 2026-09-02 */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 12px;
  border-radius: 8px;
  background: rgba(7, 16, 30, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.08);
  pointer-events: none;
  font-size: 0.82rem;
  line-height: 1.35;
}
/* Vertical rink (client.js viewRot): the ice runs top to bottom, so the
   top-centre strip sits right over the far end of the play — and on a wide
   window the rink fills the width. The side columns beside the boards are
   the least important pixels on screen, so the ticker moves to the right,
   under the audio controls, and wraps instead of truncating. */
.frame.gameMode.verticalView .hudTicker {
  left: auto;
  right: 12px;
  top: 64px;
  transform: none;
  width: min(26%, 320px);
  max-height: 150px;
}
.frame.gameMode.verticalView .hudTicker .tickerLine { white-space: normal; }
/* Vertical view on a phone held upright (client.js syncCanvasShape): no
   need to rotate — hide the prompt — and the ticker goes back across the
   top, full width, since there are no side columns in portrait. */
.frame.gameMode.verticalView .rotatePrompt,
.frame.gameMode.verticalPref .rotatePrompt { display: none !important; } /* verticalPref: set from the setting, before the first render (client.js syncVerticalPrefClass) */
/* Upright phone (owner, 2026-09-29, on-device screenshot: the Turbo bar
   and ticker covered play, Leave room sat by the joystick). Nothing sits on
   the ice any more: the canvas lives between an 82px top band (score bar +
   audio, then one ticker line with Leave room at its right end — away from
   the thumbs) and a 150px bottom band for the controls. Keep both in sync
   with client.js PORTRAIT_TOP_RESERVE_PX / PORTRAIT_CONTROLS_RESERVE_PX.
   Turbo and Heat are rings on the ⚡ and shoot buttons instead of a bar. */
.frame.gameMode.portraitCanvas .stage { padding: 82px 0 150px; align-items: stretch; }
.frame.gameMode.portraitCanvas canvas { height: 100%; object-position: center top; }
.frame.gameMode.verticalView.portraitCanvas .hudTicker { left: 8px; right: 92px; top: 52px; width: auto; transform: none; max-height: 26px; padding: 3px 10px; font-size: 0.74rem; } /* newest line only */
.frame.gameMode.verticalView.portraitCanvas .hudTicker .tickerLine { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; }
.frame.gameMode.verticalView.portraitCanvas .hudTicker .tickerLine:not(:last-child) { display: none; }
.frame.gameMode.portraitCanvas .perfStats { display: none; }
.frame.gameMode.portraitCanvas .arcadeMetersBar { display: none !important; }
.frame.gameMode.portraitCanvas .touchJoystick { left: 14px; bottom: 18px; width: 112px; height: 112px; }
.frame.gameMode.portraitCanvas .touchButtons { right: 16px; bottom: 34px; flex-direction: row; gap: 18px; }
.frame.gameMode.portraitCanvas .touchBtn { width: 60px; height: 60px; font-size: 1.3rem; }
.hudTicker .tickerLine {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  animation: tickerIn 0.18s ease-out;
}
.hudTicker .tickerLine .tickerName { font-weight: 700; }
.hudTicker .tickerLine.chat.blue .tickerName { color: var(--blue); }
.hudTicker .tickerLine.chat.red .tickerName { color: var(--red); }
.hudTicker .tickerLine.system { color: #ffd24a; }
.hudTicker .tickerLine.capture { color: #7cff9e; font-weight: 600; }
.hudTicker .tickerLine.tag { color: rgba(255, 255, 255, 0.75); }
.hudTicker .tickerLine.powerup { color: #b388ff; }
.hudTicker .tickerLine.countdown { color: #fff; font-weight: 700; text-align: center; }
@keyframes tickerIn {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

/* --- bottom scoreboard/timer bar --- */
.matchBar {
  position: absolute;
  left: 50%;
  bottom: 10px; /* keep in sync with positionArcadeMetersBar's own "10" (client.js) */
  transform: translateX(-50%);
  display: flex;
  align-items: stretch;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
  font-weight: 700;
  user-select: none;
  transition: opacity 0.25s ease;
}
/* Ghosted while a live ball is rendered underneath (see render()'s
   hudOccluded) — the play matters more than the chrome in that moment. */
.matchBar.ghosted, .flagStatusPill.ghosted { opacity: 0.15; }
/* Owner, 2026-09-08 (2nd + 3rd passes — the DESKTOP/bottom bar this
   time, not the coarse-pointer one above): "the bottom scoreboard HUD
   .. we need to not overlap the playfield as much" + "knock the font
   size down a point", then, live screenshot, still stacked tight
   against #arcadeMetersBar above it: "This bottom is still too much
   too .. shrink the font size a bit. In the crazygames iframe, we're
   just overlapping too much of the rink." This is the bar real
   CrazyGames traffic actually sees (that embed is pointer:fine, not
   coarse; the mobile block only matches an actual touchscreen). */
.matchPill { display: flex; align-items: center; gap: 7px; padding: 4px 14px; }
.matchPill.blue { background: linear-gradient(90deg, #2f6fd6, var(--blue)); border-radius: 999px 0 0 999px; padding-right: 18px; }
.matchPill.red { background: linear-gradient(90deg, var(--red), #c23a3a); border-radius: 0 999px 999px 0; padding-left: 18px; flex-direction: row-reverse; }
.matchTeamLabel { font-size: 0.66rem; letter-spacing: 0.06em; }
.matchScoreBox {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5em;
  padding: 0px 7px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.94);
  color: #12151a;
  font-size: 0.88rem;
}
.matchDots { display: flex; gap: 4px; }
.matchDots .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, 0.3); }
.matchDots .dot.filled { background: rgba(255, 255, 255, 0.95); }
/* Hockey only (owner request 2026-08-25) — appears in the top bar once
   someone on this team has a hat trick this match; see client.js's
   updateHatTrickBadges. A gentle pulse (not keychip-pulse's background
   swap — a flat emoji glyph needs a scale beat instead) keeps it from
   just reading as another static icon next to the score. */
.hatTrickBadge { font-size: 1.05rem; line-height: 1; animation: hatTrickBadgePulse 1.6s ease-in-out infinite; }
@keyframes hatTrickBadgePulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.22); } }
.matchTime {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3px 16px;
  background: #12151a;
}
.matchTimeLabel { font-size: 0.54rem; letter-spacing: 0.12em; color: rgba(255, 255, 255, 0.55); }
.matchTimeValue { font-variant-numeric: tabular-nums; font-size: 0.96rem; color: #ffd24a; }
/* Regulation clock inside its last 30s (see updateMatchTime in client.js) —
   also reused for the whole of overtime/Clutch Time, both genuinely urgent. */
.matchTimeValue.urgent { color: var(--red); }
/* Pre-match freeze ("Match starting in N…") — deliberately NOT .urgent's
   red, which reads as "time's about to run out"; this is the opposite. */
.matchTimeValue.starting { color: #7cc4ff; }

/* Personal Turbo/Heat/Ability readout (2026-09-07) — see its own HTML
   comment. Positioned by client.js (positionArcadeMetersBar), not a
   hardcoded bottom offset, so it always clears #matchBar's real
   rendered height regardless of font-size/padding changes to that bar. */
/* Owner, 2026-09-08, live CrazyGames screenshot (Turbo/Heat/Sprint bar
   stacked right on top of the score bar): "This bottom is still too
   much too -- we could maybe reduce the cap between the elements, and
   shrink the font size a bit. In the crazygames iframe, we're just
   overlapping too much of the rink." Padding/gap/track size trimmed
   here; the GAP to #matchBar below it is positionArcadeMetersBar's own
   "+10" (client.js), trimmed alongside this. */
.arcadeMetersBar {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  background: #12151a;
  border-radius: 10px;
  padding: 4px 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
  font-weight: 700;
  user-select: none;
  transition: opacity 0.25s ease;
}
.arcadeMetersBar.ghosted { opacity: 0.15; }
.arcadeMeterItem { display: flex; align-items: center; gap: 6px; }
/* Owner, 2026-09-08, 4th pass, live screenshot: "Drop the font size for
   Turbo/Heat/(Ability) Ready by another point? .. Every pixel counts
   with crazygames tight inframe." */
.arcadeMeterLabel { font-size: 0.56rem; letter-spacing: 0.06em; color: rgba(255, 255, 255, 0.65); }
.arcadeMeterTrack {
  width: 54px;
  height: 6px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
.arcadeMeterFill { height: 100%; border-radius: 4px; transition: width 0.15s linear; }
.arcadeMeterFill.turbo { background: #8fd8ff; } /* same color as the on-ball turbo bar (shared/shop.js's TRAILS.comet) */
.arcadeMeterFill.heat { background: #ff6a00; }
/* Hot Meter flashing full — same idea as the on-ball bar's own flash. */
.arcadeMeterFill.heat.full { animation: arcadeHeatFlash 1s ease-in-out infinite; }
@keyframes arcadeHeatFlash { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
.arcadeMeterAbility { padding-left: 10px; border-left: 1px solid rgba(255, 255, 255, 0.18); font-size: 0.7rem; }
.arcadeMeterAbilityState { color: rgba(255, 255, 255, 0.55); font-variant-numeric: tabular-nums; }
.arcadeMeterAbilityState.ready { color: #6ee06e; }

/* Neutral Flag only ("Flag: open / held by BLUE / held by RED") — hidden
   on classic-CTF rooms, which already show flag status via the two home
   markers on the field itself. Was centered just below the scoreboard;
   moved to the bottom-right corner (2026-09-07, owner screenshot) once
   the new #arcadeMetersBar landed in that exact centered spot and the
   two started overlapping ("Puck: loose covering the new HUD stuff").
   Bottom-right sits clear of every other anchored HUD piece (audio
   controls top-right, #hudStack top-left, Leave Room bottom-left,
   matchBar/arcadeMetersBar/ticker all center) — see render()'s new
   off-screen puck-direction arrow (client.js) for the other half of
   that same owner request, covering the case this pill alone can't:
   knowing which way to look when the puck isn't just off-camera but
   genuinely out of view. */
.flagStatusPill {
  position: absolute;
  right: 20px;
  bottom: 14px;
  transition: opacity 0.25s ease;
  padding: 4px 14px;
  border-radius: 999px;
  background: rgba(7, 16, 30, 0.85);
  border: 1px solid var(--line);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: var(--gold);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.flagStatusPill.held-blue { color: var(--blue); }
.flagStatusPill.held-red { color: var(--red); }
/* Ultimate Frisbee's own pill (#discStatusPill) reuses this exact class —
   .stalling overrides held-blue/held-red's color once a mark has started
   the stall clock, same urgency signal as .matchTimeValue.urgent. */
.flagStatusPill.stalling { color: var(--red); }

/* Owner reports 2026-09-01 — first a real phone, then (same complaint,
   same screenshot shape) a smaller desktop browser window testing
   puckit.gg: the top HUD row (You:/ability-cooldown/Leave Room, from
   .frame.gameMode .topbar .hud) collides with the score bar + message
   ticker above it whenever the window isn't wide enough for both to fit
   side by side — NOT a touch-only problem, so these three live as
   universal rules rather than inside the @media (pointer: coarse) block
   below (which still layers a few mobile-only deltas on top — joystick
   clearance, smaller type, a shorter ticker to clear).
   1. The "You: Red (name)" pill is dropped outright — a player already
      sees which ball is theirs on the field; it wasn't earning its share
      of contested screen space.
   2. The ability/shot pill drops below the score bar + ticker's own
      maximum extent (ticker: top 10px, max-height 118px on desktop)
      instead of sharing that horizontal band with them — a vertical
      stack avoids the collision at any window width, where a sideways
      dodge wouldn't (the ticker is centered and can be up to 640px wide).
   3. Leave Room moves to the bottom-left, out of the top HUD's way
      entirely. position:fixed (not absolute) since its ancestor .topbar
      is only a thin strip pinned to the true top of the screen, not the
      full frame. */
#youPill { display: none; }
/* 2026-09-02 — the powerup/ability pill, the controls legend, and the
   perf readout are ONE real flex column now (index.html's #hudStack),
   top-left, in that order (owner: "I'd almost move that [FPS/Ping]
   below the controls"). This replaces an earlier attempt at the same
   top-left goal that positioned #powerupPill via .topbar .hud's own
   flow while .perfStats/.controlsLegend stayed independently
   magic-number-positioned elsewhere in .stage — the moment the pill's
   height could vary (1 line vs. wrapped), those fixed offsets stopped
   lining up and the owner reported the exact "spacing is a bit off"
   result. A real flex-column + gap can't drift like that: whatever
   height the pill actually renders at, its siblings below it just move
   with it. Mobile keeps #powerupPill's own separately-tuned fixed+
   centered position (below, in its media-query block) — position:fixed
   escapes a flex parent's layout regardless of where in the DOM it sits,
   so moving this element into #hudStack didn't require touching that. */
/* Mini-game leaderboard, top right. Sized to be readable at a glance
   mid-match and then ignored — it's a target, not a panel to study. */
/* The lobby bench — group members sitting a game out. Visually quieter
   than a team column (it isn't a side) but not tucked away, because who
   is sitting is part of the group's own negotiation. */
.benchArea { margin-top: 14px; }
.benchArea h3 {
  margin: 0 0 6px; font-size: 0.78rem; letter-spacing: 0.05em;
  text-transform: uppercase; color: rgba(255, 255, 255, 0.45);
}
.benchArea .rosterRow { background: rgba(255, 255, 255, 0.04); }
/* A lock reads as a state, not a button — it is something done TO you. */
.lockChip {
  margin-left: 6px; padding: 1px 6px; border-radius: 6px;
  background: rgba(255, 210, 77, 0.16); border: 1px solid rgba(255, 210, 77, 0.4);
  font-size: 0.7rem; color: #ffd24d; white-space: nowrap;
}
.rosterSitOut, .rosterBenchJoin, .rosterLock {
  padding: 3px 9px; border-radius: 7px; border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.06); color: inherit; font: inherit;
  font-size: 0.76rem; cursor: pointer;
}
.rosterSitOut:hover, .rosterBenchJoin:hover, .rosterLock:hover { background: rgba(255, 255, 255, 0.12); }
/* A full side is shown disabled rather than hidden — a missing button
   reads as a bug, a greyed one reads as "that side is full". It has to
   LOOK disabled to do that job, though; without this it was identical to
   a live button and strictly worse than hiding it. */
.rosterBenchJoin:disabled {
  opacity: 0.38; cursor: not-allowed; background: transparent;
  border-style: dashed; color: rgba(255, 255, 255, 0.55);
}
.rosterBenchJoin:disabled:hover { background: transparent; }
.rosterLock.on { background: rgba(255, 210, 77, 0.2); border-color: rgba(255, 210, 77, 0.45); color: #ffd24d; }

/* Restart — stacked directly above Leave Room in the bottom-left corner
   (owner, 2026-09-15: "we should consider moving the restart button
   somewhere more accessible, as people will often want to hit it if they
   start failing in a time trial minigame"). Where it came FROM was the
   top HUD row, which in game mode is a thin absolute strip that #hudStack
   (powerup pill / controls legend / FPS, top:36 left:14) sits directly
   under — so the button overlapped them, which is the other half of the
   same report.
   Corners are the cheapest targets on screen to hit, and Leave Room
   already owns this one, so Restart joins it rather than inventing a
   fifth cluster: position:fixed and left:14px are copied from #leaveBtn
   deliberately, and bottom clears it (leave: bottom 20 + ~34 tall) with a
   10px gap. The gap plus the accent fill are what keep "restart my run"
   from being mistaken for "leave the room"; they were side-by-side
   before, which is the worse version of the same adjacency. R does the
   same thing without any pointer travel at all. */
.minigameRestart {
  position: fixed;
  left: 14px;
  bottom: 64px;
  z-index: 21; /* the topbar strip is 20; this is a sibling overlay, not part of it */
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  border-radius: 999px;
  border: 1px solid rgba(110, 170, 255, 0.5);
  background: rgba(24, 52, 92, 0.88);
  color: #eaf2ff;
  font: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
.minigameRestart:hover { background: rgba(38, 78, 132, 0.95); border-color: rgba(150, 200, 255, 0.75); }
.minigameRestart:active { transform: scale(0.96); }
.minigameRestart:disabled { opacity: 0.45; cursor: default; transform: none; }
.minigameRestartIcon { font-size: 1rem; }
.minigameRestartKey {
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.78);
}
@media (pointer: coarse) {
  /* No keyboard to show a key for, and Leave Room is already lifted to
     bottom:154px here to clear the touch joystick (see its own rule) —
     so this stacks above THAT, not above the desktop position. */
  .minigameRestartKey { display: none; }
  .minigameRestart { bottom: 188px; padding: 5px 10px; font-size: 0.72rem; }
}

.mgRecords {
  position: absolute;
  /* top/left are set from the CANVAS box in JS (anchorRecordsToRink) —
     .stage is wider than the rink on a widescreen, so anchoring here
     put the panel out in the letterbox over the music controls. */
  top: 36px;
  right: 14px;
  z-index: 15;
  min-width: 168px;
  max-width: 230px;
  padding: 8px 11px 9px;
  border-radius: 10px;
  /* Translucent, because it now sits INSIDE the rink's top-right corner
     rather than out in the letterbox (owner: "somewhere more
     visible/prominent, but not interfering with game play... have it go
     translucent when needed"). The amber border keeps it findable at a
     glance while the fill lets the ice through. */
  background: rgba(7, 17, 29, 0.55);
  border: 1px solid rgba(255, 210, 77, 0.45);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(2px);
  font-size: 0.8rem;
  line-height: 1.4;
  overflow: hidden; /* belt and braces: nothing inside may paint outside the panel */
  pointer-events: none; /* never steals a click during play */
}
.mgRecordsTitle {
  font-size: 0.66rem; letter-spacing: 0.07em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5); margin-bottom: 4px;
  /* WRAPS. This was nowrap, which was fine while every board label was
     short ("Fastest sweep") and broke the moment one was not: "Most floor
     held — solo · this week" ran straight out of the 230px panel and was
     clipped mid-word. Wrapping fixes it for any label length rather than
     for the one that happened to overflow — the next long label would
     have reopened this. */
  white-space: normal;
  overflow-wrap: anywhere;
}
.mgRecordsRow { display: flex; gap: 6px; align-items: baseline; white-space: nowrap; }
.mgRecordsRow .rank { color: rgba(255, 255, 255, 0.4); width: 12px; flex: none; }
.mgRecordsRow .who { overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; }
.mgRecordsRow .val { color: #ffd24d; font-variant-numeric: tabular-nums; flex: none; }
/* A record set against bots is marked rather than hidden — the owner's
   call ("count, but mark them"), since otherwise the boards sit empty.
   It qualifies the TIME, so it lives with the time. It used to be a 🤖
   sitting immediately after the player's NAME, which is exactly where
   this game puts a player's flair (see the degree/ladder flairs), so it
   read as "this player is a bot" — owner, 2026-09-11, of a board showing
   two real people: "these are both real users, but appear to be using bot
   flairs on the leaderboard." Words rather than an emoji for the same
   reason: an icon in a badge position is a claim about the person. */
.mgRecordsRow .val .vsBots {
  margin-left: 5px;
  font-size: 0.72em;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.4);
  text-transform: uppercase;
}
.mgRecordsEmpty { color: rgba(255, 255, 255, 0.45); }
@media (max-width: 620px) { .mgRecords { display: none; } }
.hudStack {
  position: absolute;
  /* Owner, live screenshots at the real CrazyGames iframe height: "Might
     need to drop the HUD 1 more line too so there's less text
     overlapping the play field." A little extra top clearance so the
     ability pill doesn't start flush against the very top edge, now that
     it's usually the only line here (controls legend defaults hidden,
     FPS/Ping defaults off — see their own comments). */
  top: 36px;
  left: 14px;
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  pointer-events: none;
}
/* Text itself trimmed considerably (client.js's updatePowerupPill) so
   the width below rarely needs the wrap at all anymore — capped/wrapped
   as a safety net for the rare busy case (several powerups + an ability
   status at once), not as the primary fix the way it was before. */
#powerupPill {
  max-width: 240px;
  white-space: normal;
}
/* Owner request 2026-09-02: "a smaller font countdown clock" — the
   numeric remaining-time span client.js's updatePowerupPill now wraps
   every countdown in (⚡ 3.1s, Charge 8.9s, etc.), de-emphasized relative
   to the icon/label it's attached to since it changes every frame and
   doesn't need to shout. tabular-nums keeps it from jittering in width as
   the digits tick down. */
.pillTime {
  font-size: 0.8em;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}
#leaveBtn {
  position: fixed;
  left: 14px;
  bottom: 20px;
}
/* Owner request 2026-09-04: a highlighted "Join" button for a spectator,
   shown only while spectating a genuinely joinable room (see
   updateSpectateJoinBtn, client.js). Sits bottom-center, above the score/
   clock bar — the natural place a spectator's eye already is, and (on
   mobile) already clear of .matchBar, which moves to top-left there. A
   gentle glow pulse draws the eye without being obnoxious, same spirit
   as the hat-trick badge/live-dot pulses elsewhere in this file. */
#spectateJoinBtn {
  position: fixed;
  left: 50%;
  bottom: 74px;
  transform: translateX(-50%);
  z-index: 20;
  animation: spectateJoinPulse 1.8s ease-in-out infinite;
}
@keyframes spectateJoinPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.55); }
  50% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
}
@media (prefers-reduced-motion: reduce) {
  #spectateJoinBtn { animation: none; }
}

/* Mobile landscape HUD (owner report 2026-09-01, real on-device play:
   "the space is so valuable and these elements often overlap the puck/
   players/etc") — same @media (pointer: coarse) gate .touchControls
   already uses (touch-primary devices only; a touchscreen laptop that
   also has a keyboard/mouse never matches this). Three changes:
   1. Score/time moves off the bottom (where it competed with the
      playfield and sat right above the touch joystick) to a small,
      out-of-the-way top-left instead.
   2. The message ticker moves to the top-RIGHT so it doesn't collide with
      the score bar now sitting top-left, and shrinks to match
      HUD_TICKER_MAX_LINES's own mobile value (client.js) — 3 lines, not 6.
   3. The flag/disc/puck status pill ("BLUE has the puck" etc.) is hidden
      outright — it sat dead center over the ice, exactly where the puck
      and players actually are, and was the single most-overlapped element
      reported. */
@media (pointer: coarse) {
  .matchBar {
    left: 8px;
    right: auto;
    bottom: auto;
    top: 8px;
    transform: none;
  }
  /* Owner, 2026-09-08, live CrazyGames screenshot: "We probably need to
     vertically shrink the scoreboard somehow due to the playfield
     overlap, even though it does change opacity [.matchBar.ghosted
     already does that]." Trimmed the vertical padding on both pills and
     the TIME stack (its own two-line label-over-value layout is the
     tallest part of this bar) — same real estate this whole @media
     block already fights for, just a shorter score bar this round. */
  .matchPill { padding: 2px 10px; gap: 5px; }
  .matchPill.blue { padding-right: 12px; }
  .matchPill.red { padding-left: 12px; }
  .matchTeamLabel { font-size: 0.62rem; }
  .matchScoreBox { font-size: 0.85rem; padding: 0px 6px; min-width: 1.2em; }
  .matchDots .dot { width: 6px; height: 6px; }
  .matchTime { padding: 1px 10px; }
  .matchTimeLabel { font-size: 0.55rem; line-height: 1.1; }
  .matchTimeValue { font-size: 0.95rem; line-height: 1.1; }
  .hudTicker {
    left: auto;
    right: 8px;
    top: 8px;
    transform: none;
    width: min(56%, 380px);
    max-height: 60px; /* ~3 lines at the smaller font below, matching HUD_TICKER_MAX_LINES */
    font-size: 0.72rem;
    padding: 4px 10px;
  }
  .flagStatusPill { display: none; }

  /* Deltas on top of the universal #youPill/#leaveBtn rules above (that
     block's own comment covers the "why" for both): Leave Room needs
     extra bottom clearance to sit above the joystick (.touchJoystick:
     left 22px, bottom 22px, 124px tall) rather than the plain bottom-left
     corner desktop uses, and shrinks to fit the smaller screen.
     #powerupPill went back to #hudStack's real top-left flex column on
     desktop (2026-09-02, see that rule's own comment) — mobile
     deliberately keeps its OWN separate, real-device-tuned spot instead
     (owner report 2026-09-01, top-left was already spoken for there by
     .matchBar/score, moved there in the very same pass): fixed +
     centered, clear of both the score (top-left) and the ticker
     (top-right) it sits between — position:fixed pulls it out of
     #hudStack's flex flow same as it always pulled it out of .topbar's,
     so moving it into #hudStack didn't need a change here. The ability/
     shot pill's actual TEXT also gets shorter on touch devices
     specifically, see IS_COARSE_POINTER in client.js's updatePowerupPill.
     #hudStack itself gets pushed down to clear .matchBar's mobile spot
     (top-left too, moved there in that same 2026-09-01 pass) since
     .controlsLegend is hidden below and #powerupPill is pulled out here,
     leaving only #perfStats actually flowing inside it on mobile. */
  #hudStack { top: 46px; }
  #powerupPill {
    position: fixed;
    top: 74px;
    left: 50%;
    transform: translateX(-50%);
    max-width: min(90%, 340px);
    font-size: 0.72rem;
    padding: 4px 10px;
  }
  #leaveBtn {
    bottom: 154px;
    padding: 5px 10px;
    font-size: 0.72rem;
  }
}

.vgsMenu {
  position: absolute;
  left: 14px;
  bottom: 14px;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(7, 16, 30, 0.92);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
  font-size: 0.88rem;
  min-width: 180px;
}
.vgsMenu .vgsTitle {
  font-weight: 700;
  margin-bottom: 6px;
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.vgsMenu .vgsRow { display: flex; gap: 8px; padding: 2px 0; align-items: baseline; }
.vgsMenu .vgsKey {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.14);
  font-weight: 700;
  text-align: center;
}
.chatInputBar {
  position: absolute;
  left: 14px;
  bottom: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(7, 16, 30, 0.92);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
  width: min(88vw, 360px);
}
/* On the actual game screen only, swap to #hudStack's top-left corner —
   see index.html's comment on this element for why (Leave Room overlap).
   client.js's openChatCompose/closeChatCompose push #hudStack itself down
   out of the way for as long as this is open (measured, not a hardcoded
   offset — this bar's own height never changes, but staying consistent
   with how every other "clear a sibling's real rendered size" spot in
   this file already works, e.g. #arcadeMetersBar vs. #matchBar). */
.frame.gameMode .chatInputBar {
  top: 16px;
  left: 14px;
  bottom: auto;
}
.chatInputScope {
  flex-shrink: 0;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.85);
}
.chatInputScope.blue { background: rgba(59, 130, 246, 0.28); color: #bfdbfe; }
.chatInputScope.red { background: rgba(239, 68, 68, 0.28); color: #fecaca; }
#chatInputBox {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  outline: none;
  color: #fff;
  font: inherit;
  font-size: 0.88rem;
}
#chatInputBox::placeholder { color: rgba(255, 255, 255, 0.4); }
.footer {
  padding: 12px 18px 16px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  color: rgba(255, 255, 255, 0.74);
  font-size: 0.92rem;
  flex-wrap: wrap;
}

/* --- lobby / mode-select landing screen --- */
.lobby { padding: 28px 18px; }

/* --- hero band: eyebrow + logo + copy on the left, a real gameplay
   screenshot (see public/media/hero-bg.jpg — captured live via headless
   Chrome, not stock art) framed on the right. Modeled on the two-panel
   "headline + preview" hero pattern from tagpro-nexus-hub's homepage
   concept, using Cappit's own logo/brand colors instead of borrowing
   theirs. --- */
.heroBand {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 0;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.03);
  overflow: hidden;
  margin-bottom: 22px;
}
.heroBandLeft { padding: 26px 26px 22px; display: flex; flex-direction: column; gap: 14px; }
.heroEyebrow {
  align-self: flex-start;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid rgba(74, 158, 255, 0.35);
  background: rgba(74, 158, 255, 0.12);
  color: #8cbdff;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.heroLogo { width: min(100%, 300px); height: auto; display: block; filter: drop-shadow(0 8px 30px rgba(255, 90, 60, 0.15)) drop-shadow(0 8px 30px rgba(74, 158, 255, 0.15)); }
.heroCopy { font-size: 0.92rem; line-height: 1.55; color: rgba(255, 255, 255, 0.68); max-width: 42ch; }
.onlineIndicator { display: flex; align-items: center; gap: 8px; font-size: 0.82rem; color: rgba(255, 255, 255, 0.6); margin-top: auto; }
.heroBandRight { background: rgba(0, 0, 0, 0.25); border-left: 1px solid var(--line); min-height: 220px; display: flex; }
.heroPreview { position: relative; width: 100%; aspect-ratio: 16 / 9; align-self: center; overflow: hidden; }
.heroPreview video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: #000; /* shows through before the first frame decodes */
}
.heroPreviewTag {
  position: absolute;
  left: 12px;
  bottom: 12px;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(7, 16, 30, 0.75);
  border: 1px solid rgba(255, 255, 255, 0.18);
  backdrop-filter: blur(4px);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.03em;
}
/* prefers-reduced-motion: CSS alone can't stop an autoplay <video>, so the
   actual pause lives in client.js (checks matchMedia at load, right next
   to where the video element is looked up) — nothing to do here. */

/* --- live-dot: a small pulsing indicator, reused by both the online-count
   pill and the "Live games" section header. --- */
.liveDot {
  position: relative;
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #3ddc73;
  flex: none;
}
.liveDot::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: #3ddc73;
  opacity: 0.55;
  animation: liveDotPing 1.8s ease-out infinite;
}
@keyframes liveDotPing {
  0% { transform: scale(0.6); opacity: 0.55; }
  100% { transform: scale(2.1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .liveDot::before { animation: none; opacity: 0.25; }
}

.modeGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }

/* Per-mode rows (Ultimate / Soccer / CTF / More ways to play) — each a
   labeled section with the same four-card grid inside. */
.modeRows { display: flex; flex-direction: column; gap: 26px; }
.modeRowHeader { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 8px; }
.modeRowHeader h3 { margin: 0; font-size: 1.05rem; font-family: "Oxanium", Inter, system-ui, sans-serif; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
.modeRowIcon { font-size: 1.1rem; }
.modeRowHint { color: var(--text-dim); font-size: 0.82rem; }
/* A four-up row collapses gracefully on narrow screens via the existing
   modeGrid auto-fit; nothing extra needed. */
.modeCard.disabled {
  cursor: default;
  opacity: 0.55;
  border-top-color: rgba(255, 255, 255, 0.18);
}
.modeCard.disabled:hover { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.15); transform: none; }
.modeCard {
  --accent: var(--blue);
  text-align: left;
  padding: 18px 18px 16px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-top: 2px solid var(--accent);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 96px;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.modeCard:hover { background: rgba(255, 255, 255, 0.09); border-color: rgba(255, 255, 255, 0.25); border-top-color: var(--accent); transform: translateY(-2px); }
.modeCard.accent-teal { --accent: var(--teal); }
.modeCard.accent-blue { --accent: var(--blue); }
.modeCard.accent-violet { --accent: var(--violet); }
.modeCard.accent-red { --accent: var(--red); }
.modeCard.accent-gold { --accent: var(--gold); }
.modeCard.accent-slate { --accent: var(--slate); }
.modeCard.accent-green { --accent: var(--green); }
.modeCard.accent-orange { --accent: var(--orange); }
.modeCard.accent-rose { --accent: var(--rose); }
.modeCardTop { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.modeCardIcon {
  position: relative; /* anchors a badge like #challengeBadge on the 1v1 Ladder card */
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.08); /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  font-size: 1.05rem;
}
.modeCardBadge {
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  color: var(--accent);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.modeCardTitle { font-weight: 700; font-size: 1.12rem; }
.modeCardSubtitle { font-size: 0.83rem; color: rgba(255, 255, 255, 0.6); line-height: 1.35; flex: 1; }
.modeCardFooter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 8px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.modeCardMeta { font-size: 0.72rem; color: rgba(255, 255, 255, 0.4); }
.modeCardCta { font-size: 0.8rem; font-weight: 700; color: var(--accent); }

/* --- "Live games" panel: every public room across every mode, right on
   the landing page (see renderRoomList(landingRoomListEl, ...) in
   client.js) — reuses .roomList/.roomRow wholesale, same as the
   matchmaking screen's own list. --- */
.liveGamesSection { margin-top: 30px; padding-top: 22px; border-top: 1px solid var(--line); }
.liveGamesHeader { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.liveGamesHeader h3 { margin: 0; font-size: 1.02rem; display: flex; align-items: center; gap: 8px; }
.lobbyHint { color: rgba(255, 255, 255, 0.6); font-size: 0.88rem; }
.primary, .secondary {
  padding: 10px 16px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--text);
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
}
/* Class-scoped (not button-scoped) so an <a> styled as a button - the
   Discord invite link in the topbar - looks identical to a real one. */
a.primary, a.secondary { text-decoration: none; display: inline-block; box-sizing: border-box; }
.primary { background: linear-gradient(135deg, var(--blue), #2f6fd6); border-color: transparent; }
.primary:hover { filter: brightness(1.1); }
.primary.green { background: linear-gradient(135deg, var(--green), #16a34a); }
/* Emphasized CTA modifier (owner request 2026-09-05: onboarding's Play
   Now "may get lost" next to the brand-colored Discord button) — a
   general utility, not scoped to one card, so the scoreboard's own Play
   Again reuses it too. */
.primary.big { padding: 16px 24px; font-size: 1.15rem; }
/* Hockey's own Start Match/Practice button (owner request 2026-08-31): more
   room above it and pushed to the right edge of the modal, rather than
   sitting flush against the checkbox row above it at the left margin. */
.hockeyStartRow { display: flex; justify-content: flex-end; margin-top: 18px; }
.secondary { background: rgba(255, 255, 255, 0.07); width: 100%; margin-top: 4px; }
.secondary:hover { background: rgba(255, 255, 255, 0.13); }
.lobbyColumns { display: grid; grid-template-columns: 1.3fr 1fr; gap: 20px; align-items: start; }
.lobbyList h3, .lobbyForms h3 {
  margin: 0 0 10px;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.55);
}
.roomList { display: flex; flex-direction: column; gap: 8px; }
.roomListEmpty { color: rgba(255, 255, 255, 0.45); font-size: 0.9rem; padding: 10px 0; }
.roomRow {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.045);
  cursor: pointer;
}
.controlsSection { margin-bottom: 16px; }
.toggleWrap { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-weight: 600; }
.toggleWrap input { width: 18px; height: 18px; accent-color: #4a9eff; cursor: pointer; }
#setThrowRingOpacity { width: 160px; accent-color: #4a9eff; }
.controlsSection h4 { margin: 0 0 8px; }
.controlRow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 0;
}
.controlLabel { flex: 1; color: rgba(255, 255, 255, 0.85); }
.keyChip, .deviceChip {
  min-width: 76px;
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.08);
  color: inherit;
  font: inherit;
  font-weight: 700;
  text-align: center;
  cursor: pointer;
}
.keyChip:hover, .deviceChip:hover { background: rgba(255, 255, 255, 0.15); }
.keyChip.capturing, .deviceChip.capturing {
  border-color: rgba(59, 130, 246, 0.8);
  background: rgba(59, 130, 246, 0.2);
  animation: keychip-pulse 1s ease-in-out infinite;
}
@keyframes keychip-pulse { 50% { background: rgba(59, 130, 246, 0.38); } }
.macroPhraseSelect { flex: 1; min-width: 0; }
.macroRemoveBtn { flex-shrink: 0; }
/* Gamepad/mouse multi-bind chips (2026-08-27) — several bindings per
   action, each removable, plus a dashed "+ Add" chip. */
.chipGroup { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; justify-content: flex-end; }
.chipWrap { display: inline-flex; align-items: center; gap: 2px; }
.deviceChipAdd { border-style: dashed; opacity: 0.85; min-width: 64px; }
.chipRemoveBtn {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
}
.chipRemoveBtn:hover { background: rgba(248, 113, 113, 0.25); color: #fca5a5; border-color: rgba(248, 113, 113, 0.5); }
.controlsHint { color: rgba(255, 255, 255, 0.55); font-size: 0.85rem; margin: 6px 0 0; }
.controlsNotice { color: #fca5a5; font-size: 0.9rem; margin: 8px 0; }
.controlsFooter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.shopHatPreview {
  width: 64px;
  height: 64px;
  display: block;
  margin: 0 auto;
}
.shopDegreeCupIcon {
  height: 48px;
  width: auto; /* the source image is taller than wide (82x128) — let it keep its real proportions rather than squashing into a square slot */
  display: block;
  margin: 0 auto 6px;
}
.updateToast {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid rgba(59, 130, 246, 0.5);
  background: rgba(15, 30, 52, 0.95);
  color: rgba(255, 255, 255, 0.9);
  font-size: 0.9rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.updateToast button {
  padding: 6px 14px;
  border-radius: 8px;
  border: 1px solid rgba(59, 130, 246, 0.6);
  background: rgba(59, 130, 246, 0.25);
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.updateToast button:hover { background: rgba(59, 130, 246, 0.4); }
/* Owner report 2026-09-05: "I almost missed that it was in the lower
   left... having it temporarily take over the HUD area or overlaying it
   there might be best. Nothing in the HUD is super critical that can't
   be covered up briefly." Moved from a bottom-left corner (easy to miss
   entirely, especially one-handed on a laptop trackpad) to dead center
   over the HUD's own top strip (.hudTicker/.academyBar both anchor at
   this same top-center spot) — .frame.gameMode fills the whole viewport
   (position:fixed; inset:0), so this fixed top:14px/left:50% lands in
   exactly the same visual spot as those, and its z-index (60, unchanged)
   already sits above the entire .frame subtree, so it visibly overlays
   the ticker/academy bar rather than competing with them for space. */
.botHintToast {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 12px;
  width: max-content;
  max-width: min(94%, 480px);
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid rgba(250, 204, 21, 0.45);
  background: rgba(35, 28, 8, 0.95);
  color: rgba(255, 255, 255, 0.92);
  font-size: 0.9rem;
  line-height: 1.4;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
  opacity: 1;
  transition: opacity 0.3s ease;
}
/* Auto-dismiss fade (client.js's dismissHint adds this, then hides once
   the transition finishes) — same owner report: "we should expire/fade
   out the tooltip in a few seconds. I was stuck seeing the F8 bot
   tooltip... didn't see the next tooltip until I force closed the
   previous one." Every hint now dismisses itself either way. */
.botHintToast.hintFadeOut { opacity: 0; }
.botHintToast kbd {
  padding: 1px 6px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  background: rgba(255, 255, 255, 0.12);
  font: inherit;
  font-weight: 700;
}
.botHintToastActions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* Class, not ID — shared by every hint that offers an inline undo/opt-in
   button (bot-hint's "Turn it off", mouse-aim's "Turn it on"), not just
   the one that happened to ship first. */
.botHintToast .botHintToggleBtn {
  padding: 6px 14px;
  border-radius: 8px;
  border: 1px solid rgba(250, 204, 21, 0.6);
  background: rgba(250, 204, 21, 0.22);
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.botHintToast .botHintToggleBtn:hover { background: rgba(250, 204, 21, 0.38); }
.botHintDismissBtn {
  padding: 4px 8px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.botHintDismissBtn:hover { background: rgba(255, 255, 255, 0.12); }
/* #botHintToast and #quickTipToast both sit at .botHintToast's plain
   top-center spot with no offset between them (unlike an earlier version
   of this rule) — client.js's hint queue guarantees only one of the two
   is ever unhidden at once, so there's nothing to stack against anymore. */
@media (max-width: 640px) {
  .botHintToast { width: calc(100% - 16px); max-width: none; }
}
/* Owner follow-up, same 2026-09-05 report: "totally hide the HUD while
   a tooltip is active. It's distracting to see text moving behind it,
   when we want the focus on the tooltip." client.js's hint queue toggles
   this class on .frame for the toast's entire visible lifetime
   (including its fade-out) — .topbar/.hudTicker/.academyBar all update
   their text live during play (ticker lines, the countdown clock), which
   is exactly the "moving behind it" .hudStack's powerup-pill countdown
   included. display:none, not just opacity/visibility, since a genuinely
   inert HUD is the point (no ticking text anywhere on screen); safe here
   because every one of these is already position:absolute inside .frame
   (removed from flow), so hiding them can't reflow the canvas or
   .touchControls, which deliberately stays untouched — the joystick/
   buttons are live input, not "the HUD" the report was about. */
.frame.gameMode.hintHudHidden .topbar,
.frame.gameMode.hintHudHidden .hudTicker,
.frame.gameMode.hintHudHidden .hudStack,
.frame.gameMode.hintHudHidden .academyBar {
  display: none;
}
.roomRow:hover { background: rgba(255, 255, 255, 0.09); }
.roomRow .roomName { font-weight: 600; }
.roomRow .roomMeta { color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; }
.roomScore {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 3px 0;
  font-variant-numeric: tabular-nums;
  font-size: 1.05rem;
}
.roomScore .sBlue { color: var(--blue); }
.roomScore .sRed { color: var(--red); }
.roomScore .scoreSep { color: rgba(255, 255, 255, 0.4); font-weight: 400; }
.roomScore .roomClock { margin-left: 4px; color: rgba(255, 255, 255, 0.55); font-size: 0.85rem; font-weight: 600; }
.roomScore .roomClock.urgent { color: var(--red); }
/* A match in progress: the row itself isn't a click target (its buttons
   are), and it carries a LIVE badge + Spectate/Join actions instead of a
   bare code. */
.roomRow.live { cursor: default; }
.roomRow.live:hover { background: rgba(255, 255, 255, 0.045); }
.liveBadge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 6px;
  background: rgba(239, 68, 68, 0.18);
  border: 1px solid rgba(239, 68, 68, 0.45);
  color: #fca5a5;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  vertical-align: 1px;
}
.roomRowActions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.roomBtn {
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.07);
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.roomBtn:hover { background: rgba(255, 255, 255, 0.14); }
.roomBtn.primaryish {
  background: rgba(59, 130, 246, 0.22);
  border-color: rgba(59, 130, 246, 0.55);
}
.roomBtn.primaryish:hover { background: rgba(59, 130, 246, 0.34); }
.lobbyForms { display: flex; flex-direction: column; gap: 18px; }
.lobbyForms form {
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  padding: 12px 14px;
  background: rgba(255, 255, 255, 0.03);
}
.lobbyForms input[type="text"], .lobbyForms input:not([type]), .lobbyForms input[type="checkbox"] + label, select.teamSizeSelect,
.lobbyForms input, .lobbyForms select {
  font-family: inherit;
  font-size: 0.92rem;
}
.lobbyForms input, .lobbyForms select {
  width: 100%;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  margin-top: 4px;
}
.lobbyForms .fieldRow { display: block; font-size: 0.85rem; color: rgba(255, 255, 255, 0.7); margin-top: 8px; }
.lobbyForms .checkRow { display: flex; align-items: center; gap: 6px; }
.lobbyForms .checkRow input { width: auto; margin: 0; }
.codeInput { text-transform: uppercase; letter-spacing: 0.12em; text-align: center; }
.lobbyError {
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 90, 90, 0.4);
  background: rgba(255, 90, 90, 0.12);
  color: #ffb3b3;
  font-size: 0.88rem;
}

/* --- matchmaking screen (Casual/Ranked "next screen", reuses .lobbyColumns/
   .lobbyList/.lobbyForms/.roomList above wholesale — just relocated out of
   the old single-screen lobby into its own dedicated screen) --- */
.matchmakingScreen { padding: 18px; }
.matchmakingQuickBtn { width: 100%; margin-bottom: 14px; }
.matchmakingGuestGate {
  text-align: center;
  padding: 32px 20px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}
.matchmakingGuestGate .lobbyHint { display: block; margin-bottom: 14px; }
/* Two-provider sign-in row (Discord + Google), reused anywhere a guest
   gate offers both — matchmaking's ranked gate, the ladder join row,
   Academy's nudges. Buttons keep their own .primary/.secondary sizing;
   this just lays them out side by side and lets them wrap on narrow
   screens instead of overflowing. */
.authButtonRow { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.authButtonRow .primary, .authButtonRow .secondary { width: auto; margin-top: 0; }
/* --- waiting room + draft screen --- */
.waitingRoom, .draftScreen { padding: 18px; }
.waitingHeader { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.waitingTitle { font-size: 1.05rem; font-weight: 700; }
.waitingMeta { color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; margin-top: 3px; }
.waitingMeta strong { color: var(--text); letter-spacing: 0.1em; }
.waitingActions { display: flex; align-items: center; gap: 12px; margin-top: 18px; flex-wrap: wrap; }
.waitingActions .secondary { width: auto; margin-top: 0; }
button.primary:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
.draftTimer {
  font-variant-numeric: tabular-nums;
  font-size: 1.6rem;
  font-weight: 800;
  min-width: 2.2ch;
  text-align: center;
  color: var(--text);
}
.draftTimer.urgent { color: var(--red); }

.rosterColumns { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.rosterTeam h3 { margin: 0 0 10px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
.rosterTeam.blue h3 { color: var(--blue); }
.rosterTeam.red h3 { color: var(--red); }
.rosterList { display: flex; flex-direction: column; gap: 8px; }
.rosterRow {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.045);
  font-size: 0.9rem;
}
.rosterRow.me { border-color: rgba(255, 255, 255, 0.35); background: rgba(255, 255, 255, 0.09); }
.rosterRow .rosterClass { display: inline-flex; align-items: center; gap: 8px; color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; }
.rosterRow .rosterClass.picked { color: var(--text); font-weight: 600; }
.rosterEmpty { color: rgba(255, 255, 255, 0.35); font-style: italic; }
.rosterSwitchTeam, .rosterMakeOwner {
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  font-size: 0.75rem;
  cursor: pointer;
}
.rosterSwitchTeam:hover, .rosterMakeOwner:hover { background: rgba(255, 255, 255, 0.14); }
/* Plain icon button, not a labeled pill (owner request 2026-09-09: "it
   can just be a red X, not the word remove in a bubble") — sits right
   next to the class label, so it reads as "remove this row" rather than
   a standalone action floating mid-row. */
.rosterKick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 90, 90, 0.4);
  background: rgba(255, 90, 90, 0.1);
  color: rgba(255, 130, 130, 0.95);
  font-size: 0.72rem;
  line-height: 1;
  cursor: pointer;
}
.rosterKick:hover { background: rgba(255, 90, 90, 0.25); border-color: rgba(255, 90, 90, 0.7); }

.classGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-top: 20px; }
.classCard {
  text-align: left;
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
}
.classCard:hover { background: rgba(255, 255, 255, 0.09); }
.classCard.selected { border-color: var(--blue); background: rgba(74, 158, 255, 0.14); }
.classCard:disabled { opacity: 0.45; cursor: not-allowed; }
.classCard .classNameRow { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.classCard .classIcon { width: 20px; height: 20px; flex-shrink: 0; color: rgba(255, 255, 255, 0.85); }
/* One color per archetype (owner request 2026-08-31), pulled from the
   existing palette above rather than new hex — same tokens the mode cards
   already use for their own accents. */
.classIcon.i-bolt { color: var(--gold); }
.classIcon.i-shield { color: var(--blue); }
.classIcon.i-target { color: var(--red); }
.classIcon.i-magnet { color: var(--violet); }
.classIcon.i-dice { color: var(--rose); }
.classIcon.i-net { color: var(--teal); }
.classCard .className { font-weight: 700; }
/* Fixed min-height (owner request 2026-08-31: the ability line was
   starting at a different row per card depending on how many lines the
   passive line above it happened to wrap to) — sized for the longest real
   passive line (Goalie's, 3 lines at the grid's narrowest column) so
   .classAbility lines up on the same axis across every card in a row. */
.classCard .classPassive { font-size: 0.82rem; color: rgba(255, 255, 255, 0.65); line-height: 1.35; min-height: calc(1.35em * 3); }
.classCard .classAbility { font-size: 0.82rem; color: rgba(255, 255, 255, 0.65); line-height: 1.35; margin-top: 4px; }
/* A little visual weight so the ability key reads as "this is the one
   that matters" rather than blending into the generic grey kbd style used
   for chat hints elsewhere. */
.classCard .classAbility kbd { background: rgba(74, 158, 255, 0.18); border-color: rgba(74, 158, 255, 0.55); color: #bfe0ff; }

.mapVoteScreen { padding: 18px; }
.mapVoteOptions { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; }
.mapVoteCard {
  text-align: center;
  padding: 24px 16px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
}
.mapVoteCard:hover { background: rgba(255, 255, 255, 0.09); }
.mapVoteCard.selected { border-color: var(--blue); background: rgba(74, 158, 255, 0.14); }
.mapVoteCard .mapVoteName { font-size: 1.1rem; font-weight: 700; margin-bottom: 8px; }
.mapVoteCard .mapVoteCount { font-size: 0.85rem; color: rgba(255, 255, 255, 0.6); }
.mapVoteCard .mapVoteCount strong { color: var(--text); font-size: 1.3rem; display: block; }

/* --- coins, daily tasks, Cappit Shop --- */
.coinBalance {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 9px;
  border-radius: 999px;
  background: rgba(255, 210, 74, 0.14);
  border: 1px solid rgba(255, 210, 74, 0.35);
  color: #ffd24a;
  font-size: 0.85rem;
  font-weight: 700;
  /* #coinBalance (top nav) is a real <button> now — click opens the shop
     (owner request 2026-08-28). #shopCoinBalance (inside the shop modal
     itself) shares this class purely for the pill look and stays a plain
     span, unaffected by this reset. */
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
button.coinBalance:hover { background: rgba(255, 210, 74, 0.24); border-color: rgba(255, 210, 74, 0.5); }

.modalOverlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(4, 8, 16, 0.72);
  padding: 20px;
}
.modalCard {
  width: min(680px, 100%);
  max-height: 85vh;
  overflow-y: auto;
  border-radius: 16px;
  border: 1px solid var(--line);
  background: var(--panel);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
  padding: 18px 20px 22px;
}
.modalHeader { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.modalHeader h3 { margin: 0; flex: 1; font-size: 1.1rem; }
.modalClose {
  border: none;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  font-size: 1.1rem;
  cursor: pointer;
  line-height: 1;
}
.modalClose:hover { background: rgba(255, 255, 255, 0.16); }
.modalCardWide { width: min(760px, 100%); }

/* --- live in-match roster (2026-08-30, Reddit-launch readiness) — Tab
   opens rosterModal, or clicking a player's ball on the canvas. Reuses
   the exact .ladderTable.scoreboardTable look the end-of-match scoreboard
   already has (client.js's scoreboardColumns is the shared per-mode
   column source for both), with an extra Actions column for report/
   vote-kick. --- */
#rosterModalBody { overflow-x: auto; }
.rosterStatsTable td, .rosterStatsTable th { white-space: nowrap; }
.rosterStatsTable .rosterActionsCell { text-align: right; }
.rosterStatsTable tr.rosterHighlight td { outline: 2px solid var(--accent); outline-offset: -2px; }
.rosterActionBtn {
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  border-radius: 8px;
  padding: 5px 9px;
  font-size: 0.85rem;
  cursor: pointer;
  margin-left: 6px;
}
.rosterActionBtn:hover { background: rgba(255, 255, 255, 0.12); }
.rosterActionBtn:disabled { opacity: 0.35; cursor: default; }
.rosterReasonPicker { display: flex; gap: 6px; flex-wrap: wrap; padding: 8px 10px; }
.rosterReasonPicker button {
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  border-radius: 999px;
  padding: 4px 11px;
  font-size: 0.78rem;
  cursor: pointer;
}
.rosterReasonPicker button:hover { background: rgba(74, 158, 255, 0.18); border-color: var(--accent); }

/* --- vote-kick bar (2026-08-30) — persistent "cast your vote" affordance
   while one's open in this room; sits just under the hudTicker. --- */
.voteKickBar {
  position: absolute;
  top: 132px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  border-radius: 999px;
  background: rgba(120, 20, 20, 0.55);
  border: 1px solid rgba(255, 120, 120, 0.4);
  font-size: 0.82rem;
  z-index: 5;
}
.voteKickBar #voteKickBarBtn {
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 0.78rem;
  font-weight: 700;
  cursor: pointer;
}
.voteKickBar #voteKickBarBtn:hover { background: rgba(255, 255, 255, 0.28); }
.voteKickBar #voteKickBarBtn:disabled { opacity: 0.5; cursor: default; }

/* Leaderboard modal tabs — same pill idiom as .academyTab, two rows
   (window toggle, then category), see client.js's openLeaderboardModal. */
.v2lbTabs, .v2lbCategoryTabs { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.v2lbTab {
  padding: 7px 13px; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04); color: var(--text);
  font-family: inherit; font-size: 0.85rem; cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.v2lbTab:hover { background: rgba(255, 255, 255, 0.09); }
.v2lbTab.active { border-color: var(--accent); background: rgba(74, 158, 255, 0.14); }
.v2lbMyRank { margin-top: 10px; text-align: center; }

.taskRow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
  margin-bottom: 8px;
}
.taskRow.completed { border-color: rgba(124, 255, 158, 0.4); background: rgba(124, 255, 158, 0.08); }
.taskRow .taskInfo { flex: 1; }
.taskRow .taskLabel { font-weight: 600; }
.taskRow .taskMeta { font-size: 0.8rem; color: rgba(255, 255, 255, 0.55); margin-top: 2px; }
.taskRow .taskDifficulty {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
}
.taskRow .taskReward { color: #ffd24a; font-weight: 700; white-space: nowrap; }
.streakInfo {
  margin-top: 14px;
  padding: 10px 12px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  font-size: 0.88rem;
  color: rgba(255, 255, 255, 0.75);
}

.shopSection h4 { margin: 16px 0 10px; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255, 255, 255, 0.55); }
.shopSection:first-child h4 { margin-top: 0; }
.shopGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
.shopItem {
  text-align: center;
  padding: 14px 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.03);
  cursor: pointer;
  font-family: inherit;
  color: var(--text);
}
.shopItem:hover { background: rgba(255, 255, 255, 0.08); }
.shopItem.owned { border-color: rgba(124, 255, 158, 0.4); }
.shopItem.equipped { border-color: var(--blue); background: rgba(74, 158, 255, 0.14); }
.shopItem .shopItemIcon { font-size: 1.6rem; margin-bottom: 6px; }
.shopItem .shopItemSwatch { width: 28px; height: 28px; border-radius: 50%; margin: 0 auto 6px; border: 2px solid rgba(255, 255, 255, 0.3); }
.shopItem .shopItemName { font-size: 0.85rem; font-weight: 600; }
.shopItem .shopItemPrice { font-size: 0.78rem; color: #ffd24a; margin-top: 3px; }
.shopItem .shopItemStatus { font-size: 0.72rem; color: rgba(124, 255, 158, 0.85); margin-top: 3px; }
/* Earned-only items (graduation caps) before they're earned — a "how to
   get this" line instead of a price, and no buy affordance. */
.shopItem .shopItemEarned { color: rgba(255, 255, 255, 0.55); }
.shopItem.lockedEarned { cursor: default; opacity: 0.75; }
.shopItem.lockedEarned:hover { transform: none; }
.shopSectionHint { text-transform: none; letter-spacing: normal; font-weight: 400; }
/* Degree flairs (win-milestone ladder) — a compact summary card instead of
   the usual buy-grid, since these are earned, never purchased. */
.shopDegreeSummary {
  padding: 10px 12px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  margin-bottom: 10px;
}
.shopDegreeCurrent { font-size: 1rem; font-weight: 700; }
.shopDegreeNext { font-size: 0.82rem; color: rgba(255, 255, 255, 0.6); margin-top: 4px; }
/* Settings → Voice: ranked, draggable list of every voice pack */
.voiceOrder { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.voiceRow {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.03);
  position: relative;
}
.voiceRow[draggable="true"] { cursor: grab; }
.voiceRow.dragging { opacity: 0.45; }
.voiceRow.dropAbove::before, .voiceRow.dropBelow::after {
  content: ""; position: absolute; left: 6px; right: 6px; height: 2px; background: var(--blue); border-radius: 2px;
}
.voiceRow.dropAbove::before { top: -3px; }
.voiceRow.dropBelow::after { bottom: -3px; }
.voiceGrip { color: rgba(255, 255, 255, 0.35); font-size: 0.9rem; letter-spacing: -2px; user-select: none; }
.voiceRank { width: 20px; text-align: right; color: rgba(255, 255, 255, 0.5); font-variant-numeric: tabular-nums; font-weight: 700; font-size: 0.85rem; }
.voiceName { flex: 1; font-weight: 600; }
.voiceTag { display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 4px; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; background: rgba(56, 189, 248, 0.18); color: #7dd3fc; vertical-align: 1px; }
.voicePlayBtn, .voiceMoveBtn {
  width: 26px; height: 26px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06); color: var(--text); cursor: pointer; font-size: 0.7rem; line-height: 1;
}
.voicePlayBtn:hover, .voiceMoveBtn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.14); }
.voiceMoveBtn:disabled { opacity: 0.3; cursor: default; }
.voiceArrows { display: inline-flex; gap: 4px; }
.segmented { display: inline-flex; border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 8px; overflow: hidden; }
.segBtn { border: 0; background: transparent; color: rgba(255, 255, 255, 0.7); padding: 6px 12px; font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; }
.segBtn + .segBtn { border-left: 1px solid rgba(255, 255, 255, 0.16); }
.segBtn.active { background: rgba(74, 158, 255, 0.2); color: #fff; }
.blockedNote { background: #7a1620; color: #fff; padding: 10px 16px; font-size: 0.9rem; text-align: center; }
.tickerScope, .vgsScope {
  display: inline-block; margin-right: 6px; padding: 0 5px; border-radius: 3px;
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; vertical-align: 1px;
  background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.7);
}
.vgsScope { margin-left: 6px; margin-right: 0; }
.voicePackGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
.voicePackItem {
  display: flex;
  align-items: center;
  gap: 6px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.03);
  padding: 4px;
}
.voicePackItem.equipped { border-color: var(--blue); background: rgba(74, 158, 255, 0.14); }
.voicePackSelectBtn {
  flex: 1;
  text-align: center;
  padding: 10px 6px;
  border: none;
  background: none;
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.voicePackPreviewBtn {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  cursor: pointer;
  font-size: 0.7rem;
  line-height: 1;
}
.voicePackPreviewBtn:hover { background: rgba(255, 255, 255, 0.14); }

/* --- 1v1 challenge ladder — the badge itself lives on the 1v1 Ladder
   mode card's icon now (.modeCardIcon is position:relative for exactly
   this), not a dedicated topbar button. --- */
.challengeBadge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--red);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}
.ladderSection h4 { margin: 16px 0 10px; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255, 255, 255, 0.55); }
.ladderSection:first-child h4 { margin-top: 0; }
.challengeRow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
  margin-bottom: 8px;
}
.challengeRow .challengeInfo { flex: 1; }
.challengeRow .challengeName { font-weight: 600; }
.challengeRow .challengeMeta { font-size: 0.8rem; color: rgba(255, 255, 255, 0.55); margin-top: 2px; }
.challengeRow .challengeActions { display: flex; gap: 6px; }
.ladderTable { width: 100%; border-collapse: collapse; }
.ladderTable th {
  text-align: left;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.5);
  padding: 6px 10px;
}
.ladderTable td { padding: 8px 10px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.ladderTable tr.me { background: rgba(74, 158, 255, 0.1); }
.ladderRank { font-weight: 700; width: 40px; }
.ladderTable .ladderAvatar { width: 22px; height: 22px; border-radius: 50%; vertical-align: middle; margin-right: 8px; }
.ladderRecord { color: rgba(255, 255, 255, 0.55); font-size: 0.85rem; white-space: nowrap; }
.ladderJoinRow { text-align: center; padding: 20px; }

/* --- Tournaments — same section-header/row/table conventions as the 1v1
   ladder just above (.ladderSection h4/.challengeRow/.ladderTable), kept
   as their own class names since a tournament row/match has a genuinely
   different shape (an entrant pair + a winner, not a challenge or a
   ranked player), not because the visual language should differ. --- */
.tournamentListRow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
  margin-bottom: 8px;
  cursor: pointer;
}
.tournamentListRow:hover { background: rgba(255, 255, 255, 0.08); }
.tournamentListRow .tournamentListInfo { flex: 1; }
.tournamentListRow .tournamentListName { font-weight: 600; }
.tournamentListRow .tournamentListMeta { font-size: 0.8rem; color: rgba(255, 255, 255, 0.55); margin-top: 2px; }
.tournamentStatusPill {
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.tournamentStatusPill.status-registration { background: rgba(45, 212, 191, 0.16); color: var(--teal); }
.tournamentStatusPill.status-in_progress { background: rgba(251, 113, 133, 0.16); color: var(--rose); }
.tournamentStatusPill.status-completed { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.6); }
.tournamentStatusPill.status-cancelled { background: rgba(255, 90, 90, 0.14); color: var(--red); }
.tournamentBackRow { margin-bottom: 12px; }
.tournamentMyMatch {
  margin: 0 0 16px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid rgba(251, 113, 133, 0.35);
  background: rgba(251, 113, 133, 0.08);
}
.tournamentMyMatch p { margin: 0 0 8px; font-size: 0.88rem; }
.tournamentMyMatch p:last-child { margin-bottom: 0; }
.tournamentRoundGroup h4 { margin: 16px 0 10px; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255, 255, 255, 0.55); }
.tournamentRoundGroup:first-child h4 { margin-top: 0; }
/* double_elim's three sections (Winners/Losers/Grand Final) — each one's
   own round groups nest inside, so .tournamentRoundGroup:first-child's
   margin-top:0 rule above already applies correctly per-section. */
.tournamentBracketSection { margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); }
.tournamentBracketSection:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.tournamentBracketSection h3 { margin: 0; font-size: 0.95rem; color: var(--text); }
.tournamentMatchRow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  margin-bottom: 6px;
  font-size: 0.88rem;
}
.tournamentMatchRow .tournamentSide { flex: 1; }
.tournamentMatchRow .tournamentSide.winner { font-weight: 700; color: #7adc96; }
.tournamentMatchRow .tournamentVs { color: rgba(255, 255, 255, 0.4); font-size: 0.75rem; padding: 0 4px; }
.tournamentMatchRow.mine { border-color: rgba(251, 113, 133, 0.4); background: rgba(251, 113, 133, 0.06); }
.tournamentTable { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.tournamentTable th { text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: rgba(255, 255, 255, 0.5); padding: 6px 10px; }
.tournamentTable td { padding: 8px 10px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.tournamentTable tr.me { background: rgba(251, 113, 133, 0.1); }
.tournamentCreateForm { border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 12px; padding: 12px 14px; background: rgba(255, 255, 255, 0.03); margin-bottom: 16px; }
.tournamentCreateForm h4 { margin: 0 0 10px; font-size: 0.85rem; }
.tournamentCreateForm .lobbyError { margin-top: 8px; }
.tournamentFormActions { margin-top: 10px; display: flex; gap: 10px; }
.tournamentDetailTitle { margin: 0 0 4px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tournamentDetailMeta { margin: 0 0 14px; }
.tournamentFormGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; }
.tournamentFormGrid .fieldRow { grid-column: span 1; }
.tournamentFormGrid .fieldRow.span2 { grid-column: span 2; }
@media (max-width: 520px) {
  .tournamentFormGrid { grid-template-columns: 1fr; }
  .tournamentFormGrid .fieldRow.span2 { grid-column: span 1; }
}

#chooseUsernameInput {
  width: 100%;
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
}
#chooseUsernameError { margin-top: 10px; }

/* --- end-of-match scoreboard --- */
.scoreboardFinalScore { font-weight: 700; font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.75); }
.scoreboardTable th:first-child, .scoreboardTable td:first-child { text-align: left; }
.scoreboardTable th, .scoreboardTable td { text-align: center; }
.scoreboardTeam.blue tr:first-child td { border-top: 2px solid var(--blue); }
.scoreboardTeam.red tr:first-child td { border-top: 2px solid var(--red); }

/* --- bots / practice mode --- */
.botChip {
  display: inline-block;
  margin-left: 6px;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.6);
  vertical-align: middle;
}
/* Room owner (see [[cappit-room-ownership]]) — same shape as .botChip
   just above, amber/gold tint instead of neutral grey so it reads as a
   role, not a status label. */
.ownerChip {
  display: inline-block;
  margin-left: 6px;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(255, 200, 80, 0.16);
  color: rgba(255, 210, 120, 0.95);
  vertical-align: middle;
}
.practiceOptionRow { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.practiceOptionRow label { font-size: 0.8rem; color: rgba(255, 255, 255, 0.6); }
.hockeyMinigamesHint { margin: 2px 0 0; font-size: 0.8rem; }
.inlineLink { background: none; border: 0; padding: 0; font: inherit; color: var(--blue, #4a9eff); text-decoration: underline; cursor: pointer; }
.difficultyCards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
/* Survivor's side-picker reuses .difficultyCard's look for a 2-up choice
   (Attacker/Defender) instead of the 3 difficulty tiers — same class,
   just a 2-column grid so there's no empty third slot. */
#survivorTeamCards { grid-template-columns: repeat(2, 1fr); }
.difficultyCard {
  padding: 10px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  text-align: center;
}
.difficultyCard.selected { border-color: var(--blue); background: rgba(74, 158, 255, 0.12); }
.difficultyCard .difficultyLabel { font-weight: 700; }
.difficultyCard .difficultyDetail { font-size: 0.72rem; color: rgba(255, 255, 255, 0.5); margin-top: 2px; }

/* --- replays --- */
.replayBar {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(7, 16, 30, 0.85);
  backdrop-filter: blur(6px);
  max-width: 94%;
}
.replayBarRow { display: flex; align-items: center; gap: 8px; }
.replayBar .audioBtn { flex: none; }
.replayBar .replayExitBtn { flex: none; padding: 6px 12px; font-size: 0.82rem; white-space: nowrap; }
/* Recording disables everything except whatever the JS leaves enabled
   (the status text) — a real MediaRecorder pass is happening in real time
   underneath, and letting the viewer scrub/change speed/exit mid-capture
   would corrupt it. */
.replayBar.recording .replayBarRow > *:not(.replayClipStatus) { opacity: 0.45; pointer-events: none; }

.replayScrubWrap { position: relative; width: 220px; flex: none; }
.replayScrub { position: relative; z-index: 1; width: 100%; display: block; margin: 0; }
.replayScrubOverlay { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.replayClipRangeBand {
  position: absolute;
  top: 50%;
  height: 6px;
  transform: translateY(-50%);
  border-radius: 3px;
  background: rgba(74, 158, 255, 0.3);
  border: 1px solid rgba(74, 158, 255, 0.55);
}
.replayCapTicks { position: absolute; inset: 0; }
.replayCapTick {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 14px;
  transform: translate(-50%, -50%);
  border-radius: 1px;
  cursor: pointer;
  pointer-events: auto;
}
.replayCapTick.blue { background: var(--blue); box-shadow: 0 0 0 1px rgba(7, 16, 30, 0.6); }
.replayCapTick.red { background: var(--red); box-shadow: 0 0 0 1px rgba(7, 16, 30, 0.6); }
.replayCapTick:hover { filter: brightness(1.35); transform: translate(-50%, -50%) scaleY(1.15); }
.replayClipHandle {
  position: absolute;
  top: 50%;
  width: 10px;
  height: 20px;
  transform: translate(-50%, -50%);
  border-radius: 3px;
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.35);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  cursor: ew-resize;
  pointer-events: auto;
}
.replayClipHandle:hover, .replayClipHandle:focus-visible { background: #ffd24a; outline: none; }
.replayClipHandle.dragging { background: #ffd24a; }

.replayTimeLabel { font-size: 0.78rem; font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.75); white-space: nowrap; }
.replaySpeed {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  font-size: 0.78rem;
  padding: 3px 4px;
}
.replayClipRow { justify-content: center; flex-wrap: wrap; row-gap: 6px; }
.replayClipLabel { font-size: 0.78rem; color: rgba(255, 255, 255, 0.65); display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.replayClipLabel input {
  width: 44px;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 5px;
  color: var(--text);
  padding: 2px 4px;
  font-size: 0.78rem;
}
.replayClipDuration { font-size: 0.72rem; color: rgba(255, 255, 255, 0.5); white-space: nowrap; }
.replayClipActionBtn { flex: none; padding: 6px 12px; font-size: 0.8rem; white-space: nowrap; }
.replayClipStatus { font-size: 0.75rem; white-space: nowrap; color: rgba(255, 255, 255, 0.7); }
.replayClipStatus.error { color: var(--red); }
.replayClipStatus.success { color: #7adc96; }

.replayRow {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.045);
  cursor: pointer;
  margin-bottom: 8px;
}
.replayRow:hover { background: rgba(255, 255, 255, 0.09); }
.replayRow .replayScore { font-weight: 700; }
.replayRow .replayScore .blue { color: var(--blue); }
.replayRow .replayScore .red { color: var(--red); }
.replayRow .replayMeta { color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; margin-top: 2px; }
.replayTag {
  display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 4px;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; vertical-align: 2px;
  background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.75);
}
.replayTag + .replayTag { margin-left: 4px; }
.replayTag.sport-ultimate { background: rgba(56, 189, 248, 0.18); color: #7dd3fc; }
.replayTag.sport-soccer { background: rgba(74, 222, 128, 0.18); color: #86efac; }
.replayTag.sport-ctf, .replayTag.sport-neutralFlag { background: rgba(251, 191, 36, 0.18); color: #fcd34d; }
.replayFilters { display: flex; flex-direction: column; gap: 6px; margin: 0 0 10px; }
.replayChipRow { display: flex; flex-wrap: wrap; gap: 6px; }
.replayChip {
  border: 1px solid rgba(255, 255, 255, 0.16); background: transparent; color: rgba(255, 255, 255, 0.7);
  border-radius: 999px; padding: 3px 10px; font-size: 0.78rem; font-weight: 600; cursor: pointer;
}
.replayChip:hover { border-color: rgba(255, 255, 255, 0.35); color: #fff; }
.replayChip.active { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.4); color: #fff; }
.replayPager {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.replayPager button:disabled { opacity: 0.35; cursor: default; }
.replayPagerLabel { color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; font-variant-numeric: tabular-nums; }

/* --- Academy tutorial ---
   Redesigned after looking at a reference TagPro Academy site the user
   pointed at — same spirit (a squared-off "gaming HUD" display face for
   titles, a single muted category-badge color instead of a rainbow per
   tag, clearer Goal/Tips structure) reusing Cappit's OWN existing Academy
   identity color (--teal, already Academy's mode-card accent on the
   lobby — see .modeCard.accent-teal) as the through-line rather than
   inventing a new one. */
.academyScreen { padding: 18px; }
.academyHeader { display: flex; align-items: center; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
.academyHeader h2 { margin: 0; font-size: 1.3rem; font-family: "Oxanium", Inter, system-ui, sans-serif; font-weight: 700; letter-spacing: 0.01em; flex: 1; }
.academyProgressSummary { color: rgba(255, 255, 255, 0.6); font-size: 0.85rem; }
/* Track tabs — one per academy (CTF / Ultimate / Soccer), pill-style,
   sitting between the header and the lesson grid. */
.academyTabs { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
.academyTab {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-family: inherit; font-size: 0.88rem;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.academyTab:hover { background: rgba(255, 255, 255, 0.09); }
.academyTab.active { border-color: var(--teal); background: rgba(66, 214, 188, 0.12); }
.academyTab .academyTabCount { color: rgba(255, 255, 255, 0.55); font-size: 0.78rem; }
/* Guest-only "sign in to save this" banner atop the lesson grid — same
   quiet-box treatment as .matchmakingGuestGate, just lower-key (this one
   isn't blocking anything, so it shouldn't compete with the grid below). */
.academyGuestBanner {
  text-align: center;
  padding: 14px 16px;
  margin-bottom: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}
.academyGuestBanner .lobbyHint { display: block; margin-bottom: 10px; }
/* Guest-only sign-in + Discord CTA at the bottom of the post-match
   scoreboard — same quiet-box treatment as .academyGuestBanner, the
   engagement-peak moment right after a real match is over. */
.scoreboardCta {
  text-align: center;
  padding: 14px 16px;
  margin-bottom: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}
.scoreboardCta .lobbyHint { display: block; margin-bottom: 10px; }
.scoreboardCta .authButtonRow { margin-bottom: 10px; }
.scoreboardCta .discordBtn { display: inline-flex; }
/* New win-milestone degree, shown right at the top of the post-match
   scoreboard (owner request 2026-09-02 — see renderScoreboard's own
   comment for why the HUD ticker line alone wasn't enough). Empty and
   collapses to nothing (:empty) most matches — only ever holds content
   the rare time a fresh degree was actually just earned, so it needs its
   own accent to stand out from the quieter .scoreboardCta/rank-callout
   boxes around it rather than reusing their neutral treatment. */
#scoreboardDegreeBanner:empty { display: none; }
#scoreboardDegreeBanner .degreeBanner {
  text-align: center;
  margin: 0 0 16px;
  padding: 12px 16px;
  border: 1px solid rgba(255, 204, 46, 0.4);
  border-radius: 12px;
  background: rgba(255, 204, 46, 0.08);
  color: var(--gold);
  font-size: 0.92rem;
}
#scoreboardDegreeBanner .degreeBanner strong { color: #fff; }
/* Bot ladder "play real people instead" exit (owner: "we want to let
   people hop out of the flow and play humans" -> later, live: "should
   be a button like the other 'Play Regular Bots' and 'Play Again'
   buttons" — the original quiet text-link treatment read as too easy to
   miss/skip next to real buttons -> still live, screenshot: "the corners
   were not rounded and it needs a little more space underneath it" —
   the FIRST attempt at this fix set the class in the HTML template but
   never actually applied it, so this button was rendering with none of
   .secondary's real button chrome at all, just a bare grey rectangle).
   Real fix: the button element now carries BOTH "secondary" (the actual
   rounded/padded/consistent-height button look, matching Play Again
   exactly) and "ladderExitBtn" (just this one extra bottom margin, so
   it doesn't sit flush against the stats table right below it). */
.ladderExitBtn { margin-top: 10px; margin-bottom: 10px; }
/* Bot ladder flair reward (2026-09-07, owner: "these flairs/callouts
   need to be more dramatic... put the flair on it's own line, some
   confetti around it, to make it feel more like a real reward"). Its
   own dedicated box — NOT a reuse of .degreeBanner above, which is
   scoped to #scoreboardDegreeBanner specifically and never actually
   applied here (a mismatch caught while building this very effect). */
.ladderRewardNote {
  text-align: center;
  margin: 8px 0 0;
  padding: 18px 16px 14px;
  border: 1px solid rgba(255, 204, 46, 0.4);
  border-radius: 12px;
  background: rgba(255, 204, 46, 0.08);
}
.ladderRewardNote:empty { display: none; }
.ladderRewardIconWrap { position: relative; display: inline-block; }
.ladderRewardPop {
  font-size: 2.6rem;
  line-height: 1;
  animation: ladderRewardPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes ladderRewardPop {
  0% { transform: scale(0); opacity: 0; }
  60% { transform: scale(1.3); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.ladderRewardLabel {
  margin-top: 8px;
  color: var(--gold);
  font-size: 0.92rem;
  line-height: 1.5;
}
.ladderRewardLabel strong { color: #fff; font-size: 1.05rem; }
/* Thrown to a random per-piece angle/distance/rotation via --dx/--dy/
   --rot custom properties set in JS (client.js) — deliberately NOT one
   shared keyframe, which would make every piece move identically and
   read as a single rigid shape instead of a scatter. Centered on the
   icon (position:relative on .ladderRewardIconWrap above), not the
   whole banner, so the burst reads as coming from the flair itself. */
.ladderConfettiPiece {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 10px;
  margin: -5px 0 0 -3px;
  opacity: 0;
  animation: ladderConfettiBurst 1s ease-out forwards;
}
@keyframes ladderConfettiBurst {
  0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ladderRewardPop { animation: none; }
  .ladderConfettiPiece { display: none; }
}
/* "Back to Lobby" — ends the shared post-match wait immediately instead
   of waiting for it to time out (2026-08-31). A static sibling of
   scoreboardBody in the modal, not part of its own re-rendered content. */
.scoreboardActions { text-align: center; margin-top: 14px; }
.scoreboardActions button { min-width: 200px; }
/* Play Again (owner report 2026-09-05, ambient pickup matches only) —
   stacked, not side-by-side, so the primary size choice (whichever room
   the player was just in) reads as dominant over the "try the other
   size" secondary option right under it. */
#scoreboardPlayAgainActions { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
/* A row of two secondary/tertiary buttons that would otherwise each get
   their own full-width stacked row (owner, live: the real CrazyGames
   iframe height is short enough that every row of buttons costs real
   scroll room — "we may need to combine the buttons onto one row").
   Buttons keep their own .secondary sizing (padding/height); this only
   changes their width. */
.scoreboardActionsRow { display: flex; gap: 8px; }
.scoreboardActionsRow > * { flex: 1; margin-top: 0; margin-bottom: 0; }
.academyLessonGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 14px; }
.lessonCard {
  position: relative;
  text-align: left;
  padding: 15px 16px 16px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: background 0.15s, border-color 0.15s, transform 0.15s;
  overflow: hidden;
}
/* A thin top accent bar ties every drill card back to Academy's own
   teal identity without tinting the whole card — visible on hover/
   completion, quiet otherwise so an untouched grid doesn't look striped. */
.lessonCard::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--teal); opacity: 0; transition: opacity 0.15s;
}
.lessonCard:hover { background: rgba(255, 255, 255, 0.09); transform: translateY(-1px); }
.lessonCard:hover::before { opacity: 0.55; }
.lessonCard.completed { border-color: rgba(120, 220, 150, 0.4); }
.lessonCard.completed::before { opacity: 0.85; background: #7adc96; }
.lessonCard .lessonCardTop { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.lessonCard .lessonNumber {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.5);
  font-family: "Oxanium", Inter, system-ui, sans-serif; font-weight: 700;
}
.lessonCard .lessonTitle { font-weight: 700; font-size: 1.05rem; font-family: "Oxanium", Inter, system-ui, sans-serif; letter-spacing: 0.005em; }
.lessonCard .lessonSubtitle { font-size: 0.82rem; color: rgba(255, 255, 255, 0.6); line-height: 1.35; }
.lessonCard .lessonMeta { display: flex; gap: 8px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
/* Deliberately ONE muted color for every category (MOVEMENT/OFFENSE/
   CHASE/etc.), not a different hue per tag — the reference site's own
   badges read as "this is a category label," not as competing brand
   colors fighting the teal accent above. */
.lessonBadge {
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
  padding: 3px 8px; border-radius: 999px;
  background: rgba(199, 168, 242, 0.16); color: #d9c2f7;
}
/* Beginner/Intermediate/Advanced — a quiet traffic-light progression
   (teal -> amber -> red) rather than another badge fighting for
   attention; text stays neutral, only the dot carries the color. */
.lessonDifficulty { font-size: 0.68rem; color: rgba(255, 255, 255, 0.55); display: inline-flex; align-items: center; gap: 5px; }
.lessonDifficulty::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.lessonDifficulty.diff-beginner { color: var(--teal); }
.lessonDifficulty.diff-intermediate { color: #ffcc66; }
.lessonDifficulty.diff-advanced { color: var(--red); }
.lessonCard .lessonDone { color: #7adc96; font-weight: 700; font-size: 0.78rem; white-space: nowrap; }
.lessonCard .lessonBestTime { font-size: 0.72rem; color: rgba(255, 255, 255, 0.5); font-variant-numeric: tabular-nums; }

.academyBar {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  /* above the game-mode .topbar (z-index 20), an invisible full-width
     strip that otherwise swallows every click on Exit/Retry up here */
  z-index: 25;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(7, 16, 30, 0.85);
  backdrop-filter: blur(6px);
  max-width: 94%;
}
.academyLessonTitle { font-weight: 700; white-space: nowrap; }
.academyProgressText { color: rgba(255, 255, 255, 0.7); font-size: 0.85rem; white-space: nowrap; }
/* Every lesson has a real time limit that fails the attempt — this used
   to only ever show as part of ONE lesson's own progressText string
   ("Hold: Xs"); a dedicated element makes it visible for all of them at
   once instead of depending on what each lesson's own check() happens to
   say. Louder than .academyProgressText (bigger, tabular so the digits
   don't jitter the layout as they change) and shifts to red once the
   attempt is genuinely running out, mirroring the bomb-timer's own
   "get louder as it matters more" treatment. */
.academyTimer { font-weight: 700; font-size: 0.95rem; font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
.academyTimer.urgent { color: var(--red); }

/* The "pop up goal" callout (see announceAcademyGoal in client.js) —
   sits just under .academyBar, out of the way of the actual play area,
   and is driven entirely by JS toggling this one "show" class: hidden/
   transparent/shifted-up by default, animates down+in on "show", holds,
   then the same transition reverses when the class is removed. No
   keyframe animation needed for the reveal itself since it's just a
   two-state transition; the "pop" feel comes from the ease-out curve and
   a slight overshoot via cubic-bezier rather than a separate keyframe. */
.academyGoalPopup {
  position: absolute;
  top: 64px;
  left: 50%;
  transform: translate(-50%, -10px);
  width: min(92%, 480px);
  padding: 10px 18px;
  border-radius: 10px;
  border: 1px solid rgba(45, 212, 191, 0.45);
  background: rgba(10, 30, 28, 0.92);
  backdrop-filter: blur(6px);
  color: var(--text);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.35;
  text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease-out, transform 0.35s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.academyGoalPopup.show { opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) {
  .academyGoalPopup { transition: opacity 0.2s linear; }
  .academyGoalPopup.show { transform: translate(-50%, 0); }
}

.academyCard {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  max-width: 440px;
  /* Safety net for a short real viewport (owner, live screenshot: card
     content taller than the visible area pushed its own top edge OFF
     SCREEN, since centering with no height limit just lets it grow past
     both edges equally). .stage (the positioning ancestor) has a real
     flex-computed height, so this percentage tracks it correctly — a
     genuine scrollbar only ever appears if a card is STILL too tall
     after this pass's own content trims. */
  max-height: 90%;
  overflow-y: auto;
  padding: 22px 24px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-top: 2px solid rgba(45, 212, 191, 0.5);
  background: rgba(7, 16, 30, 0.94);
  backdrop-filter: blur(8px);
  text-align: center;
}
.academyCard h3 { margin: 0 0 10px; font-size: 1.25rem; font-family: "Oxanium", Inter, system-ui, sans-serif; font-weight: 700; }
.academyCard.success h3 { color: #7adc96; }
.academyCard.fail h3 { color: var(--red); }
.academyCard p { color: rgba(255, 255, 255, 0.7); margin: 0 0 16px; line-height: 1.4; }
.academyCard .academyCardActions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* Onboarding's own completion card (owner request 2026-09-05: "move the
   play now button above the join the discord CTA -- it's smaller and
   may get lost... larger as well") — Play Now stacked above, and above,
   a plainly secondary Keep Learning, instead of two same-row buttons
   competing for attention next to a brand-colored Discord button below. */
.academyCard .academyCardActions.stacked { flex-direction: column; align-items: stretch; }
/* Lesson-1 completion card's auto-advance-to-the-ladder countdown (owner:
   "throw them in the 3v3 ladder after ~5 seconds... not rely on them
   hitting Play Now" — visible countdown + an explicit pause, "some
   standard UI/UX rules" for an auto-advancing UI: show the time
   remaining AND give a real one-click way to cancel it, same idea as a
   video site's "up next" countdown). Quiet, secondary text under the
   real buttons — the countdown is a convenience/nudge, not something
   competing with Play Now/Keep Learning for attention. */
.onboardAutoAdvanceHint {
  margin: 8px 0 0;
  text-align: center;
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.82rem;
}
.onboardAutoAdvancePauseBtn {
  background: none;
  border: none;
  padding: 0;
  color: rgba(255, 255, 255, 0.75);
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
}
.onboardAutoAdvancePauseBtn:hover { color: #fff; }
.academyCardMeta { display: flex; gap: 8px; justify-content: center; margin-bottom: 10px; }
/* The pre-lesson "what am I actually trying to do" callout — set apart
   from the plain takeaway/tip text below with its own quiet box and a
   target icon, so it reads as THE goal rather than one more line of
   body copy (this was previously just a bare `<p><strong>Goal:</strong>`
   with no visual weight of its own). */
.academyGoalLine {
  display: flex; align-items: flex-start; gap: 10px; text-align: left;
  margin: 0 0 14px; padding: 10px 12px;
  border-radius: 10px; border: 1px solid rgba(45, 212, 191, 0.3); background: rgba(45, 212, 191, 0.08);
  color: rgba(255, 255, 255, 0.85); font-size: 0.9rem; line-height: 1.4;
}
.academyGoalLine strong { color: var(--teal); }
.academyGoalIcon { font-size: 1.05rem; line-height: 1.3; flex-shrink: 0; }
.academyTipsList {
  text-align: left; margin: 0 0 16px; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 7px;
  color: rgba(255, 255, 255, 0.72); font-size: 0.85rem; line-height: 1.4;
}
.academyTipsList li { position: relative; padding-left: 20px; }
.academyTipsList li::before { content: "\2713"; position: absolute; left: 0; color: var(--teal); font-weight: 700; }
/* The periodic guest sign-in nudge on a lesson's success card (see
   ACADEMY_SIGNIN_NUDGE_LESSON_NUMBERS) — visually set apart from the
   "Cleared!" celebration text above it as its own quiet, secondary box,
   not competing with the main "Next Lesson" action below. */
.academyNudge {
  margin: 0 0 16px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
}
/* Owner, live screenshot (lesson-1 completion card): "The Keep Learning
   button needs some space to separate it from the Discord CTA." This
   card's own Play Now/Keep Learning actions sit directly above the
   Discord nudge box (the OTHER .academyNudge usage, on a regular
   lesson's "Cleared!" card, has it in the opposite order — before the
   actions, already separated by the takeaway paragraph's own margin —
   so this is scoped to the actions-then-nudge order specifically). */
.academyCardActions + .academyNudge { margin-top: 14px; }
.academyNudge p { margin: 0 0 10px; font-size: 0.85rem; }
/* Onboarding's own completion card, forced-flow only (owner, live
   screenshot: "still way too much info/card is too tall here" — the
   card's own top edge was landing off-screen on the real CrazyGames
   viewport). Same Discord CTA the boxed .academyNudge above gives every
   other Academy card, just a single compact line instead of a padded
   box + full brand button — the one screen tightest on real vertical
   room, right after a first-timer's very first cleared drill. */
.academyDiscordLink {
  display: block;
  margin-top: 12px;
  color: #8ea1ff;
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
}
.academyDiscordLink:hover { text-decoration: underline; color: #a7b6ff; }

/* Sign-in provider picker (see #signInModal) */
.signInModalCard { max-width: 380px; }
.signInProviderRow { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }

@media (max-width: 760px) {
  .topbar, .footer { flex-direction: column; align-items: flex-start; }
  .lobbyColumns, .rosterColumns { grid-template-columns: 1fr; }
  .heroBand { grid-template-columns: 1fr; }
  .heroBandRight { border-left: none; border-top: 1px solid var(--line); min-height: 0; }
  .heroLogo { width: min(100%, 220px); }
  /* Nav buttons rendered at visibly different sizes on mobile (mixed
     .secondary buttons, .secondary anchors, and .reset buttons, each with
     its own padding/line-height) — normalize the whole accountBar to one
     compact, consistent control size and let it wrap cleanly. */
  .topbarRight { align-items: stretch; width: 100%; }
  .accountBar { flex-wrap: wrap; gap: 6px; }
  .accountBar .secondary,
  .accountBar .reset,
  .accountBar .discordBtn,
  .accountBar .accountMenuBtn,
  .accountBar a.secondary {
    padding: 6px 10px;
    font-size: 0.8rem;
    line-height: 1.2;
    margin: 0;
    display: inline-flex;
    align-items: center;
    border-radius: 8px;
  }
  .accountSignedIn { flex-wrap: wrap; gap: 6px; }
}
/* --- mini-game chooser (2026-09-09, owner request modelled on Super
   Mario Party's mini-game board): a grid of art cards you pick one of, or
   up to four of in order, plus a Random button.

   The art is the point of this screen — the owner asked for "images for
   each mini-game card here instead of just giving them blocks of text" —
   so the card is mostly artwork with the name and pitch underneath, and
   the pitch is what gets sacrificed at narrow widths rather than the
   image. --- */
.mgGrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 4px 0 16px;
}
.mgCard {
  position: relative;
  padding: 0 0 9px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  overflow: hidden;
  text-align: left;
  transition: border-color 0.12s, background 0.12s, transform 0.12s;
}
.mgCard:hover { border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px); }
.mgCard.selected { border-color: var(--blue); background: rgba(74, 158, 255, 0.14); }
.mgCardArt {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: #cfe3f7; /* the card art's own ice, so a slow-loading image doesn't flash the dark panel */
}
.mgCardName { font-weight: 700; font-size: 0.86rem; margin: 8px 10px 0; }
.mgCardBlurb { font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); margin: 3px 10px 0; line-height: 1.35; }
/* The pick order, shown only in a multi-game series — in a single-game
   pick the number would be noise, since there's only ever a 1. */
.mgCardOrder {
  position: absolute;
  top: 7px;
  left: 7px;
  min-width: 21px;
  height: 21px;
  padding: 0 5px;
  border-radius: 11px;
  background: var(--blue);
  color: #06121f;
  font-weight: 800;
  font-size: 0.74rem;
  line-height: 21px;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}
/* An admin-only mini-game still being playtested. It's shown rather than
   hidden so the owner can see the whole board at once; a normal player
   never receives these cards at all (the server's own /api/maps gate). */
.mgCardTest {
  position: absolute;
  top: 7px;
  right: 7px;
  padding: 2px 7px;
  border-radius: 9px;
  background: rgba(7, 17, 29, 0.85);
  color: var(--gold);
  font-weight: 700;
  font-size: 0.64rem;
  letter-spacing: 0.04em;
}
/* A one-seat game. Sits on the LEFT so it never collides with the
   TESTING badge on the right — Target Range currently carries both. */
.mgCardSolo {
  position: absolute;
  top: 7px;
  left: 7px;
  padding: 2px 7px;
  border-radius: 9px;
  background: rgba(7, 17, 29, 0.85);
  color: rgba(160, 205, 255, 0.95);
  font-weight: 700;
  font-size: 0.64rem;
  letter-spacing: 0.04em;
}
/* Solo games can't be a leg of a series, so while one is being built they
   read as unavailable rather than disappearing from the board. Dims the
   art and the text but NOT the badges or the hover overlay — the overlay
   is the bit that says WHY it can't be picked, and dimming the whole card
   would fade the explanation along with everything else. */
.mgCardBarred { cursor: not-allowed; }
.mgCardBarred .mgCardArt,
.mgCardBarred .mgCardName,
.mgCardBarred .mgCardBlurb { opacity: 0.38; }
.mgCardBarred:hover { transform: none; }
.mgBar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.mgBar .mgCount { font-size: 0.78rem; color: rgba(255, 255, 255, 0.55); margin-left: auto; }
.mgSlots { display: flex; gap: 6px; align-items: center; }
.mgSlot {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  border: 1px dashed rgba(255, 255, 255, 0.22);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 24px;
  text-align: center;
  color: rgba(255, 255, 255, 0.35);
}
.mgSlot.filled { border-style: solid; border-color: var(--blue); background: rgba(74, 158, 255, 0.16); color: var(--text); }
@media (max-width: 640px) {
  .mgGrid { grid-template-columns: repeat(2, 1fr); }
  .mgCardBlurb { display: none; }
}

/* Mini-game series banner — shown in the waiting room between games so a
   series reads as one thing with a running score, rather than four
   unrelated matches that happen to share a room code. */
.mgSeriesBar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 10px 0 4px;
  padding: 9px 13px;
  border-radius: 11px;
  border: 1px solid rgba(74, 158, 255, 0.35);
  background: rgba(74, 158, 255, 0.09);
}
.mgSeriesNow { font-weight: 700; font-size: 0.9rem; }
.mgSeriesNow .mgSeriesStep { color: rgba(255, 255, 255, 0.5); font-weight: 600; margin-right: 6px; }
.mgSeriesScore { margin-left: auto; font-weight: 800; font-size: 0.95rem; letter-spacing: 0.02em; }
.mgSeriesScore .b { color: var(--blue); }
.mgSeriesScore .r { color: var(--red); }
.mgSeriesList { display: flex; gap: 5px; flex-wrap: wrap; width: 100%; }
.mgSeriesPip {
  padding: 2px 8px;
  border-radius: 8px;
  font-size: 0.68rem;
  font-weight: 600;
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.45);
}
.mgSeriesPip.done { border-color: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.3); text-decoration: line-through; }
.mgSeriesPip.now { border-color: var(--blue); background: rgba(74, 158, 255, 0.18); color: var(--text); }

/* Final series result on the post-match scoreboard (2026-09-10, owner:
   "When all 4 games are complete, we need a final scoreboard showing the
   winners"). Deliberately bigger and louder than the running seriesBar
   above — this one is the payoff for four games, not a status line. */
.mgSeriesFinal { text-align: left; }
.mgSeriesVerdict {
  margin: 0 0 8px;
  font-weight: 800;
  font-size: 1.05rem;
  text-align: center;
}
.mgSeriesBreakdown {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
}
.mgSeriesBreakdown li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.82rem;
  color: rgba(255, 255, 255, 0.72);
  padding: 4px 8px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
}
.mgSeriesBreakdown li strong { margin-left: auto; color: var(--text); }
.mgSeriesGameNo {
  min-width: 18px;
  height: 18px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  font-size: 0.66rem;
  font-weight: 700;
  background: rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.75);
}

/* Mini-game rating on the post-match scoreboard (2026-09-09). Sized to
   read as a single quick question, not a survey — the thumbs are the
   whole ask, and the comment box only appears once one is clicked, so
   nobody has to decide whether to write something before they've
   answered the easy part. */
.mgRate {
  margin: 12px 0 4px;
  padding: 11px 13px;
  border-radius: 11px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
}
.mgRateQ { font-size: 0.86rem; font-weight: 600; margin: 0 0 9px; }
.mgRateBtns { display: flex; gap: 8px; }
.mgRateBtn {
  flex: 1;
  padding: 9px;
  border-radius: 9px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font-size: 1.05rem;
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
.mgRateBtn:hover { border-color: rgba(255, 255, 255, 0.35); }
.mgRateBtn.up.picked { border-color: var(--green); background: rgba(74, 222, 128, 0.16); }
.mgRateBtn.down.picked { border-color: var(--red); background: rgba(255, 90, 90, 0.16); }
.mgRateComment { display: flex; gap: 7px; margin-top: 9px; }
.mgRateComment input {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  font: inherit;
  font-size: 0.82rem;
}
.mgRateComment button { padding: 8px 13px; border-radius: 8px; border: none; background: var(--blue); color: #06121f; font-weight: 700; cursor: pointer; }
.mgRateThanks { font-size: 0.8rem; color: rgba(255, 255, 255, 0.55); margin: 8px 0 0; }

/* Segmented toggles in the chooser's top bar (2026-09-09). Both decisions
   a player makes before touching a card — how many games, and whether the
   room is listed — live up here where they're visible, because clicking a
   card now launches immediately and there's no later screen to change
   your mind on. */
/* Mini-game setup step — the screen between picking a game and playing it.
   It REPLACES the card grid rather than sitting under it: the earlier
   layout stranded its launch button below thirteen cards, off the bottom
   of the modal, and every click looked like it had done nothing. */
/* A key named inside a how-to line, rendered from the player's own
   binding rather than written into the prose (see keyifyHowTo). */
.mgKey {
  display: inline-block; padding: 1px 7px; margin: 0 1px; border-radius: 5px;
  background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.26);
  font: 600 0.92em ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.02em;
}
.mgSetup { display: flex; flex-direction: column; gap: 16px; }
.mgSetupHead { display: flex; gap: 14px; align-items: flex-start; }
.mgSetupArt {
  width: 168px; max-width: 40%; border-radius: 10px; flex: none;
  border: 1px solid rgba(255, 255, 255, 0.14);
}
.mgSetupText { min-width: 0; }
.mgSetupText h4 { margin: 0 0 4px; font-size: 1.12rem; }
.mgSetupBlurb { margin: 0 0 8px; color: rgba(255, 255, 255, 0.72); font-size: 0.86rem; }
.mgSetupHow { margin: 0 0 5px; color: rgba(255, 255, 255, 0.58); font-size: 0.8rem; line-height: 1.45; }
.mgSetupRows { display: flex; flex-direction: column; gap: 10px; }
.mgSetupRow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mgSetupLabel {
  min-width: 68px; font-size: 0.78rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: rgba(255, 255, 255, 0.5);
}
/* The explanation of what the current choice MEANS, which is the part a
   segmented control can't say on its own. */
.mgSetupNote { font-size: 0.78rem; color: rgba(255, 255, 255, 0.5); flex: 1 1 200px; }
/* A game whose roster is fixed states its format as a fact. Offering dead
   buttons instead invites a choice the launch would silently discard. */
.mgSetupFixed { font-size: 0.86rem; color: rgba(255, 255, 255, 0.72); }
.mgSetupFoot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-top: 4px; border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mgSetupPlay { font-size: 0.98rem; padding: 10px 22px; }
@media (max-width: 560px) {
  .mgSetupHead { flex-direction: column; }
  .mgSetupArt { width: 100%; max-width: none; }
  .mgSetupFoot { flex-direction: column-reverse; align-items: stretch; }
}
.mgSeg { display: inline-flex; border-radius: 9px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.16); }
.mgSeg button {
  border: none;
  background: rgba(255, 255, 255, 0.04);
  color: rgba(255, 255, 255, 0.62);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 7px 13px;
  cursor: pointer;
}
.mgSeg button + button { border-left: 1px solid rgba(255, 255, 255, 0.16); }
.mgSeg button:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
.mgSeg button.on { background: rgba(74, 158, 255, 0.22); color: var(--text); }

/* The play affordance on a card in single-game mode. The card click is
   the launch, so it needs to LOOK clickable before you click it — the
   previous layout gave no signal at all, and the real button was below
   the fold. */
.mgCardPlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(7, 17, 29, 0.55);
  color: #fff;
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.02em;
  opacity: 0;
  transition: opacity 0.12s;
}
.mgCard:hover .mgCardPlay, .mgCard:focus-visible .mgCardPlay { opacity: 1; }
/* Touch has no hover, so the affordance is always visible there instead
   of never. */
@media (hover: none) {
  .mgCardPlay { opacity: 1; background: rgba(7, 17, 29, 0.28); align-items: flex-start; justify-content: flex-end; padding: 8px; font-size: 0.78rem; }
}

/* The series launch row. Sticky to the modal's bottom edge so a grid of
   ten cards can never push it out of sight — the exact failure the old
   static row had. */
.mgFooter {
  position: sticky;
  bottom: -22px;
  margin: 4px -20px -22px;
  padding: 12px 20px 16px;
  background: linear-gradient(to bottom, rgba(7, 16, 30, 0), var(--panel) 26%);
}
.mgFooter .primary { width: 100%; }

/* Mini-game how-to card — replaces staring at a ten-second countdown with
   two lines telling you what you're about to do. Deliberately NOT a modal:
   it sits over the rink, needs no dismissing, and disappears on its own
   the instant the freeze ends. */
.mgHowTo {
  position: absolute;
  left: 50%;
  top: 16%;
  transform: translateX(-50%);
  z-index: 30;
  width: min(440px, calc(100vw - 32px));
  padding: 14px 18px 16px;
  border-radius: 14px;
  border: 1px solid rgba(74, 158, 255, 0.4);
  background: rgba(7, 16, 30, 0.93);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
  text-align: center;
  pointer-events: none; /* never eats a click in the first seconds of a match */
}
.mgHowToName { font-weight: 800; font-size: 1.05rem; letter-spacing: 0.01em; }
.mgHowToObjective { margin-top: 5px; font-size: 0.92rem; font-weight: 600; color: var(--blue); }
.mgHowToControls { margin-top: 7px; font-size: 0.8rem; line-height: 1.45; color: rgba(255, 255, 255, 0.72); }
.mgHowToGo { margin-top: 10px; font-size: 0.76rem; font-weight: 700; letter-spacing: 0.06em; color: rgba(255, 255, 255, 0.5); }

/* Leave Room in the waiting room. Pulled out of the header and parked at
   the far right of the action row, in a real colour, because as a quiet
   outline button up near the invite link it kept getting lost. Warm red
   rather than the destructive red used for kicking someone — leaving is
   normal, it just needs to be findable. */
.waitingLeave {
  margin-left: auto;
  padding: 10px 18px;
  border-radius: 10px;
  border: 1px solid rgba(255, 90, 90, 0.5);
  background: rgba(255, 90, 90, 0.14);
  color: #ff8f8f;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.waitingLeave:hover { background: rgba(255, 90, 90, 0.26); border-color: rgba(255, 90, 90, 0.8); color: #fff; }

/* Waiting-room headline when the room is a mini-game: the GAME's name,
   sized up, with its one-line objective under it. The room name
   ("BilldaCat vs Bots") says nothing about what you're about to play, and
   in a series the map changes underneath a fixed room name. */
.waitingTitle.isMinigame { font-size: 1.6rem; letter-spacing: -0.01em; }
.waitingObjective { margin-top: 2px; font-size: 0.92rem; font-weight: 600; color: var(--blue); }

/* Free-for-all lobby roster. Two team columns actively misdescribe a
   1v1v1v1 game — they tell you the player next to you is on your side
   when they are the person you are about to fight. So on an FFA map the
   columns are replaced by one list of four, each row carrying its own
   colour. */
.rosterFfa { margin-top: 4px; }
.rosterFfa h3 { margin: 0 0 8px; font-size: 0.82rem; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(255, 255, 255, 0.5); }
.rosterFfa .rosterList { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
@media (max-width: 640px) { .rosterFfa .rosterList { grid-template-columns: 1fr; } }
/* The colour swatch that replaces "which column am I in". */
.ffaDot {
  display: inline-block;
  width: 11px; height: 11px;
  border-radius: 50%;
  margin-right: 8px;
  vertical-align: -1px;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35);
}

/* Third and fourth team columns (2026-09-09). The grid goes to two-up on
   three or four sides rather than four-across, because four columns of
   eight seats each is unreadably narrow on anything but a desktop. */
.rosterColumns.teams3, .rosterColumns.teams4 { grid-template-columns: repeat(2, 1fr); }
.rosterTeam.green h3 { color: #4ade80; }
.rosterTeam.yellow h3 { color: #ffcc2e; }

/* Third and fourth side pills in the in-match bar (2026-09-09). Green and
   yellow reuse the same hues as everywhere else so a player recognises
   their own colour from the lobby, the ice and the scoreboard alike. */
.matchPill.green { background: linear-gradient(180deg, #4ade80, #1f8a4c); }
.matchPill.yellow { background: linear-gradient(180deg, #ffcc2e, #b8860b); }
.matchPill.yellow .matchTeamLabel, .matchPill.yellow .matchScoreBox { color: #3a2a00; }

/* Role suffix on a lobby team header (Freeze Tag's Chasers/Runners). The
   role is the part that decides how you'll play, so it isn't whispered —
   same weight as the colour it follows. */
.rosterTeam h3.hasRole { letter-spacing: 0.04em; }


/* First-time arrival cover (see #bootIntoGame in index.html). Opaque and
   full-bleed on purpose: its whole job is that a brand-new player never
   sees the front page flash past before the match they came for. Sits
   under the reconnecting pill (z-index 60) so a genuine connection
   problem still reads over the top of it. */
.bootIntoGame {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #07111d;
}
.bootIntoGameInner { text-align: center; }
.bootIntoGameTitle {
  font-size: 20px;
  font-weight: 700;
  color: #e8eaee;
  letter-spacing: 0.2px;
}
.bootIntoGameSub {
  margin-top: 6px;
  font-size: 13px;
  color: #8ea0b5;
}


/* Compact post-match stats (client.js's renderScoreboard). The full table
   is ~340px tall at 5v5, which is more vertical space than everything
   else on the card put together; this is the same seven stats at ~60% of
   the height. Nothing is removed — only padding, type size and header
   length change, plus numerics right-aligned so a column scans. */
.scoreboardTight th { padding: 3px 6px; font-size: 0.64rem; }
.scoreboardTight td { padding: 3px 6px; font-size: 0.72rem; border-top: 1px solid rgba(255, 255, 255, 0.055); }
.scoreboardTight td.num, .scoreboardTight th.num { text-align: right; width: 34px; }
.scoreboardTight .scoreboardNameCell {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px;
}
/* Team colour as a bar on the name cell instead of its own header row —
   saves two rows and reads faster than a coloured heading. */
.scoreboardTight tbody.blue tr td:first-child { box-shadow: inset 3px 0 0 var(--blue, #3b82f6); }
.scoreboardTight tbody.red tr td:first-child { box-shadow: inset 3px 0 0 var(--red, #ef4444); }
.scoreboardTight .ladderAvatar { width: 16px; height: 16px; margin-right: 5px; }

/* The fold. A first-timer has no idea what a good box score looks like,
   so it starts closed for them and open for everyone else — but whichever
   the player last chose is remembered, which is what stops the threshold
   from mattering. */
.scoreboardStatsFold {
  margin-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 8px;
  color: rgba(255, 255, 255, 0.5); font-size: 0.78rem; text-align: center;
  cursor: pointer; background: none; border-left: 0; border-right: 0; border-bottom: 0; width: 100%;
}
.scoreboardStatsFold:hover { color: rgba(255, 255, 255, 0.8); }


/* First-run post-match card (client.js's firstRunHeader/firstRunTiles).
   Only rendered for a player who arrived without a tutorial; everyone
   else gets the ordinary scoreboard. */
.scoreboardResult { text-align: center; padding: 10px 0 4px; }
.scoreboardResultBig {
  font: 800 26px/1.1 Oxanium, Inter, sans-serif; letter-spacing: 0.3px;
}
.scoreboardResultSub { color: rgba(255, 255, 255, 0.6); font-size: 0.8rem; margin-top: 5px; }
.scoreboardTiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 12px; }
.scoreboardTile {
  border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 10px;
  background: rgba(255, 255, 255, 0.04); padding: 9px 4px 7px;
  text-align: center; text-decoration: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 2px; color: inherit;
}
.scoreboardTile:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.22); }
.scoreboardTileIcon { font-size: 18px; line-height: 1; }
.scoreboardTileName { font: 700 0.68rem Inter, sans-serif; color: rgba(255, 255, 255, 0.82); }
.scoreboardTileSub { font-size: 0.58rem; color: rgba(255, 255, 255, 0.45); }
@media (max-width: 460px) {
  .scoreboardTiles { grid-template-columns: repeat(2, 1fr); }
  .scoreboardResultBig { font-size: 21px; }
}

/* ─────────────────────────────────────────────────────────────────────
   Rookie Cup — the onboarding tournament bracket, 8-bit.

   Owner: "retro 8-bit style graphics." Which is a real constraint, not a
   coat of paint: no gradients, no blur, no rounded corners, no
   half-pixels. Every border is a hard multiple of 2px and every shadow is
   a hard offset, because a blurred shadow is the one thing that instantly
   reads as "modern UI wearing a pixel font".
   ───────────────────────────────────────────────────────────────────── */
.cupWrap { --px: 2px; font-family: "Press Start 2P", ui-monospace, monospace; overflow-x: auto; padding: 4px 2px 10px; }
.cupGrid { display: flex; gap: 26px; align-items: stretch; min-width: max-content; margin-inline: auto; }
.cupCol { display: flex; flex-direction: column; gap: 10px; }
.cupColHead {
  font-size: 0.56rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: #6f8299; text-align: center; padding-bottom: 4px;
}
/* Even spacing per column is what makes a bracket read as a bracket:
   later rounds have fewer matches over the same height. */
.cupColBody { display: flex; flex-direction: column; justify-content: space-around; flex: 1; gap: 10px; }

.cupMatch {
  position: relative;
  border: var(--px) solid #3d4756;
  background: #121a26;
  box-shadow: var(--px) var(--px) 0 #0a0f17;   /* hard offset, never blurred */
  min-width: 164px;
}
.cupMatchNow { border-color: #ffd24a; box-shadow: var(--px) var(--px) 0 #6b5310; }
.cupMatchFlag {
  position: absolute; top: -9px; left: 8px;
  background: #ffd24a; color: #20160a;
  font-size: 0.42rem; padding: 2px 5px; letter-spacing: 0.06em;
}
.cupTeam {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  padding: 7px 8px; font-size: 0.5rem; line-height: 1.5; color: #c7d3e0;
}
.cupTeam + .cupTeam { border-top: var(--px) solid #3d4756; }
.cupTeamName { white-space: nowrap; overflow: hidden; text-overflow: clip; }
.cupTeamScore { color: #93a7bb; }
.cupTeamMe .cupTeamName { color: #4a9eff; }
.cupTeamWon { background: rgba(255, 210, 74, 0.10); }
.cupTeamWon .cupTeamName, .cupTeamWon .cupTeamScore { color: #ffd24a; }
.cupTeamOut { opacity: 0.45; }

.cupColCup .cupColBody { justify-content: center; }
.cupTrophy { font-size: 2rem; text-align: center; filter: grayscale(1); opacity: 0.4; }
.cupTrophyWon { filter: none; opacity: 1; }
.cupChampName { font-size: 0.5rem; color: #ffd24a; text-align: center; margin-top: 6px; }
.cupChampTbd { color: #4a5568; }

/* Surface picker */
.cupSurfRow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.cupSurf {
  font-family: "Press Start 2P", ui-monospace, monospace;
  border: 2px solid #3d4756; background: #121a26; color: #c7d3e0;
  box-shadow: 2px 2px 0 #0a0f17;
  padding: 10px 6px; cursor: pointer; text-align: center; display: block;
}
.cupSurf:hover:not(:disabled) { border-color: #6f8299; }
.cupSurf.on { border-color: #4a9eff; box-shadow: 2px 2px 0 #14304f; }
.cupSurf.locked { opacity: 0.4; cursor: not-allowed; }
.cupSurfName { display: block; font-size: 0.52rem; letter-spacing: 0.04em; }
.cupSurf.on .cupSurfName { color: #4a9eff; }
.cupSurfBlurb {
  display: block; margin-top: 6px; font-size: 0.4rem; line-height: 1.7;
  color: #7f93a8; font-family: Inter, system-ui, sans-serif;
}
.cupSurfNote { font-size: 0.72rem; color: #7f93a8; text-align: center; margin: 10px 0 0; }

@media (max-width: 640px) {
  .cupGrid { gap: 14px; }
  .cupMatch { min-width: 132px; }
  .cupSurfRow { grid-template-columns: 1fr; }
}

/* The march to the next degree, on the post-match scoreboard. */
.degreeProg { margin: 10px 0 2px; }
.degreeProgTop {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 0.72rem; color: #7f93a8; margin-bottom: 5px;
}
.degreeProgNow { font-weight: 700; color: #c7d3e0; }
.degreeProgNext { font-weight: 700; color: #ffd24a; margin-left: auto; }
.degreeProgNeed { color: #7f93a8; }
.degreeProgBar {
  height: 9px; border-radius: 5px; overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.10);
}
.degreeProgFill {
  height: 100%; border-radius: 5px;
  background: linear-gradient(90deg, #4a9eff, #ffd24a);
  transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p.degreeProg { font-size: 0.78rem; }

/* Rookie Cup screen. The bracket itself is styled above; this is the
   frame around it. */
.cupScreen { padding: 22px 24px 20px; }
.cupHeader { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.cupTitle {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.92rem; color: #ffd24a; letter-spacing: 0.02em;
}
.cupSub { color: #7f93a8; font-size: 0.8rem; margin-top: 7px; }
.cupRoundPill {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.5rem; color: #c7d3e0; white-space: nowrap;
  border: 2px solid #3d4756; background: #121a26; box-shadow: 2px 2px 0 #0a0f17;
  padding: 8px 10px;
}
.cupResultLine {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.56rem; line-height: 1.8; padding: 10px 12px; margin-bottom: 12px;
  border: 2px solid #3d4756; background: #121a26; box-shadow: 2px 2px 0 #0a0f17;
}
.cupResultLine.won { border-color: #ffd24a; color: #ffd24a; box-shadow: 2px 2px 0 #6b5310; }
.cupResultLine.lost { border-color: #6b3b3b; color: #e0a3a3; box-shadow: 2px 2px 0 #3a1d1d; }
.cupActions { display: flex; gap: 10px; align-items: center; margin-top: 16px; flex-wrap: wrap; }
/* Post-tutorial fork — see #cupFork in index.html / renderCupFork in client.js */
.cupFork { margin: 14px 0 6px; padding: 14px; border: 2px solid rgba(255, 255, 255, 0.14); border-radius: 12px; background: rgba(255, 255, 255, 0.04); }
.cupForkHead { font-size: 1.05rem; margin-bottom: 10px; }
.cupForkGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
/* The cup screen puts its pixel font on every button (.cupScreen button,
   below). Not here: the player has a few seconds to choose, so the fork
   reads in the ordinary interface font (owner, 2026-09-23, after
   testing: "that 8 bit font is a bit hard to read"). Same for the Join
   buttons in the waiting-games list underneath it. */
.cupScreen .cupForkBtn, .cupScreen .cupFork .roomBtn {
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 1rem; letter-spacing: normal; line-height: 1.3;
}
.cupForkBtn { text-align: left; padding: 12px 14px; border-radius: 10px; border: 2px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.06); color: inherit; cursor: pointer; display: flex; flex-direction: column; gap: 4px; }
.cupForkBtn:hover { background: rgba(255, 255, 255, 0.12); }
.cupForkBtn.isDefault { border-color: #ffd24a; box-shadow: 2px 2px 0 #6b5310; }
.cupForkBtn b { font-size: 1.05rem; }
.cupForkBtn .cupForkCount { color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; }
.cupForkBtn .cupForkTimer { color: #ffd24a; font-size: 0.85rem; min-height: 1em; }
.cupForkSub { margin: 12px 0 6px; color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; }
.cupForkError { color: #e0a3a3; margin-top: 8px; }
.cupPlayBtn {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.62rem; padding: 14px 18px; letter-spacing: 0.02em;
}
.cupPrize {
  margin-top: 14px; padding: 14px; text-align: center;
  border: 2px solid #ffd24a; background: rgba(255, 210, 74, 0.08); box-shadow: 2px 2px 0 #6b5310;
}
.cupPrizeBig { font-family: "Press Start 2P", ui-monospace, monospace; font-size: 0.78rem; color: #ffd24a; line-height: 1.8; }
.cupPrizeSub { color: #c7d3e0; font-size: 0.82rem; margin-top: 10px; line-height: 1.6; }
.cupPrizeTiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 14px; }
.cupPrizeTile {
  border: 2px solid rgba(255,255,255,.14); background: rgba(255,255,255,.04);
  padding: 10px 4px 8px; text-align: center; text-decoration: none; display: block; color: inherit;
}
.cupPrizeTile:hover { border-color: rgba(255,255,255,.34); }
.cupPrizeTileIcon { font-size: 18px; line-height: 1; }
.cupPrizeTileName { font: 700 10.5px Inter, system-ui, sans-serif; color: #c7d3e0; margin-top: 5px; }
@media (max-width: 640px) { .cupPrizeTiles { grid-template-columns: repeat(2, 1fr); } }

/* ── 8-bit bracket connectors ──────────────────────────────────────────
   Owner's reference: "Blocky Pixel Lines: Ditch smooth paths. Connect
   matches using jagged, stepped lines that mimic a strict grid
   resolution."

   So every segment here is purely horizontal or vertical, at a fixed 2px
   weight, meeting at hard right angles. No diagonals, no curves, no
   border-radius — a single smooth path is the fastest way to break the
   illusion. Built from pseudo-elements on the pair wrapper rather than
   an SVG overlay so the lines track the boxes' real positions when the
   bracket reflows.
   ───────────────────────────────────────────────────────────────────── */
.cupPair {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-around;  /* puts the two match centres at 25% / 75% */
  flex: 1;
  gap: 10px;
}
/* Stub out of each match, rightward. */
.cupCol:not(.cupColCup) .cupMatch::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  width: 13px;
  height: 2px;
  background: #4c586a;
}
/* The riser joining a pair: vertical, spanning centre-to-centre. */
.cupPair:not(.cupPairSolo)::before {
  content: "";
  position: absolute;
  left: calc(100% + 13px);
  top: 25%;
  bottom: 25%;
  width: 2px;
  background: #4c586a;
}
/* And the stub out of the riser's midpoint into the next column. */
.cupPair::after {
  content: "";
  position: absolute;
  left: calc(100% + 13px);
  top: 50%;
  width: 13px;
  height: 2px;
  background: #4c586a;
}
/* The final has no pair to join — it runs straight on to the trophy. */
.cupPairSolo::after { left: calc(100% + 13px); }

/* Capitalised, per the reference: "Keep text capitalized to match
   original game carts." */
.cupTeamName, .cupColHead, .cupMatchFlag, .cupChampName, .cupSurfName { text-transform: uppercase; }

/* The guest sign-up ask on the cup screen. */
.cupSignIn {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  border: 2px solid rgba(74,158,255,.45); background: rgba(60,120,220,.12);
  box-shadow: 2px 2px 0 #10203a;
  padding: 11px 13px; margin-bottom: 12px;
}
.cupSignInText { font-size: 0.82rem; color: #cfe2ff; }
.cupSignInBtn {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.5rem; padding: 10px 12px; white-space: nowrap; cursor: pointer;
  border: 2px solid rgba(120,180,255,.6); background: rgba(60,120,220,.3); color: #eaf3ff;
  box-shadow: 2px 2px 0 #10203a;
}
.cupSignInBtn:hover { border-color: #9cc6ff; }

/* Owner: "Play the Next Round should be centered." */
.cupActions { justify-content: center; }

/* ── One font rule for the cup screen ─────────────────────────────────
   Owner: "I do like the 8 bit font but we have 2 different font styles on
   this page... The most jarring instance is the Play the next round
   button is a different font than 'Back to home', which is right on the
   same row."

   The mix is worth keeping — Press Start 2P is unreadable for prose at
   small sizes — but it needs a rule rather than being ad hoc. The rule:

     pixel font = CHROME (headings, labels, buttons, the bracket itself)
     Inter      = PROSE (anything that is a sentence someone reads)

   Two buttons side by side are both chrome, so they must match. That was
   the actual bug: not the mixture, but two things in the same role
   wearing different faces.
   ───────────────────────────────────────────────────────────────────── */
.cupScreen button,
.cupScreen .v2act,
.cupScreen .primary {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.62rem;
  letter-spacing: 0.02em;
  line-height: 1.6;
}
/* "Back to home" sits beside the primary, so it gets the same metrics —
   only the weight of the treatment differs, which is what makes one read
   as secondary rather than as a different kind of control entirely. */
#cupLeaveBtn { padding: 14px 16px; }

/* A surface unlocking. Owner: "Maybe some sort of visual unlocking of the
   unlocked ice surface would help, so they notice it."

   Animated in steps() rather than smoothly — an eased fade is the exact
   thing that reads as modern UI, and the rest of this screen has already
   committed to hard edges. Three hard flashes, then it settles. */
@keyframes cupUnlockFlash {
  0%, 100% { border-color: #3d4756; background: #121a26; }
  8%, 24%, 40% { border-color: #ffd24a; background: rgba(255, 210, 74, 0.22); }
  16%, 32%, 48% { border-color: #3d4756; background: #121a26; }
  60%, 99% { border-color: #ffd24a; background: rgba(255, 210, 74, 0.10); }
}
@keyframes cupUnlockStamp {
  0% { transform: scale(0); }
  40% { transform: scale(0); }
  55% { transform: scale(1.4); }
  70% { transform: scale(1); }
  100% { transform: scale(1); }
}
.cupSurfFresh {
  position: relative;
  animation: cupUnlockFlash 1.6s steps(1, end) 1 both;
}
.cupSurfNew {
  position: absolute;
  top: -9px; left: 50%;
  margin-left: -34px;
  width: 68px;
  background: #ffd24a; color: #20160a;
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.38rem; line-height: 1.6; padding: 3px 0; text-align: center;
  animation: cupUnlockStamp 1.6s steps(1, end) 1 both;
}
/* Respect a reduced-motion preference: keep the highlight, drop the
   flashing. Three hard flashes is exactly the pattern that setting
   exists for. */
@media (prefers-reduced-motion: reduce) {
  .cupSurfFresh { animation: none; border-color: #ffd24a; background: rgba(255, 210, 74, 0.10); }
  .cupSurfNew { animation: none; }
}

/* Claim button inside the prize panel, so a champion never has to hunt
   back up the page for the guest banner's own one. */
.cupClaimBtn {
  font-family: "Press Start 2P", ui-monospace, monospace;
  font-size: 0.5rem; padding: 9px 12px; margin-left: 8px; cursor: pointer;
  border: 2px solid #ffd24a; background: rgba(255, 210, 74, 0.18); color: #ffd24a;
  box-shadow: 2px 2px 0 #6b5310; vertical-align: middle;
}
.cupClaimBtn:hover { background: rgba(255, 210, 74, 0.3); }
/* What is actually through the door, rather than the word "Home". */
.cupPrizeNext { font-size: 0.86rem; color: #c7d3e0; line-height: 1.6; margin: 12px auto 0; max-width: 60ch; }
.cupPrizeNext strong { color: #ffd24a; }
.cupPrizeTileSub { font-size: 9.5px; color: #7f93a8; margin-top: 2px; }
.cupPrizeTileLead { border-color: rgba(255, 210, 74, 0.55); background: rgba(255, 210, 74, 0.07); }
.cupTierNote { text-align: center; color: #7f93a8; font-size: 0.8rem; margin: 10px 0 0; }

/* Admin-only testing control on the cup screen — deliberately drab, so it
   never reads as part of the player-facing flow. */
.cupAdminBtn { opacity: 0.6; font-size: 0.46rem !important; }
/* Green and yellow sections. The scoreboard only ever drew blue and red,
   so these rules never existed — a 3- or 4-team match had no divider
   colour for its extra sides. */
.scoreboardTeam.green tr:first-child td { border-top: 2px solid #3ecf8e; }
.scoreboardTeam.yellow tr:first-child td { border-top: 2px solid #facc15; }

/* AFK warning (client.js showAfkWarning / server.js sweepAfk) — centred,
   loud, and above the HUD, since the player it's for isn't looking. */
#afkWarning {
  position: fixed; left: 50%; top: 38%; transform: translate(-50%, -50%);
  z-index: 60; pointer-events: none; text-align: center;
  padding: 16px 28px; border-radius: 12px;
  background: rgba(20, 8, 8, 0.88); border: 2px solid #ff5a4f;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  color: #fff; font-family: Inter, system-ui, sans-serif;
  max-width: calc(100vw - 32px);
}
#afkWarning[hidden] { display: none; }
#afkWarning .afkTitle { font-size: 26px; font-weight: 800; color: #ff8a80; }
#afkWarning .afkBody { font-size: 17px; margin-top: 4px; }
#afkWarning .afkBody b { font-size: 20px; }

/* "You're in goal!" (client.js showGoalieBanner) — a one-line strip under
   the top chat ticker, never over the crease; gone on the first key press,
   a click, or after 5s (owner, 2026-09-26: the old centred card covered too
   much of the screen for someone who just got dropped into net). */
#roleBanner {
  position: fixed; left: 50%; top: 104px; transform: translateX(-50%); /* below the chat ticker AND the one-time top tips (they reach ~90px) */
  z-index: 60; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px 6px 14px; border-radius: 999px;
  background: rgba(8, 16, 32, 0.82); border: 1px solid #4f9dff;
  color: #fff; font-family: Inter, system-ui, sans-serif; font-size: 14px; white-space: nowrap;
  max-width: calc(100vw - 32px); overflow: hidden; text-overflow: ellipsis;
}
#roleBanner[hidden] { display: none; }
#roleBanner strong { color: #9cc8ff; }
#roleBanner .roleClose { opacity: 0.6; font-size: 12px; margin-left: 2px; }
@media (pointer: coarse) { #roleBanner { top: 96px; font-size: 12px; } }

/* Pickup waiting room: surface vote (client.js renderSurfaceVote). */
.surfaceVote { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0 4px; }
.surfaceVote[hidden] { display: none; }
.surfaceVoteLabel { font-weight: 700; margin-right: 4px; }
.surfaceVoteBtn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 12px; border-radius: 8px; cursor: pointer;
  border: 2px solid transparent; font: inherit; font-weight: 600;
  background: var(--panel-2, #243044); color: inherit;
}
.surfaceVoteBtn.surface-ice { background: #d9ecf7; color: #10324a; }
.surfaceVoteBtn.surface-concrete { background: #c9c2b8; color: #2f2a24; }
.surfaceVoteBtn.surface-sand { background: #e8d3a2; color: #4a3814; }
.surfaceVoteBtn.leading { border-color: #ffd34d; }
.surfaceVoteBtn.mine { box-shadow: 0 0 0 3px #4f9dff; }
.surfaceVoteCount { min-width: 1.4em; padding: 0 6px; border-radius: 999px; background: rgba(0, 0, 0, 0.18); font-size: 0.85em; text-align: center; }

/* No double-tap zoom while a game is on screen (client.js setGameZoomLock).
   manipulation keeps taps and scrolling but drops double-tap zoom; the game
   canvas itself needs no browser gestures at all. */
html.inGame, html.inGame body { touch-action: manipulation; }
html.inGame canvas { touch-action: none; }

/* Mid-match "⇄ Help Red/Blue" (client.js updateLiveSwitchBtn): reads as an
   invitation, not a system control, so it's green rather than grey.
   Stacked just above Leave Room in the bottom-left corner — the same slot
   mini-games' Restart uses (bottom 64 = Leave's bottom 20 + ~34 tall +
   10 gap; the two never show together, Help is never offered in a
   mini-game). It first sat in the top HUD row, which in game mode is a
   thin strip over the FPS line, the powerup/controls column and — on a
   narrow window — the chat ticker; the owner: "The help button is
   covering up other elements in the top left, like the beginning of the
   chat". */
#liveSwitchBtn {
  position: fixed;
  left: 14px;
  bottom: 64px;
  z-index: 20;
  background: #1f7a4a;
  border-color: #37b36f;
  color: #fff;
}
#liveSwitchBtn:hover { background: #26915a; }
#liveSwitchBtn[hidden] { display: none; }
/* Phones: Leave Room sits above the joystick there (bottom 154, smaller) —
   Help goes right above it. */
@media (pointer: coarse) {
  #liveSwitchBtn { bottom: 192px; padding: 5px 10px; font-size: 0.72rem; }
}

/* Email opt-in in the screen-name picker (index.html #chooseUsernameEmailRow):
   its own card, clearly separated from the name box above it, with a
   generous tap target — the old bare line sat flush under the input. */
.emailOptInCard {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 18px 0 16px;
  padding: 12px 14px 14px 14px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  text-align: left;
}
.emailOptInCard[hidden] { display: none; }
.emailOptInCard:hover { border-color: rgba(120, 180, 255, 0.45); }
.emailOptInCard:has(input:checked) { border-color: #4f9dff; background: rgba(79, 157, 255, 0.10); }
.emailOptInCard input[type="checkbox"] { width: 20px; height: 20px; margin: 1px 0 0; flex: 0 0 auto; accent-color: #4f9dff; cursor: pointer; }
.emailOptInText { display: flex; flex-direction: column; gap: 4px; line-height: 1.35; }
.emailOptInText strong { font-size: 0.98rem; }
.emailOptInText span { font-size: 0.82rem; opacity: 0.75; }

/* "Email me when tournaments and events are on" — scoreboard slot and the
   one-time home-screen card (client.js emailPromptCardHtml). */
.emailPromptCard { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 12px 14px; border: 1px solid rgba(79, 157, 255, 0.45); border-radius: 10px; background: rgba(79, 157, 255, 0.08); text-align: left; }
.emailPromptText { flex: 1 1 240px; display: flex; flex-direction: column; gap: 3px; line-height: 1.35; }
.emailPromptText strong { font-size: 0.98rem; }
.emailPromptText span { font-size: 0.82rem; opacity: 0.75; }
.emailPromptActions { display: flex; gap: 8px; flex-wrap: wrap; }
.emailPromptDone { font-weight: 600; padding: 2px 0; }
/* In the page flow at the top of the home screen, not floating: a fixed
   card covered the hero's buttons on shorter windows. */
.emailPromptHome { position: relative; margin: 12px auto 0; width: min(760px, calc(100% - 32px)); background: #0c1628; border-radius: 12px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); z-index: 5; }
.emailPromptHome[hidden] { display: none; }
.emailPromptHome .emailPromptCard { padding-right: 36px; }
.emailPromptClose { position: absolute; top: 6px; right: 8px; background: none; border: none; color: inherit; opacity: 0.6; font-size: 1.3rem; cursor: pointer; line-height: 1; }
.emailPromptClose:hover { opacity: 1; }
.emailPromptActions { flex: 0 0 auto; }
.emailPromptActions button { width: auto; white-space: nowrap; margin: 0; }

/* In a match the bottom-left corner already holds Leave Room (bottom 20,
   ~34 tall) and, stacked on it, Help/Restart (bottom 64, ~39 tall) — open
   the comms menu above all of them instead of on top (owner, 2026-09-26,
   screenshot: the Custom list's last row sat under "Leave room"). Lobby
   screens keep the original spot; nothing else lives there. */
.frame.gameMode .vgsMenu { bottom: 112px; }

/* Upright phone in the vertical view (see .portraitCanvas above): Leave room
   goes in the top band, at the right end of the ticker line.
   Last in the file so it overrides the coarse-pointer #leaveBtn rule. */
body:has(.frame.gameMode.portraitCanvas) #leaveBtn { top: 52px; bottom: auto; left: auto; right: 8px; padding: 3px 10px; font-size: 0.72rem; }

/* Pre-game attributes (client.js renderAttributeGrid): 1943-style pip rows. */
.attrPanel { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 460px; }
.attrRoles { display: flex; gap: 8px; margin-bottom: 4px; }
.attrRoleBtn { flex: 1; padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: var(--text); font-weight: 700; cursor: pointer; }
.attrRoleBtn.selected { background: rgba(255, 211, 77, 0.16); border-color: #ffd34d; color: #ffd34d; }
.attrRoleBtn:disabled { opacity: 0.4; cursor: default; }
.attrRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.attrLabel { font-weight: 700; letter-spacing: 0.02em; min-width: 120px; }
.attrPips { display: flex; gap: 5px; }
.attrPip { width: 30px; height: 20px; border-radius: 3px; border: 2px solid rgba(255,255,255,0.35); background: transparent; cursor: pointer; padding: 0; }
.attrPip.on { background: #d9822b; border-color: #f0a24a; }
.attrPip.fixed { background: #8a6a4a; border-color: #8a6a4a; cursor: default; opacity: 0.85; } /* NES mode: pips the body type locks in */
.attrFoot { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }
.attrLeft { font-weight: 700; color: #ffd34d; }
.attrReset { padding: 5px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2); background: transparent; color: var(--text); cursor: pointer; }
@media (pointer: coarse) { .attrPip { width: 36px; height: 26px; } }

/* Rookie Cup countries (shared/hockeyNations.js): a shirt swatch per team,
   a YOU tag on your row, and the "Playing for" picker. */
.cupKit { display: inline-block; width: 12px; height: 12px; border: 2px solid; margin-right: 6px; vertical-align: -1px; image-rendering: pixelated; }
.cupYou { font-size: 0.62em; letter-spacing: 0.08em; padding: 1px 4px; border-radius: 3px; background: #ffd34d; color: #10141c; margin-left: 4px; vertical-align: 1px; }
.cupNationPick { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; font-weight: 700; }
.cupNationPick select { font: inherit; padding: 2px 6px; border-radius: 6px; background: rgba(255,255,255,0.08); color: var(--text); border: 1px solid rgba(255,255,255,0.2); }

/* NES mode draft (client.js renderBodyTypeGrid): three body-type cards. */
.bodyCards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.bodyCard { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 8px; border-radius: 12px; border: 2px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.04); color: var(--text); cursor: pointer; }
.bodyCard.selected { border-color: #ffd34d; background: rgba(255, 211, 77, 0.1); }
.bodyDot { display: block; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #9cc4ff, #2f6fd6 70%); border: 2px solid #0e2a5a; }
.bodyName { font-weight: 800; letter-spacing: 0.03em; }
.bodyBlurb { font-size: 0.7rem; color: rgba(255,255,255,0.6); text-align: center; line-height: 1.3; }

/* Admin-only test toggles in the practice/custom room modal (owner,
   2026-09-29: "The bottom needs some line breaks between the options"):
   one option per line, the nation pickers wrapping under their label. */
.adminTestOptions { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.adminTestOptions .checkRow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0; }
.adminTestOptions .checkRow input[type="checkbox"] { width: auto; margin: 0; }
