/* ── Reset & Variables ── */
/* Theme tokens. Default is dark; [data-theme="light"] overrides below.
   Hover/row tokens (--hover, --row-bg, etc.) replace the ad-hoc
   rgba(255,255,255,...) fills sprinkled across the file so they can
   actually flip in light mode. New CSS should always reach for these. */
:root, [data-theme="dark"] {
  --bg: #07091a;
  --surface: #0d1230;
  --card: #141b3d;
  --border: #2a325d;
  --accent: #38bdf8;
  --accent-hover: #0ea5e9;
  --accent2: #a78bfa;
  --accent-glow: rgba(56, 189, 248, 0.35);
  --accent2-glow: rgba(167, 139, 250, 0.3);
  --text: #e6edf3;
  --text-muted: #8b94ad;
  --danger: #ef4444;
  --chess-light: #eeeed2;
  --chess-dark: #769656;
  --chess-selected: rgba(255, 220, 70, 0.15);
  --chess-move: rgba(80, 170, 240, 0.2);
  --board-wood: #dcb483;
  --board-line: #8b5e3c;
  --shadow: 0 8px 32px rgba(0,0,0,0.4);
  --radius: 12px;
  /* Theme-aware fills used by hover states and row backgrounds. */
  --hover: rgba(255, 255, 255, 0.06);
  --hover-strong: rgba(255, 255, 255, 0.10);
  --row-bg: rgba(255, 255, 255, 0.03);
  --bg-grad-1: rgba(56, 189, 248, 0.12);
  --bg-grad-2: rgba(167, 139, 250, 0.12);
  /* Picture frame tones used by the page-level body backdrop. */
  color-scheme: dark;
}

[data-theme="light"] {
  --bg: #f3f6fc;
  --surface: #ffffff;
  --card: #ffffff;
  --border: #dde3ee;
  --accent: #0ea5e9;
  --accent-hover: #0284c7;
  --accent2: #7c3aed;
  --accent-glow: rgba(14, 165, 233, 0.32);
  --accent2-glow: rgba(124, 58, 237, 0.22);
  --text: #0f172a;
  --text-muted: #5b6678;
  --danger: #dc2626;
  --shadow: 0 8px 28px rgba(15, 23, 42, 0.10);
  --hover: rgba(15, 23, 42, 0.05);
  --hover-strong: rgba(15, 23, 42, 0.10);
  --row-bg: rgba(15, 23, 42, 0.03);
  --bg-grad-1: rgba(14, 165, 233, 0.10);
  --bg-grad-2: rgba(124, 58, 237, 0.08);
  color-scheme: light;
}

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

/* Eliminate the 300ms double-tap-zoom delay on iOS Safari for every
   interactive control. `manipulation` keeps single-tap fast while still
   allowing native scrolling/panning around the element. Anchors that
   look-and-act like buttons (.btn-secondary used in <a>, footer links)
   get the same treatment. */
button,
[role="button"],
a.btn-primary,
a.btn-secondary,
a.btn-decline,
.btn-primary,
.btn-secondary,
.btn-decline,
.btn-copy,
.btn-watch,
.btn-resign,
.lang-btn,
.lang-option { touch-action: manipulation; }

html { scroll-behavior: smooth; }

body {
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(ellipse at top left, var(--bg-grad-1), transparent 55%),
    radial-gradient(ellipse at bottom right, var(--bg-grad-2), transparent 60%),
    var(--bg);
  /* No background-attachment: fixed — it forces the browser to repaint the
     gradient on every scroll frame. Letting the gradient scroll with the
     content is virtually invisible and dramatically smoother. */
  color: var(--text);
  min-height: 100vh;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: none;
}

/* Pause always-on continuous animations while the user is actively
   scrolling (toggled by /js/scroll-perf.js). Animated box-shadows,
   text-shadows, and transforms compete with the scroll for paint time
   on the GPU; pausing them gives the browser back ~3-4ms per frame. */
body.is-scrolling .hlb-medal-disc::after,
body.is-scrolling .hero h1,
body.is-scrolling .online-dot,
body.is-scrolling .hlb-row .hlb-tier-text,
body.is-scrolling .hlb-row .hlb-metric-cell {
  animation-play-state: paused !important;
}

/* CSS containment lets the browser short-circuit paint/layout work that
   stays inside the element. Cards that hover-elevate, leaderboard rows
   that flash on hover, and the entire leaderboard table all paint to
   their own contained region instead of dirtying their ancestors. */
.game-card { contain: layout paint; }
.hlb-row { contain: layout paint; }
.home-lb-table-wrap { contain: layout paint; }

/* Sections below the fold use content-visibility: auto so the browser
   skips rendering them entirely when off-screen. The contain-intrinsic-
   size hint reserves space so the scrollbar doesn't jump as content
   loads in/out of the renderer. */
.home-lb-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 720px;
}
/* Note: content-visibility:auto WAS set on .footer for paint cost, but it
   creates a containment context that clips the language dropdown when it
   pops up above the button. Drop it — the footer is tiny anyway. */

.container { max-width: 1100px; margin: 0 auto; padding: 0 20px; }

/* ── Header ── */
.header {
  padding: 18px 0;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.header-row { display: flex; align-items: center; justify-content: space-between; }
.logo {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -0.3px;
  line-height: 1;
  filter: drop-shadow(0 0 8px var(--accent-glow));
  /* Style preserved whether .logo is rendered as <div> or <a>. The
     anchor form is the click-to-home link on every page (homepage,
     game, replay) — anchor defaults (underline + accent color)
     would clash with the branded mark, so flatten them here. */
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
.logo:hover { filter: drop-shadow(0 0 12px var(--accent-glow)) brightness(1.08); }
.logo span {
  background: linear-gradient(135deg, #e2f4ff 0%, #7dd3fc 45%, #38bdf8 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  line-height: 1;
}
.logo-icon {
  width: 1.06em;
  height: 1.06em;
  flex-shrink: 0;
  display: block;
  border-radius: 3px;
}

/* ── Language Selector ── */
.lang-selector { position: relative; }
.lang-btn {
  display: inline-flex !important;
  align-items: center; justify-content: center; gap: 6px;
  background: transparent !important;
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
  color: var(--text);
  padding: 0 8px;
  height: 36px;
  border-radius: 0 !important;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  transition: background 0.18s ease;
}
.lang-btn:hover { background: rgba(255, 255, 255, 0.06) !important; }
.lang-flag.fi {
  width: 1.4em;
  height: 1em;
  font-size: 1.05rem;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.15);
  vertical-align: middle;
}
.lang-name { font-weight: 600; white-space: nowrap; }
.lang-caret { font-size: 0.7rem; opacity: 0.7; }
.lang-menu {
  position: absolute; right: 0; top: calc(100% + 6px);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 6px;
  min-width: 180px;
  z-index: 200;
  display: flex; flex-direction: column; gap: 2px;
}
.lang-option {
  display: flex; align-items: center; gap: 10px;
  background: transparent;
  border: none;
  color: var(--text);
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  text-align: left;
  transition: background 0.15s;
}
.lang-option:hover { background: var(--surface); }
.lang-option.active { background: var(--accent); color: #fff; }
.lang-name-sm { display: none; font-weight: 600; white-space: nowrap; }

/* When the language picker lives inside the footer:
   1) show the language name next to the flag (more discoverable than a lone flag at the bottom)
   2) flip the dropdown so it opens upward instead of off-screen */
/* In-footer lang pill — shares its visual chassis with the theme
   toggle next to it (same height, same width, border, rounded-pill,
   soft fill). The two controls now read as a matching pair sitting
   on one perfectly horizontal baseline. Width is fixed (not auto)
   so the pair is symmetric regardless of which language is active —
   "Português" and "中文" must produce identical chassis widths. */
.lang-selector.in-footer .lang-btn {
  width: 140px;
  height: 32px;
  padding: 0 10px;
  border-radius: 999px !important;
  background: rgba(255,255,255,0.04) !important;
  border: 1px solid var(--border) !important;
  box-sizing: border-box;
}
.lang-selector.in-footer .lang-name-sm {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 80px;
}
@media (max-width: 600px) {
  .lang-selector.in-footer .lang-btn {
    width: 120px;
    height: 32px;
  }
  .lang-selector.in-footer .lang-name-sm { max-width: 64px; }
}
.lang-selector.in-footer .lang-btn:hover { background: rgba(255,255,255,0.10) !important; }
[data-theme="light"] .lang-selector.in-footer .lang-btn {
  background: rgba(15, 23, 42, 0.04) !important;
  border: 1px solid var(--border) !important;
}
[data-theme="light"] .lang-selector.in-footer .lang-btn:hover {
  background: rgba(15, 23, 42, 0.08) !important;
}
.lang-selector.in-footer .lang-name-sm { display: inline; }
.lang-selector.in-footer .lang-menu {
  top: auto;
  bottom: calc(100% + 6px);
  right: 0;
  min-width: 180px;
}

/* ── Hero ── */
.hero {
  padding: 20px 0 32px;
  text-align: center;
}
.hero h1 {
  font-size: clamp(1.1rem, 3.4vw, 2.2rem);
  font-weight: 800;
  line-height: 1.2;
  white-space: nowrap;
  background: linear-gradient(100deg,
    #ff2bd6 0%,
    #ff5ab0 18%,
    #c04af0 38%,
    #7a5af8 58%,
    #3b82f6 78%,
    #22d3ee 100%);
  background-size: 220% auto;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  filter: drop-shadow(0 0 10px rgba(255, 64, 192, 0.25))
          drop-shadow(0 0 18px rgba(61, 130, 246, 0.2));
  animation: heroShimmer 6s ease-in-out infinite alternate;
}
@keyframes heroShimmer {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}
.hero-sub {
  margin-top: 12px;
  color: var(--text-muted);
  font-size: 0.85rem;
  letter-spacing: 0.4px;
  text-align: left;
}
.step-arrow { color: var(--accent); margin: 0 4px; font-weight: 700; }

/* ── Game Cards ── */
.games-section { padding: 0 0 32px; }
.game-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
}
/* Apple-style Liquid Glass card — horizontal compact layout, ~80% the
   height of the original square cards so the leaderboard below has more
   vertical room. */
.game-card {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  align-items: center;
  gap: 8px 22px;
  padding: 36px 26px;
  text-align: left;
  cursor: default;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid rgba(255, 255, 255, 0.12);
  /* Scaled blur down from 30→16 — visually nearly identical at this
     translucent opacity, but ~3x cheaper to repaint during scroll. */
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  /* Collapsed from 6 shadows to 2 — the 4 inset edges were near-invisible
     against the translucent backdrop and each shadow forces an extra
     sub-pass at paint time. Outer shadow + a single inset highlight read
     near-identical and cut paint cost on this large surface roughly in half. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    0 12px 36px rgba(0, 0, 0, 0.42);
  /* Animate transform only on hover — box-shadow transitions force a paint
     each frame; transform stays on the compositor. */
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.25s;
  overflow: hidden;
  /* will-change: transform tells the browser to keep this card on its own
     compositor layer so hover-elevation doesn't trigger a layer rebuild. */
  will-change: transform;
}
.game-card::before {
  /* Diagonal light sweep — gives the glass a directional sheen */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    140deg,
    rgba(255, 255, 255, 0.18) 0%,
    rgba(255, 255, 255, 0.04) 35%,
    rgba(255, 255, 255, 0) 55%,
    rgba(255, 255, 255, 0.02) 100%
  );
  pointer-events: none;
}
.game-card::after {
  /* Per-card color halo glowing from the bottom */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    75% 60% at 50% 120%,
    var(--card-tint, rgba(56, 189, 248, 0.32)),
    transparent 70%
  );
  pointer-events: none;
  opacity: 0.5;
  transition: opacity 0.35s;
  mix-blend-mode: screen;
}
.game-card > * { position: relative; z-index: 1; }
#card-chess   { --card-tint: rgba(56, 189, 248, 0.32); }
#card-xiangqi { --card-tint: rgba(239, 85, 85, 0.32); }
#card-gomoku  { --card-tint: rgba(167, 139, 250, 0.32); }

.game-card:hover {
  transform: translateY(-6px);
  border-color: rgba(255, 255, 255, 0.22);
  /* Same shadow shape as the resting state, just deeper — keeps the
     transition smooth even though we're not animating box-shadow. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.32),
    0 22px 56px rgba(0, 0, 0, 0.48),
    0 6px 18px var(--card-tint, rgba(56, 189, 248, 0.28));
}
.game-card:hover::after { opacity: 1; }
.game-icon {
  grid-row: 1 / span 2;
  margin-bottom: 0;
  line-height: 1;
  height: 6rem;
  width: 6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.chess-icon {
  color: #f0d9b5;
  text-shadow: 0 2px 8px rgba(240,217,181,0.4);
  font-size: 6rem;
  line-height: 1;
}
.xiangqi-icon {
  color: #c94033;
  font-family: 'Noto Serif', 'Songti SC', serif;
  font-weight: 900;
  font-size: 2.6rem;
  width: 4.6rem;
  height: 4.6rem;
  border-radius: 50%;
  border: 3px solid #c94033;
  background: radial-gradient(circle at 35% 28%, #ffe8bf, #e9b775 55%, #b8813c);
  box-shadow: 0 4px 12px rgba(0,0,0,0.45), inset 0 2px 3px rgba(255,255,255,0.4), inset 0 -3px 5px rgba(0,0,0,0.2);
  text-shadow: 0 1px 1px rgba(0,0,0,0.25);
  display: flex;
  align-items: center;
  justify-content: center;
}
.gomoku-icon {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  font-size: 2.6rem;
  font-weight: 900;
  line-height: 0.9;
  width: fit-content;
  text-shadow: none;
}
.gomoku-icon span {
  width: 1.7rem;
  height: 1.7rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gomoku-icon span:nth-child(1),
.gomoku-icon span:nth-child(4) { color: #2f7fff; }
.gomoku-icon span:nth-child(2),
.gomoku-icon span:nth-child(3) { color: #e63946; font-size: 2.15rem; }

/* Title + CTA share the same column on the right of the icon. We center
   them horizontally so "Cờ Vua / Cờ Tướng / Cờ Caro" sit visually above
   the matching "Tạo phòng" button on every breakpoint. */
.game-card h2 { font-size: 1.4rem; font-weight: 700; margin: 0; align-self: end; justify-self: center; text-align: center; }
.game-card .btn-primary { padding: 9px 20px; font-size: 0.92rem; align-self: start; justify-self: center; }

/* ── Buttons ── */
.btn-primary {
  background: linear-gradient(135deg, #3c9ede 0%, #2b7fbf 100%);
  color: #f0f9ff;
  border: none;
  padding: 12px 28px;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: box-shadow 0.25s ease, transform 0.1s, filter 0.2s;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  box-shadow: 0 2px 10px rgba(56,158,222,0.25), inset 0 1px 0 rgba(255,255,255,0.1);
}
.btn-primary:hover {
  filter: brightness(1.1);
  transform: scale(1.02);
  box-shadow: 0 4px 18px rgba(56,158,222,0.4), inset 0 1px 0 rgba(255,255,255,0.15);
}
.btn-primary:active { transform: scale(0.98); filter: brightness(0.95); }
.btn-icon { font-size: 1.2rem; font-weight: 400; }

.btn-secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
  text-decoration: none;
  display: inline-block;
}
.btn-secondary:hover { background: var(--card); border-color: var(--accent); }

/* ── Join Section ── */
.join-section { padding: 0 0 80px; }
.join-box {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 28px;
  max-width: 480px;
  margin: 0 auto;
  text-align: center;
}
.join-box h2 { font-size: 1.15rem; margin-bottom: 2px; }
.join-box > p { color: var(--text-muted); font-size: 0.82rem; margin-bottom: 12px; }
.join-form { display: flex; gap: 10px; }
.join-form input {
  flex: 1;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 1rem;
  font-family: 'Courier New', monospace;
  font-weight: 700;
  letter-spacing: 3px;
  text-align: center;
  outline: none;
  transition: border-color 0.2s;
}
.join-form input:focus { border-color: var(--accent); }
.join-form .btn-secondary { padding: 8px 16px; font-size: 0.9rem; }
.error-msg { color: var(--danger); margin-top: 6px; font-size: 0.82rem; min-height: 16px; }

/* ── Footer ── */
.footer { border-top: 1px solid var(--border); padding: 18px 0; }
.footer p { color: var(--text-muted); font-size: 0.9rem; margin: 0; }
/* Mobile + tablet: collapse the copyright line to a single line and drop
   the "All rights reserved" tail so the bare copyright fits nicely. */
@media (max-width: 1024px) {
  .footer-copy { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.78rem; }
  .footer-rights { display: none; }
}
.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.footer-copy { flex: 1 1 auto; text-align: left; }
.footer-lang { flex: 0 0 auto; }
.footer-controls {
  /* Grid with two fixed-width cells: the theme toggle is intentionally
     compact (~1/3 of the lang pill) so it reads as a tight icon-switch
     next to the longer language pill. The grid still guarantees the
     pair sits on a single horizontal baseline with no shrink/grow. */
  display: grid;
  grid-template-columns: 48px 140px;
  align-items: center;
  justify-items: stretch;
  gap: 10px;
  flex: 0 0 auto;
  line-height: 0;
}
.footer-controls > * {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 32px;
}
@media (max-width: 600px) {
  .footer-row { justify-content: center; gap: 10px; }
  .footer-copy { text-align: center; flex-basis: 100%; }
  .footer-lang { display: flex; justify-content: center; }
  .footer-controls {
    grid-template-columns: 44px 120px;
    justify-content: center;
    gap: 8px;
  }
}

/* Country flag rendered inline before a player nickname. Used by the
   home leaderboard player cell, friend rows, friend search rows,
   notification cards, and the in-game player panels so the flag→name
   reading order is consistent. No shadow / outline — the flag-icons
   artwork already has a clean edge and a stroke ring read as a grey
   halo on light backgrounds. */
.hlb-flag-inline,
.row-flag-inline,
.notif-flag,
.player-flag {
  display: inline-block;
  width: 1.4em;
  height: 1em;
  vertical-align: -0.18em;
  margin-right: 6px;
  border-radius: 2px;
}
.notif-flag { width: 1.2em; vertical-align: -0.15em; margin-right: 5px; }
/* vertical-align: middle (not a hardcoded -em offset) so the flag
   visually sits at the text mid-height regardless of font size or
   line-height — the same player panel ships across chess / xiangqi /
   gomoku and the previous -0.16em nudge looked off on at least one
   of them (flag floated above the baseline of the nickname). */
.player-flag { width: 1.25em; vertical-align: middle; margin-right: 5px; }

/* Click-to-enlarge avatar modal in the game panel. Backdrop dims the
   board, the image scales up to ~70vmin so it's readable on every
   viewport. Click anywhere or press Escape to dismiss. */
.avatar-zoom-overlay {
  position: fixed;
  inset: 0;
  background: rgba(8, 11, 22, 0.78);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
  opacity: 0;
  transition: opacity 0.18s ease;
  cursor: zoom-out;
}
.avatar-zoom-overlay.open { opacity: 1; }
.avatar-zoom-img {
  width: min(70vmin, 480px);
  height: min(70vmin, 480px);
  object-fit: cover;
  border-radius: 50%;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5), 0 0 0 4px rgba(255,255,255,0.08);
  transform: scale(0.92);
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.avatar-zoom-overlay.open .avatar-zoom-img { transform: scale(1); }
[data-theme="light"] .avatar-zoom-overlay { background: rgba(15, 23, 42, 0.55); }

/* Floating language pill for the game page (no real footer there).
   Standalone case (no .footer-floating wrapper) keeps its own anchor;
   when wrapped inside .footer-floating with the theme toggle, it just
   sits inline and the wrapper handles positioning. */
.footer-lang-floating {
  position: fixed;
  bottom: max(10px, calc(env(safe-area-inset-bottom, 0px) + 4px));
  right: max(12px, calc(env(safe-area-inset-right, 0px) + 6px));
  z-index: 90;
}
.footer-floating .footer-lang-floating {
  position: static;
}
@media (max-width: 600px) {
  .footer-lang-floating {
    bottom: max(8px, calc(env(safe-area-inset-bottom, 0px) + 4px));
    right: max(8px, calc(env(safe-area-inset-right, 0px) + 4px));
  }
}

/* ── Centered confirm dialog (replaces window.confirm) ──────────────── */
.cl-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(8, 11, 22, 0.66);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  padding: 16px;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.cl-confirm-overlay.open { opacity: 1; }
.cl-confirm-box {
  width: min(380px, 100%);
  background: var(--card, #161b2c);
  color: var(--text, #e6e8ee);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 16px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04) inset;
  padding: 24px 22px 18px;
  text-align: center;
  transform: translateY(8px) scale(0.96);
  opacity: 0;
  transition: transform 0.22s cubic-bezier(.2,.9,.3,1.2), opacity 0.18s ease;
  position: relative;
}
/* Small X button in the top-right of a confirm modal — used by the
   incoming-challenge popup so users can dismiss the popup WITHOUT
   accepting or declining (the invite stays in the bell). */
.cl-confirm-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  border: none;
  color: var(--text-muted, #8b94a8);
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
  padding: 0;
}
.cl-confirm-close:hover { background: rgba(255, 255, 255, 0.12); color: var(--text, #e6e8ee); }
[data-theme="light"] .cl-confirm-close { background: rgba(15, 23, 42, 0.06); }
[data-theme="light"] .cl-confirm-close:hover { background: rgba(15, 23, 42, 0.12); }
.cl-confirm-overlay.open .cl-confirm-box {
  transform: translateY(0) scale(1);
  opacity: 1;
}
.cl-confirm-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto 12px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  background: rgba(99, 102, 241, 0.14);
  color: #a5b4fc;
}
.cl-confirm-icon.is-danger {
  background: rgba(239, 68, 68, 0.16);
  color: #fca5a5;
}
.cl-confirm-title {
  margin: 0 0 6px;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.cl-confirm-subject {
  margin: 4px 0 8px;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text, #e6e8ee);
  word-break: break-word;
}
.cl-confirm-msg {
  margin: 0 0 18px;
  color: var(--text-muted, #8b94a8);
  font-size: 0.92rem;
  line-height: 1.5;
}
.cl-confirm-actions {
  display: flex;
  gap: 10px;
  justify-content: stretch;
}
.cl-confirm-roomchip-row {
  display: flex;
  justify-content: center;
  margin: 0 0 12px;
}
.cl-confirm-actions button {
  flex: 1 1 0;
  padding: 11px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text, #e6e8ee);
  font-family: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.05s ease, border-color 0.15s ease;
}
.cl-confirm-actions button:hover { background: rgba(255, 255, 255, 0.10); }
.cl-confirm-actions button:active { transform: scale(0.98); }
.cl-confirm-actions .cl-confirm-ok {
  background: var(--accent, #4f46e5);
  border-color: transparent;
  color: #fff;
}
.cl-confirm-actions .cl-confirm-ok:hover {
  background: color-mix(in srgb, var(--accent, #4f46e5) 86%, white 14%);
}
.cl-confirm-actions .cl-confirm-ok.is-danger {
  background: linear-gradient(180deg, #ef4444, #dc2626);
  border-color: rgba(0, 0, 0, 0.2);
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.28);
}
.cl-confirm-actions .cl-confirm-ok.is-danger:hover {
  background: linear-gradient(180deg, #f55, #e23);
}
@media (max-width: 480px) {
  .cl-confirm-box { padding: 20px 16px 14px; border-radius: 14px; }
  .cl-confirm-icon { width: 48px; height: 48px; font-size: 1.3rem; }
  .cl-confirm-title { font-size: 1.05rem; }
  .cl-confirm-actions button { padding: 10px 12px; font-size: 0.88rem; }
}

/* Avatar shown at the top of the "challenge accepted" popup — circular
   image instead of the generic ⚔ icon, sized roughly like the icon
   would have been. */
.cl-challenge-avatar {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  object-fit: cover;
  margin: 0 auto 12px;
  display: block;
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.30);
}

/* ── Challenge dialog ───────────────────────────────────────────────────
   Centered modal (reuses .cl-confirm-overlay/.cl-confirm-box scaffolding)
   with a 3-card grid for picking the game type. Each card flips through
   normal -> sending -> sent / error states inline so the user sees the
   outcome without leaving the dialog. */
.cl-challenge-box { width: min(440px, 100%); }
.cl-challenge-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 14px 0 16px;
}
.cl-challenge-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 16px 8px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--row-bg);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
.cl-challenge-card:hover {
  background: var(--hover);
  border-color: rgba(167, 139, 250, 0.45);
  transform: translateY(-1px);
}
.cl-challenge-card:active { transform: scale(0.98); }
.cl-challenge-card[disabled] { cursor: default; opacity: 0.55; }
.cl-challenge-card[disabled]:hover { transform: none; background: var(--row-bg); border-color: var(--border); }
.cl-challenge-icon {
  font-size: 1.8rem;
  line-height: 1;
}
.cl-challenge-label {
  font-size: 0.86rem;
  font-weight: 600;
  text-align: center;
}
.cl-challenge-card.is-sending { opacity: 0.7; }
.cl-challenge-card.is-sent {
  background: rgba(34, 197, 94, 0.14);
  border-color: rgba(34, 197, 94, 0.45);
  color: #86efac;
  opacity: 1;
}
.cl-challenge-card.is-error {
  background: rgba(239, 68, 68, 0.14);
  border-color: rgba(239, 68, 68, 0.45);
  color: #fca5a5;
  opacity: 1;
}
@media (max-width: 480px) {
  .cl-challenge-grid { gap: 8px; }
  .cl-challenge-card { padding: 12px 6px; gap: 6px; }
  .cl-challenge-icon { font-size: 1.5rem; }
  .cl-challenge-label { font-size: 0.78rem; }
}

/* ── Loading Overlay ── */
.loading-overlay {
  position: fixed; inset: 0;
  background: rgba(13,17,23,0.85);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 16px; z-index: 1000;
}
.loading-overlay p { color: var(--text-muted); }
.spinner {
  width: 40px; height: 40px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.hidden { display: none !important; }

/* ══════════════════════════════════════
   GAME PAGE
══════════════════════════════════════ */
/* viewport-fit=cover on game.html lets content extend behind the iPhone
   notch / home indicator. Inset the body so the bottom player-panel
   isn't hidden by the home indicator and the header isn't clipped by
   the side notches in landscape. box-sizing:border-box (from the *
   reset above) means the padding is subtracted from the 100svh height,
   so the inner content area stays fully visible. Top inset is handled
   inside the header padding-top below so the surface color extends
   behind the notch instead of leaving a gradient band. */
.game-body {
  display: flex;
  flex-direction: column;
  height: 100vh; height: 100svh;
  overflow: hidden;
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.game-header {
  display: flex; align-items: center; justify-content: space-between;
  /* padding-top picks up the iPhone status-bar inset (non-zero only when
     viewport-fit=cover puts the page behind the notch). Keeping the
     inset inside the header — not on .game-body — means the header's
     --surface background extends up to the very top edge of the screen
     so there's no visible gradient band behind the notch. */
  padding: calc(10px + env(safe-area-inset-top, 0px)) 20px 10px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.game-header-left { display: flex; align-items: center; gap: 12px; }
.logo-small {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 1.1rem; font-weight: 700;
  color: var(--text); text-decoration: none;
  line-height: 1;
}
.logo-icon-sm {
  width: 0.94em;
  height: 0.94em;
  flex-shrink: 0;
  display: block;
  border-radius: 3px;
}
.game-title-badge {
  background: var(--card);
  border: 1px solid var(--border);
  padding: 3px 12px;
  border-radius: 20px;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.game-header-right { display: flex; align-items: center; gap: 10px; }
.room-label { color: var(--text-muted); font-size: 0.85rem; }
.room-code {
  font-family: 'Courier New', monospace;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 3px;
}
.btn-copy {
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 6px 14px;
  border-radius: 6px;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.2s;
}
.btn-copy:hover { background: var(--border); }

/* ── Online badge (header) ── */
/* Color tuned to match the "Tạo phòng" primary button (#3c9ede / accent
   cyan) so the header reads as one cohesive palette. */
.header-online {
  display: inline-flex !important;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 0 8px !important;
  height: 36px;
  min-width: 0 !important;
  background: transparent !important;
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
  border-radius: 0 !important;
  color: #7dc6f0;
  font-size: 0.84rem;
  font-weight: 600;
  white-space: nowrap;
  user-select: none;
}
.header-online strong {
  color: #e0f2fe;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.online-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #3c9ede;
  box-shadow: 0 0 0 0 rgba(60, 158, 222, 0.55);
  animation: online-pulse 1.6s ease-out infinite;
  flex-shrink: 0;
}
@keyframes online-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(60, 158, 222, 0.55); }
  70%  { box-shadow: 0 0 0 8px rgba(60, 158, 222, 0); }
  100% { box-shadow: 0 0 0 0 rgba(60, 158, 222, 0); }
}
.header-online-sm { padding: 4px 10px 4px 8px; gap: 5px; font-size: 0.78rem; }
.header-online-sm .online-dot { width: 7px; height: 7px; }
@media (max-width: 600px) {
  /* Strip the cyan pill chrome on phones — keep just the pulsing dot + the
     count, so the header has more room for the language picker + auth chip
     without feeling crowded. */
  .header-online {
    padding: 0;
    min-width: 0;
    height: auto;
    background: transparent;
    border: none;
    font-size: 0.82rem;
    gap: 6px;
  }
  .online-label { display: none; }
}

/* ── Header link (leaderboard nav) ── */
.header-link {
  color: var(--text);
  text-decoration: none;
  font-size: 0.88rem;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.10);
  background: rgba(20, 27, 61, 0.45);
  transition: all 0.18s ease;
  white-space: nowrap;
}
.header-link:hover {
  border-color: rgba(56, 189, 248, 0.4);
  background: rgba(56, 189, 248, 0.10);
  color: var(--text);
}
.header-link-sm { padding: 6px 10px; font-size: 0.95rem; }
@media (max-width: 600px) {
  .header-link { padding: 5px 10px; font-size: 0.78rem; }
}

/* Sound toggle: small round outline button matching the bell + theme
   switch. Strokes use currentColor — monochrome in both themes, no
   accent fill in either state. The mute state is signalled by the
   crossed-out icon, not a colour change. */
.sound-toggle {
  width: 24px;
  height: 24px;
  padding: 0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.sound-toggle:hover { background: var(--hover); color: var(--text); }
.sound-toggle svg { display: block; width: 14px; height: 14px; }
@media (max-width: 600px) {
  .sound-toggle { width: 22px; height: 22px; }
  .sound-toggle svg { width: 12px; height: 12px; }
}

/* ── Google Sign-In ── */
.header-tools { display: flex; align-items: center; gap: 12px; }
.auth-mount { display: inline-flex; align-items: center; min-height: 36px; }
.auth-mount:empty { display: none; }

.btn-google-signin {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 18px 0 6px;
  height: 36px;
  min-width: 130px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.01)),
    rgba(20, 27, 61, 0.55);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
  color: var(--text);
  font-family: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  user-select: none;
  transition: transform 0.18s ease, box-shadow 0.22s ease, border-color 0.22s ease, background 0.22s ease;
  box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 4px 14px rgba(0,0,0,0.25);
}
.btn-google-signin:hover:not(:disabled) {
  border-color: rgba(56, 189, 248, 0.55);
  background:
    linear-gradient(180deg, rgba(56,189,248,0.10), rgba(167,139,250,0.06)),
    rgba(20, 27, 61, 0.7);
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 6px 20px rgba(56,189,248,0.22);
  transform: translateY(-1px);
}
.btn-google-signin:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 2px 8px rgba(0,0,0,0.3);
}
.btn-google-signin:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow), 0 4px 14px rgba(0,0,0,0.25);
}
.btn-google-signin:disabled {
  opacity: 0.55;
  cursor: progress;
  transform: none;
}
.btn-google-signin .g-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: #fff;
  flex-shrink: 0;
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.btn-google-signin .g-logo svg { width: 13px; height: 13px; display: block; }
.btn-google-signin .g-text { line-height: 1; }
.btn-google-signin.is-loading .g-logo svg { animation: g-spin 0.9s linear infinite; }
@keyframes g-spin { to { transform: rotate(360deg); } }

.auth-user {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 3px 12px 3px 3px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: rgba(20, 27, 61, 0.55);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  max-width: 260px;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease;
}
.auth-user:hover {
  border-color: rgba(56, 189, 248, 0.45);
  background: rgba(20, 27, 61, 0.75);
}
.auth-user:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.auth-user-caret {
  font-size: 0.7rem;
  color: var(--text-muted);
  margin-left: 2px;
}
.auth-user img {
  width: 28px; height: 28px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--border);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
  display: block;
}

.auth-user-name {
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--text);
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.2;
}
@media (max-width: 600px) {
  /* On phones, collapse Google sign-in / signed-in chip to a tappable
     circle. The full label has no room next to the online badge + lang
     dropdown, and a circle is large enough to hit comfortably (>=40px). */
  .btn-google-signin { padding: 0; gap: 0; min-width: 0; width: 40px; height: 40px; }
  .btn-google-signin .g-text { display: none; }
  .btn-google-signin .g-logo { width: 22px; height: 22px; }
  .btn-google-signin .g-logo svg { width: 12px; height: 12px; }
  .auth-user { padding: 0; gap: 0; max-width: none; width: 40px; height: 40px; justify-content: center; }
  .auth-user img { width: 32px; height: 32px; }
  .auth-user-name { display: none; }
  .auth-user-caret { display: none; }
}

/* ── Profile modal ── */
.profile-overlay {
  position: fixed;
  inset: 0;
  background: rgba(7, 9, 26, 0.72);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  /* Sits above the room-creation loading spinner (z-index: 1000) so the
     profile modal stays on top if a user accidentally triggers Tạo phòng
     while the modal is open. */
  z-index: 1100;
  display: flex;
  justify-content: center;
  padding: 5vh 16px;
  overflow-y: auto;
  animation: profile-fade 0.18s ease both;
}
@keyframes profile-fade { from { opacity: 0; } to { opacity: 1; } }
body.profile-open { overflow: hidden; }

.profile-modal {
  position: relative;
  width: min(545px, 100%);
  /* margin: auto 0 lets the modal center vertically when there's spare room,
     and gracefully falls back to flex-start when content overflows the
     viewport — avoiding the classic flex-center "top gets cut off" bug. */
  margin: auto 0;
  background: rgba(36, 37, 38, 0.98);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.02) inset;
  color: var(--text);
  animation: profile-pop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes profile-pop {
  from { transform: translateY(12px) scale(0.97); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}
.profile-close {
  position: absolute;
  top: 0; right: 0;
  width: 44px; height: 44px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 50%;
  color: var(--text-muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  transition: all 0.18s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}
.profile-close:hover { color: var(--text); background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); }
.profile-close:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }

.profile-body {
  padding: 14px 14px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Facebook-style profile dropdown:
   - User card on top (avatar + name)
   - Cards with rounded corners separated by gaps
   - Rows with leading icon, label, trailing chevron */
.fbp-user-card {
  display: flex; align-items: center; gap: 14px;
  background: rgba(58, 59, 60, 0.7);
  border-radius: 12px;
  padding: 16px 18px;
}
/* Rank badge anchored at the right edge of the user card — pushes itself
   to the right by stretching the .fbp-id flex item. */
.fbp-user-card > .rank-badge {
  margin-left: auto;
  flex-shrink: 0;
}
@media (max-width: 480px) {
  .fbp-user-card > .rank-badge .rank-text { display: none; }
  .fbp-user-card > .rank-badge.rank-big .rank-hex { width: 44px; height: 50px; }
}
.fbp-avatar {
  width: 56px; height: 56px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(255,255,255,0.08);
  background: var(--border);
  flex-shrink: 0;
  display: block;
}

/* Wraps the avatar so clicking it opens a file picker. The dim camera
   overlay surfaces on hover/focus so users discover they can change it. */
.fbp-avatar-btn {
  position: relative;
  width: 56px; height: 56px;
  padding: 0;
  border: none;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
  flex-shrink: 0;
  outline: none;
  transition: transform 0.15s ease;
}
.fbp-avatar-btn:hover { transform: scale(1.04); }
.fbp-avatar-btn:focus-visible {
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.45);
}
.fbp-avatar-overlay {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: rgba(7, 9, 26, 0.62);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.18s ease;
  pointer-events: none;
}
.fbp-avatar-btn:hover .fbp-avatar-overlay,
.fbp-avatar-btn:focus-visible .fbp-avatar-overlay {
  opacity: 1;
}
.fbp-avatar-btn.is-uploading .fbp-avatar-overlay {
  opacity: 1;
  background: rgba(7, 9, 26, 0.78);
}
.fbp-avatar-btn.is-uploading .fbp-avatar-overlay svg { display: none; }
.fbp-avatar-btn.is-uploading .fbp-avatar-overlay::after {
  content: '';
  width: 18px; height: 18px;
  border: 2px solid #fff;
  border-top-color: transparent;
  border-radius: 50%;
  animation: saveBtnSpin 0.7s linear infinite;
}

/* Friend CTA inside the public-profile user-card. Mounted as a flex row so
   the secondary button (Cancel / Decline) sits next to the primary one. */
.fbp-friend-cta {
  display: flex; gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.fbp-friend-primary,
.fbp-friend-secondary {
  border: none;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.84rem;
  font-weight: 600;
  font-family: inherit;
  transition: background 0.18s, opacity 0.18s, transform 0.12s;
}
.fbp-friend-primary:active,
.fbp-friend-secondary:active { transform: scale(0.97); }
.fbp-friend-primary:disabled,
.fbp-friend-secondary:disabled { opacity: 0.55; cursor: default; }
.fbp-friend-primary.fbp-friend-none {
  background: linear-gradient(135deg, #38bdf8, #0ea5e9);
  color: #07091a;
}
.fbp-friend-primary.fbp-friend-none:hover { filter: brightness(1.08); }
.fbp-friend-primary.fbp-friend-pending_outgoing {
  background: rgba(251, 191, 36, 0.18);
  color: #fbbf24;
  border: 1px solid rgba(251, 191, 36, 0.30);
  cursor: default;
}
.fbp-friend-primary.fbp-friend-pending_incoming {
  background: rgba(110, 231, 183, 0.18);
  color: #6ee7b7;
  border: 1px solid rgba(110, 231, 183, 0.35);
}
.fbp-friend-primary.fbp-friend-pending_incoming:hover { background: rgba(110, 231, 183, 0.28); }
.fbp-friend-primary.fbp-friend-accepted {
  background: rgba(110, 231, 183, 0.10);
  color: #6ee7b7;
  border: 1px solid rgba(110, 231, 183, 0.25);
  cursor: default;
}
/* Even when disabled the "Bạn bè" pill should read as active state, not
   greyed out — it's an info badge, not a "blocked button". */
.fbp-friend-primary.fbp-friend-accepted:disabled { opacity: 1; }
.fbp-friend-primary.fbp-friend-accepted::before {
  content: '✓ ';
  font-weight: 900;
}
.fbp-friend-primary.fbp-friend-blocked {
  background: rgba(252, 165, 165, 0.10);
  color: #fca5a5;
  border: 1px solid rgba(252, 165, 165, 0.25);
}
.fbp-friend-secondary {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-muted);
  border: 1px solid rgba(255, 255, 255, 0.10);
}
.fbp-friend-secondary:hover {
  background: rgba(252, 165, 165, 0.12);
  color: #fca5a5;
  border-color: rgba(252, 165, 165, 0.25);
}
.fbp-friend-cta-hint {
  margin-top: 6px;
  font-size: 0.78rem;
  color: var(--text-muted);
  font-style: italic;
}
.fbp-public-history-title {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 10px 14px 6px;
}
.fbp-id { min-width: 0; flex: 1; }
.fbp-name {
  font-size: 1.1rem;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fbp-bio {
  /* See .hlb-bio for the oblique-vs-italic rationale — same fix applied
     here so Vietnamese diacritics survive on the profile user-card.
     Tier-themed colour matches Elo + Win Rate so the user card reads
     as one accent band. Smaller size keeps the player's name dominant. */
  font-style: oblique 9deg;
  font-size: 0.74rem;
  font-weight: 500;
  color: var(--row-tier-color, rgba(229, 231, 235, 0.85));
  opacity: 0.92;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  margin-top: 3px;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.fbp-bio:empty { display: none; }
.fbp-sub {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px; margin-top: 4px;
  font-size: 0.78rem;
  color: var(--text-muted);
}
.fbp-country { display: inline-flex; align-items: center; gap: 5px; }
.fbp-country .fi { width: 1.1em; box-shadow: 0 0 0 1px rgba(0,0,0,0.2); border-radius: 2px; }
.fbp-private-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.14);
  color: #cbd5e1;
  font-size: 0.7rem;
  font-weight: 600;
}

.fbp-card {
  background: rgba(58, 59, 60, 0.55);
  border-radius: 10px;
  overflow: hidden;
}
.fbp-stats-card {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0;
  padding: 14px 10px;
}
.fbp-stat {
  display: flex; flex-direction: column; align-items: center;
  gap: 4px;
  border-right: 1px solid rgba(255,255,255,0.05);
  padding: 4px 6px;
}
.fbp-stat:last-child { border-right: none; }
.fbp-stat-num {
  font-size: 1.05rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.fbp-stat-label {
  font-size: 0.65rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.fbp-stat-w .fbp-stat-num { color: #34d399; }
.fbp-stat-l .fbp-stat-num { color: #fb7185; }
.fbp-stat-d .fbp-stat-num { color: #a78bfa; }
.fbp-stat-r .fbp-stat-num { color: var(--accent); }

.fbp-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--text);
  padding: 12px 14px;
  font-family: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
  transition: background 0.15s ease;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.fbp-card .fbp-row:last-of-type { border-bottom: none; }
.fbp-row:hover { background: rgba(255,255,255,0.05); }
.fbp-row:focus-visible { outline: none; background: rgba(56, 189, 248, 0.08); }
.fbp-row-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(255,255,255,0.06);
  flex-shrink: 0;
}
.fbp-row-icon svg { width: 18px; height: 18px; }
.fbp-row-icon.icon-trophy  { color: #fbbf24; background: rgba(251, 191, 36, 0.10); }
.fbp-row-icon.icon-edit    { color: #38bdf8; background: rgba(56, 189, 248, 0.10); }
.fbp-row-icon.icon-gender  { color: #a78bfa; background: rgba(167, 139, 250, 0.10); }
.fbp-row-icon.icon-history { color: #6ee7b7; background: rgba(110, 231, 183, 0.10); }
.fbp-row-icon.icon-out     { color: #fca5a5; background: rgba(252, 165, 165, 0.12); }
.fbp-row-icon.icon-bio     { color: #c4b5fd; background: rgba(196, 181, 253, 0.10); }
.fbp-row-icon.icon-cake    { color: #f472b6; background: rgba(244, 114, 182, 0.10); }
.fbp-row-icon.icon-camera  { color: #60a5fa; background: rgba(96, 165, 250, 0.10); }
.fbp-row-icon.icon-lock    { color: #94a3b8; background: rgba(148, 163, 184, 0.12); }
.fbp-row-icon.icon-friends { color: #34d399; background: rgba(52, 211, 153, 0.10); }
.fbp-row-icon.icon-medal   { color: #fbbf24; background: rgba(251, 191, 36, 0.10); }
.fbp-row-label { flex: 1; min-width: 0; }
.fbp-row-chev {
  font-size: 1.4rem;
  line-height: 1;
  color: var(--text-muted);
  transition: transform 0.18s ease;
}
.fbp-row.is-open .fbp-row-chev { transform: rotate(90deg); }

.fbp-row-body {
  /* Symmetric vertical padding so the body content sits visually centered
     between the section header above and the divider below. Left-aligns
     to the card edge (no longer indented past the icon column) so inputs
     and option pills can use the full row width. */
  padding: 14px 16px;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.fbp-card .fbp-row-body:last-child { border-bottom: none; }
.fbp-row-body[hidden] { display: none; }
.fbp-row-body .profile-section { margin: 0; }
/* Section nested inside an .fbp-card: drop its outer margin (the body
   gap handles vertical spacing). Title gets a small left/top inset so
   it doesn't hug the corner; rows keep their own padding so they
   continue stretching edge-to-edge inside the card. */
/* Inside an .fbp-card the section gets symmetric horizontal padding so
   the title AND the ranking rows are inset evenly from the card edges
   instead of the rows hugging the corners while the title floated
   indented. The body gap handles vertical spacing between cards. */
.fbp-no-section {
  padding: 12px 14px 14px !important;
  margin-bottom: 0 !important;
}
.fbp-no-section > .profile-section-title {
  margin: 0 0 10px;
  padding: 0;
}
/* When nested in a .fbp-row-body (own profile accordion), the body
   already provides horizontal padding — drop the section's own so we
   don't double-inset the ranking rows asymmetrically. */
.fbp-row-body > .fbp-no-section {
  padding: 0 !important;
}

.fbp-signout-row { color: #fca5a5; }
.fbp-signout-row:hover { background: rgba(239, 68, 68, 0.10); color: #fecaca; }

/* Mobile profile: keep the same visual structure as desktop — the goal is
   parity, not a separate "compact" layout. Only ease padding/font enough
   to fit a 360–420px viewport without crowding. */
@media (max-width: 600px) {
  .profile-overlay { padding: 3vh 10px; }
  .profile-modal { width: 100%; }
  .profile-body { padding: 12px 12px 16px; gap: 10px; }
  .fbp-user-card { padding: 14px 16px; gap: 12px; }
  .fbp-name { font-size: 1rem; }
  .fbp-bio { font-size: 0.7rem; }
  .fbp-stats-card { padding: 12px 6px; }
  .fbp-stat { padding: 4px 4px; }
  .fbp-stat-num { font-size: 0.98rem; }
  .fbp-stat-label { font-size: 0.6rem; }
  .fbp-row { padding: 12px 14px; }
  .fbp-row-icon { width: 34px; height: 34px; }
  .fbp-row-icon svg { width: 17px; height: 17px; }
  .fbp-row-label { font-size: 0.92rem; }
  .fbp-row-body { padding: 12px 14px; }
}
.profile-loading, .profile-error, .profile-empty {
  text-align: center;
  color: var(--text-muted);
  padding: 24px 0;
}
.profile-error { color: #fca5a5; }

.profile-head {
  display: flex;
  gap: 16px;
  align-items: center;
  margin-bottom: 24px;
}
.profile-avatar {
  width: 72px; height: 72px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(255,255,255,0.1);
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  background: var(--border);
}
.profile-head-meta { min-width: 0; flex: 1; }
.profile-name {
  font-size: 1.4rem;
  font-weight: 700;
  margin: 0 0 6px;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.profile-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.profile-country { display: inline-flex; align-items: center; gap: 6px; }
.profile-country .fi { width: 1.2em; box-shadow: 0 0 0 1px rgba(0,0,0,0.2); border-radius: 2px; }
.profile-email {
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-section { margin-bottom: 22px; }
.profile-section-title {
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0 0 12px;
  color: var(--text);
  letter-spacing: 0.01em;
}
.profile-label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 8px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* === Profile accordion bodies — shared rhythm =============================
   Every section (nickname / gender / bio / privacy) lives in a .fbp-row-body
   container. The rules below pin a single height/padding/border vocabulary
   so the controls line up regardless of which section is open. */
.fbp-row-body > * + * { margin-top: 10px; }

.profile-nick-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.profile-nick-input,
.profile-bio-input,
.friend-search-input {
  flex: 1;
  width: 100%;
  min-width: 0;
  height: 40px;
  padding: 0 14px;
  background: rgba(7, 9, 26, 0.6);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 10px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
  box-sizing: border-box;
}
.profile-bio-input {
  display: block;
  height: auto;
  min-height: 96px;
  padding: 12px 14px;
  resize: vertical;
  line-height: 1.5;
}
.profile-nick-input::placeholder,
.profile-bio-input::placeholder,
.friend-search-input::placeholder { color: var(--text-muted); }
.profile-nick-input:focus,
.profile-bio-input:focus,
.friend-search-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
  background: rgba(7, 9, 26, 0.75);
}
.profile-nick-save {
  flex-shrink: 0;
  min-width: 84px;
  height: 40px;
  padding: 0 18px;
  background: var(--accent);
  border: none;
  border-radius: 10px;
  color: #06121f;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.18s ease, transform 0.12s ease, box-shadow 0.18s ease;
  box-sizing: border-box;
}
.profile-nick-save:hover:not(:disabled) { background: var(--accent-hover); }
.profile-nick-save:active:not(:disabled) { transform: scale(0.97); }
.profile-nick-save:disabled { opacity: 0.5; cursor: progress; }
.profile-nick-hint {
  font-size: 0.78rem;
  color: var(--text-muted);
  line-height: 1.45;
}
/* Foot row under the nickname input: hint on the left, char counter
   on the right. Mirrors the bio section's layout for consistency. */
.profile-nick-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.profile-nick-foot .profile-nick-hint { flex: 1; min-width: 0; }
.profile-nick-count {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  flex-shrink: 0;
}
/* The .ok / .err pill animation is layered on top of this in the
   "Save feedback pills" block further down; the base sizing lives here so
   the empty (placeholder) feedback element doesn't push other elements. */
.profile-nick-feedback,
.profile-gender-feedback,
.profile-bio-feedback,
.profile-avatar-feedback,
.profile-privacy-feedback {
  font-size: 0.82rem;
  min-height: 1em;
  margin-top: 0;
}

/* Gender picks — three equal-width pills aligned with the rest of the
   inputs. Drops to a 2-up layout on very narrow screens. */
.profile-gender-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.gender-pick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 40px;
  padding: 0 12px;
  background: rgba(7, 9, 26, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 10px;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
  box-sizing: border-box;
}
.gender-pick svg { flex-shrink: 0; }
.gender-pick:hover { color: var(--text); border-color: rgba(255,255,255,0.2); }
.gender-pick.active.g-male   { color: #93c5fd; border-color: #60a5fa; background: rgba(96, 165, 250, 0.10); }
.gender-pick.active.g-female { color: #f9a8d4; border-color: #f472b6; background: rgba(244, 114, 182, 0.10); }
.gender-pick.active.g-other  { color: #c4b5fd; border-color: #a78bfa; background: rgba(167, 139, 250, 0.10); }
.gender-pick:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
@media (max-width: 480px) {
  .profile-gender-row { grid-template-columns: repeat(3, 1fr); }
  .gender-pick { font-size: 0.8rem; padding: 0 8px; }
}

.profile-stats {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin-bottom: 24px;
}
.stat-card {
  background: rgba(7, 9, 26, 0.55);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 12px;
  padding: 14px 8px;
  text-align: center;
  transition: border-color 0.18s ease;
}
.stat-card:hover { border-color: rgba(255,255,255,0.14); }
.stat-num {
  font-size: 1.45rem;
  font-weight: 700;
  line-height: 1;
  margin-bottom: 6px;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: 0.72rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.stat-card.stat-win  .stat-num { color: #34d399; }
.stat-card.stat-loss .stat-num { color: #fb7185; }
.stat-card.stat-draw .stat-num { color: #a78bfa; }
.stat-card.stat-rate .stat-num { color: var(--accent); }

/* ── Home leaderboard (single unified table) ── */
.home-lb-section { padding: 30px 0 60px; }

.home-lb-title {
  text-align: center;
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0 0 18px;
  letter-spacing: 0.02em;
  /* Toned-down antique gold — warm, less neon than the previous tone. */
  background: linear-gradient(135deg, #e6c478, #c89045 60%, #8a5a2b);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.home-lb-filters {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}
.home-lb-segmented {
  display: inline-flex;
  background: rgba(7, 9, 26, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  padding: 3px;
  gap: 3px;
}
.lb-seg {
  flex: 0 0 auto;
  padding: 7px 16px;
  background: transparent;
  border: none;
  border-radius: 999px;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.84rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: color 0.18s ease, background 0.18s ease, transform 0.12s ease;
  white-space: nowrap;
}
.lb-seg:hover { color: var(--text); }
.lb-seg.active {
  background: linear-gradient(135deg, #c89045, #e6c478);
  color: #1a1106;
  box-shadow: 0 3px 10px rgba(200, 144, 69, 0.28);
}
.lb-seg.active:active { transform: scale(0.98); }
.lb-seg:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(200, 144, 69, 0.35); }

.home-lb-table-wrap {
  /* Desktop: everything fits, no scroll. Mobile: media queries below flip
     to overflow-x: auto + min-width on the table so all columns stay
     visible and the user can swipe horizontally to see them. */
  overflow-x: hidden;
  background:
    linear-gradient(180deg, rgba(200, 144, 69, 0.04), transparent 30%),
    rgba(13, 18, 48, 0.65);
  border: 1px solid rgba(200, 144, 69, 0.15);
  border-radius: 14px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  transition: opacity 0.18s ease;
}
.home-lb-section.is-loading .home-lb-table-wrap { opacity: 0.55; }

.home-lb-table {
  width: 100%;
  border-collapse: collapse;
  /* No min-width — the table shrinks to whatever fits the wrap. The
     hidden-overflow rule on .home-lb-table-wrap depends on this so
     content never spills off the right edge on phone screens. */
  font-size: 0.88rem;
}
.home-lb-table thead th {
  text-align: left;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 600;
  color: var(--text-muted);
  padding: 12px 12px;
  border-bottom: 1px solid rgba(200, 144, 69, 0.20);
  white-space: nowrap;
  background: rgba(7, 9, 26, 0.4);
}
.home-lb-table tbody td {
  padding: 9px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  vertical-align: middle;
  white-space: nowrap;
}
.home-lb-table tbody tr:last-child td { border-bottom: none; }
.home-lb-table tbody tr:hover td { background: rgba(255, 255, 255, 0.03); }

.hlb-row.is-interactive { cursor: pointer; }
.hlb-row.is-interactive:hover td {
  background: rgba(56, 189, 248, 0.06);
}
.hlb-row.is-interactive:hover .hlb-name {
  color: #38bdf8;
}

.hlb-th-rank, .hlb-rank { width: 56px; text-align: center !important; }
.hlb-rank { text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; }
.hlb-rank-num { color: var(--text-muted); font-size: 0.85rem; }

/* Custom medal: shared red ribbon for all 3 ranks, gold/silver/bronze
   disc, and a soft pulsing glow that draws the eye to the podium. */
.hlb-medal {
  position: relative;
  display: inline-block;
  width: 26px;
  height: 30px;
  vertical-align: middle;
}
.hlb-medal-ribbon {
  position: absolute;
  top: 0;
  left: 50%;
  width: 18px;
  height: 14px;
  transform: translateX(-50%);
  background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
  /* Forked tail at the bottom + flat top so it reads like a ribbon. */
  clip-path: polygon(0 0, 32% 100%, 50% 70%, 68% 100%, 100% 0);
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
}
.hlb-medal-disc {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 20px;
  height: 20px;
  transform: translateX(-50%);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.74rem;
  font-weight: 900;
  font-family: 'Segoe UI', system-ui, sans-serif;
  color: rgba(0, 0, 0, 0.55);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
  /* Static shadow stack — no animation here. */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.25),
    inset 0 1px 1px rgba(255, 255, 255, 0.55),
    inset 0 -1px 1px rgba(0, 0, 0, 0.2);
}
/* Glow ring rendered as a ::after layer with a static box-shadow — only
   the opacity animates. opacity is composite-only on modern browsers, so
   the medal pulse no longer triggers paint on every frame (was box-shadow
   animation, which forced ~3 paints/frame for the 3 medals = idle scroll
   tax). */
.hlb-medal-disc::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 14px var(--medal-glow, transparent);
  pointer-events: none;
  animation: medalGlowOpacity 2.4s ease-in-out infinite;
  will-change: opacity;
}
@keyframes medalGlowOpacity {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1; }
}
.hlb-medal-1 .hlb-medal-disc {
  background: radial-gradient(circle at 35% 30%, #fef3c7 0%, #fbbf24 45%, #b45309 100%);
  --medal-glow: rgba(251, 191, 36, 0.65);
}
.hlb-medal-1 .hlb-medal-disc::after { animation-delay: 0s; }
.hlb-medal-2 .hlb-medal-disc {
  background: radial-gradient(circle at 35% 30%, #f9fafb 0%, #d1d5db 45%, #6b7280 100%);
  --medal-glow: rgba(229, 231, 235, 0.55);
}
.hlb-medal-2 .hlb-medal-disc::after { animation-delay: 0.4s; }
.hlb-medal-3 .hlb-medal-disc {
  background: radial-gradient(circle at 35% 30%, #fed7aa 0%, #c2410c 50%, #7c2d12 100%);
  --medal-glow: rgba(217, 119, 6, 0.55);
}
.hlb-medal-3 .hlb-medal-disc::after { animation-delay: 0.8s; }
@media (max-width: 720px) {
  .hlb-th-rank, .hlb-rank { width: 44px; }
  .hlb-medal { width: 22px; height: 26px; }
  .hlb-medal-ribbon { width: 14px; height: 11px; }
  .hlb-medal-disc { width: 17px; height: 17px; font-size: 0.66rem; }
}

.hlb-th-player { min-width: 240px; }
.hlb-player-cell {
  display: flex; align-items: center; gap: 10px;
  min-width: 0;
}
.hlb-avatar {
  width: 36px; height: 36px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(255,255,255,0.08);
  background: var(--border);
  flex-shrink: 0;
}
.hlb-avatar-fallback { background: linear-gradient(135deg, #c89045, #e6c478); }
.hlb-player-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}
.hlb-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 240px;
}
.hlb-bio {
  /* Bio renders white (slightly muted alpha for visual hierarchy under
     the player name) so it stays neutral regardless of the row's tier
     accent. oblique-9deg keeps Vietnamese diacritics intact at this
     size + weight (italic face crops them). */
  font-style: oblique 9deg;
  font-size: 0.68rem;
  font-weight: 500;
  /* Same tier accent as Elo + Win Rate so the row reads as one
     coherent colour band; falls back to soft white if no tier var. */
  color: var(--row-tier-color, rgba(229, 231, 235, 0.85));
  opacity: 0.92;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 280px;
  margin-top: 2px;
}
/* Stronger animation for Cao Thủ + Đại Cao Thủ + Thách Đấu rows: tier
   text glows + scales subtly; bio + Elo also pulse to reinforce the
   "elite" feeling. Lower tiers stay calm. */
.hlb-row.tier-master .hlb-tier-text,
.hlb-row.tier-grandmaster .hlb-tier-text,
.hlb-row.tier-challenger .hlb-tier-text {
  animation: tierTextPulse 2.2s ease-in-out infinite;
}
.hlb-row.tier-challenger .hlb-tier-text,
.hlb-row.tier-challenger .hlb-metric-cell {
  animation-duration: 1.6s;
  text-shadow:
    0 0 8px var(--row-tier-glow),
    0 0 14px var(--row-tier-glow);
}
.hlb-row.tier-master .hlb-metric-cell,
.hlb-row.tier-grandmaster .hlb-metric-cell,
.hlb-row.tier-challenger .hlb-metric-cell {
  animation: tierTextPulse 2.2s ease-in-out infinite;
}
@keyframes tierTextPulse {
  0%, 100% {
    text-shadow: 0 0 4px var(--row-tier-glow);
    transform: scale(1);
  }
  50% {
    text-shadow:
      0 0 10px var(--row-tier-glow),
      0 0 18px var(--row-tier-glow);
    transform: scale(1.04);
  }
}

.hlb-th-country, .hlb-country {
  width: 56px;
  text-align: center !important;
}
.hlb-country .hlb-flag {
  display: inline-flex;
  align-items: center;
}
.hlb-country .fi {
  width: 1.4em;
  height: 1em;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.25);
  border-radius: 2px;
  flex-shrink: 0;
}
.hlb-country-empty { color: var(--text-muted); font-size: 0.85rem; }

.hlb-th-metric, .hlb-metric-cell {
  text-align: center !important;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  /* Falls back to the gold accent for rows without a tier (shouldn't
     happen with real data but keeps demos rendering correctly). */
  color: var(--row-tier-color, #e6c478);
  text-shadow: 0 0 6px var(--row-tier-glow, transparent);
  width: 90px;
  font-size: 0.95rem;
}

.hlb-th-tier, .hlb-tier-cell {
  text-align: center !important;
  width: 120px;
  padding-left: 8px !important;
  padding-right: 8px !important;
}
.hlb-tier-text {
  display: inline-block;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--row-tier-color);
  text-shadow: 0 0 6px var(--row-tier-glow, transparent);
}
@media (max-width: 720px) {
  .hlb-th-tier, .hlb-tier-cell { width: 84px; }
  .hlb-tier-text { font-size: 0.78rem; }
}

.hlb-th-winrate, .hlb-winrate-cell {
  text-align: center !important;
  width: 100px;
  font-variant-numeric: tabular-nums;
}
.hlb-winrate {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 0.85rem;
  /* Match row tier colour so winrate, Elo, and tier text on the same
     row read as one cohesive theme. The wr-hot/mid/cold gradient that
     used to drive this is gone — Elo level already conveys quality. */
  color: var(--row-tier-color, rgba(229, 231, 235, 0.85));
  background: color-mix(in srgb, var(--row-tier-color, rgba(229,231,235,1)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--row-tier-color, rgba(229,231,235,1)) 30%, transparent);
}
.hlb-winrate.wr-none {
  color: var(--text-muted);
  background: transparent;
  border-color: transparent;
}

.hlb-th-gender, .hlb-gender-cell { width: 70px; text-align: center !important; }
.hlb-gender {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.hlb-gender.g-male   { color: #60a5fa; }
.hlb-gender.g-female { color: #f472b6; }
.hlb-gender.g-other  { color: #a78bfa; }
.hlb-gender.g-none   {
  color: var(--text-muted);
  font-size: 0.85rem;
}

.hlb-th-joined, .hlb-joined {
  text-align: center !important;
  width: 110px;
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  color: var(--text-muted);
}

.home-lb-empty {
  text-align: center;
  padding: 36px 14px;
  color: var(--text-muted);
}
.hlb-empty-icon { font-size: 2rem; margin-bottom: 8px; opacity: 0.65; }
.hlb-empty-title { font-weight: 600; color: var(--text); }

.home-lb-cta {
  display: flex; justify-content: center;
  margin-top: 22px;
}
.home-lb-cta .btn-secondary {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 22px;
  border: 1px solid rgba(255,255,255,0.12);
  background: rgba(20, 27, 61, 0.5);
  color: var(--text);
  text-decoration: none;
  border-radius: 999px;
  font-size: 0.9rem; font-weight: 600;
  transition: all 0.18s ease;
}
.home-lb-cta .btn-secondary:hover {
  border-color: rgba(200, 144, 69, 0.4);
  background: rgba(200, 144, 69, 0.10);
  transform: translateY(-1px);
}

@media (max-width: 720px) {
  .home-lb-title { font-size: 1.3rem; }
  .lb-seg { padding: 6px 12px; font-size: 0.78rem; }
  .home-lb-table { font-size: 0.8rem; }
  .home-lb-table thead th, .home-lb-table tbody td { padding: 8px 8px; }
  .hlb-avatar { width: 30px; height: 30px; }
  /* Now that the table scrolls horizontally, drop name truncation so each
     player's full nickname is visible — users can swipe right for the rest
     of the columns. */
  .hlb-name { max-width: none; }
  .hlb-bio { max-width: none; font-size: 0.62rem; }
  /* Mobile: keep every column visible, but make the wrap horizontally
     scrollable so users can swipe right to see the trailing info
     (Hạng, Quốc gia, Giới tính, Tham gia). The min-width on the table
     forces overflow when the wrap is narrower than the table itself. */
  .home-lb-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .home-lb-table { min-width: 720px; }
  .hlb-winrate { padding: 2px 7px; font-size: 0.78rem; }
  .hlb-th-winrate, .hlb-winrate-cell { width: 70px; }
}

@media (max-width: 600px) {
  .home-lb-table { font-size: 0.78rem; min-width: 700px; }
  .home-lb-table thead th, .home-lb-table tbody td { padding: 7px 6px; }
  .hlb-avatar { width: 26px; height: 26px; }
  .hlb-player-cell { gap: 8px; }
  .hlb-name { max-width: none; font-size: 0.82rem; }
  .hlb-bio { max-width: none; font-size: 0.58rem; }
  .hlb-tier-text { font-size: 0.74rem; }
  .hlb-metric-cell { font-size: 0.86rem; }
  .hlb-winrate { padding: 1px 6px; font-size: 0.74rem; }
  .hlb-th-winrate, .hlb-winrate-cell { width: 56px; }
  .hlb-th-country, .hlb-country { width: 36px; }
  .hlb-th-rank, .hlb-rank { width: 38px; }
}


/* ── Leaderboard ── */
.lb-section { padding: 40px 0 60px; }
.lb-title { font-size: 1.8rem; margin: 0 0 24px; letter-spacing: -0.01em; }

.lb-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  margin-bottom: 24px;
}
.lb-filter { display: flex; flex-direction: column; gap: 8px; }
.lb-filter label {
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
}
.lb-tabs {
  display: inline-flex;
  background: rgba(7, 9, 26, 0.55);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 999px;
  padding: 3px;
}
.lb-tab {
  background: transparent;
  border: none;
  color: var(--text-muted);
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: all 0.18s ease;
}
.lb-tab:hover { color: var(--text); }
.lb-tab.active {
  background: var(--accent);
  color: #06121f;
}

.lb-table-wrap {
  overflow-x: auto;
  background: rgba(7, 9, 26, 0.55);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 14px;
}
.lb-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  min-width: 600px;
}
.lb-table thead th {
  text-align: left;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  padding: 14px 14px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
}
.lb-table tbody td {
  padding: 12px 14px;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  vertical-align: middle;
}
.lb-table tbody tr:last-child td { border-bottom: none; }
.lb-table tbody tr:hover td { background: rgba(255,255,255,0.025); }
.lb-rank {
  width: 60px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
tr.rank-1 .lb-rank { color: #fbbf24; font-size: 1.1rem; }
tr.rank-2 .lb-rank { color: #d1d5db; font-size: 1.05rem; }
tr.rank-3 .lb-rank { color: #d97706; font-size: 1.0rem; }
.lb-player-cell {
  display: flex; align-items: center; gap: 10px;
  min-width: 0;
}
.lb-avatar {
  width: 32px; height: 32px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 1px solid rgba(255,255,255,0.08);
  background: var(--border);
}
.lb-avatar-fallback { background: linear-gradient(135deg, var(--accent), var(--accent2)); }
.lb-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
.lb-country .fi { width: 1.2em; box-shadow: 0 0 0 1px rgba(0,0,0,0.2); border-radius: 2px; margin-right: 6px; }
.lb-country-empty { color: var(--text-muted); }
.lb-games, .lb-rate { font-variant-numeric: tabular-nums; font-weight: 600; }
.lb-rate { color: var(--accent); }
.lb-wld { font-variant-numeric: tabular-nums; }
.lb-empty { text-align: center; color: var(--text-muted); padding: 40px 14px; }

@media (max-width: 600px) {
  .lb-section { padding: 24px 0 40px; }
  .lb-title { font-size: 1.4rem; }
  .lb-filters { gap: 16px; }
  .lb-table thead th, .lb-table tbody td { padding: 10px 10px; }
  .lb-table { font-size: 0.8rem; }
  .lb-name { max-width: 110px; }
  .lb-country span:not(.fi) { display: none; }
}

/* Personal ranking inside profile modal — one compact row per game type
   (chess/xiangqi/gomoku). Grid keeps the columns aligned even when one
   row is "no games yet". */
.rk-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column;
  gap: 6px;
}
.rk-row {
  display: grid;
  /* Fixed column widths so every row aligns: icon · name · rank · Elo ·
     W/L/D · winrate. The name column flexes to fill the extra horizontal
     room from the wider modal; the data columns stay locked so 81% / 89%
     / 67% land on the same x across rows. */
  grid-template-columns: 22px minmax(80px, 1fr) 56px 100px 84px 56px;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: rgba(7, 9, 26, 0.55);
  border: 1px solid rgba(200, 144, 69, 0.18);
  border-radius: 10px;
  transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.rk-row:hover {
  border-color: rgba(200, 144, 69, 0.35);
  transform: translateY(-1px);
  background: rgba(7, 9, 26, 0.7);
}
.rk-icon { color: #e6c478; display: inline-flex; }
.rk-icon-chess   { color: #fbbf24; }
.rk-icon-xiangqi { color: #fb7185; }
.rk-icon-gomoku  { color: #60a5fa; }
.rk-game { font-weight: 600; font-size: 0.88rem; }
.rk-rank {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 40px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  /* Color, bg, and border all keyed off --row-tier-color so the rank
     pill matches the Elo + winrate pill on the same row. Fallback to
     the original gold when tier var isn't set. */
  color: var(--row-tier-color, #e6c478);
  font-size: 0.92rem;
  letter-spacing: 0.02em;
  padding: 2px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--row-tier-color, #e6c478) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--row-tier-color, #e6c478) 30%, transparent);
}
.rk-rank-none {
  color: var(--text-muted);
  background: transparent;
  border-color: rgba(255,255,255,0.08);
  font-weight: 600;
}
.rk-elo {
  white-space: nowrap;
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  justify-content: flex-start;
}
.rk-wld {
  text-align: center;
  justify-self: center;
}
.rk-pct {
  justify-self: center;
}
.rk-elo strong {
  font-size: 0.95rem;
  /* Tier-themed Elo so it visually pairs with rank + winrate pills. */
  color: var(--row-tier-color, var(--text));
  font-variant-numeric: tabular-nums;
}
.rk-elo strong::after {
  content: ' Elo';
  font-size: 0.68rem;
  color: var(--text-muted);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: inline;
  white-space: nowrap;
}
.rk-wld {
  font-variant-numeric: tabular-nums;
  font-size: 0.84rem;
  font-weight: 600;
  white-space: nowrap;
}
.rk-wld .rk-w { color: #34d399; }
.rk-wld .rk-l { color: #fb7185; }
.rk-wld .rk-d { color: #a78bfa; }
.rk-wld .rk-sep { color: var(--text-muted); margin: 0 2px; font-weight: 400; }
.rk-wld-empty { color: var(--text-muted); font-style: italic; }
.rk-pct {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  padding: 2px 8px;
  border-radius: 999px;
  /* Single tier-keyed colour for the win-rate pill so Elo, rank, and
     winrate all match the row's tier theme. */
  color: var(--row-tier-color, #6ee7b7);
  background: color-mix(in srgb, var(--row-tier-color, #6ee7b7) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--row-tier-color, #6ee7b7) 30%, transparent);
}
/* Legacy hot/mid/cold tier classes kept for back-compat but inherit
   from .rk-pct now (no override needed). */
.rk-pct-hot   { color: #6ee7b7; background: rgba(110, 231, 183, 0.10); border-color: rgba(110, 231, 183, 0.25); }
.rk-pct-mid   { color: #fbbf24; background: rgba(251, 191, 36, 0.10); border-color: rgba(251, 191, 36, 0.25); }
.rk-pct-cold  { color: #fca5a5; background: rgba(252, 165, 165, 0.10); border-color: rgba(252, 165, 165, 0.25); }
.rk-pct-empty { color: var(--text-muted); background: transparent; border-color: transparent; }
@media (max-width: 480px) {
  .rk-row {
    grid-template-columns: 20px minmax(60px, 1fr) 44px 78px 56px;
    gap: 8px;
    padding: 8px 10px;
  }
  /* On tiny screens drop the W/L/D triple — Elo + winrate is enough at a glance. */
  .rk-wld { display: none; }
  .rk-game { font-size: 0.82rem; }
  .rk-elo strong { font-size: 0.88rem; }
  .rk-elo strong::after { display: none; }
}

.bd-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.bd-row {
  display: grid;
  grid-template-columns: 110px 1fr auto auto;
  align-items: center;
  gap: 14px;
  padding: 10px 12px;
  background: rgba(7, 9, 26, 0.45);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 10px;
  font-size: 0.85rem;
  transition: border-color 0.15s ease;
}
.bd-row:hover { border-color: rgba(255,255,255,0.12); }
.bd-game { font-weight: 600; }
.bd-bar {
  display: flex;
  height: 8px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255,255,255,0.04);
}
.bd-bar-w { background: #34d399; }
.bd-bar-d { background: #a78bfa; }
.bd-bar-l { background: #fb7185; }
.bd-bar > span { display: block; height: 100%; transition: width 0.3s ease; }
.bd-counts { font-variant-numeric: tabular-nums; font-size: 0.8rem; }
.bd-counts .bd-w { color: #34d399; font-weight: 600; }
.bd-counts .bd-l { color: #fb7185; font-weight: 600; }
.bd-counts .bd-d { color: #a78bfa; font-weight: 600; }
.bd-counts .bd-sep { color: var(--text-muted); margin: 0 3px; }
.bd-rate { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; min-width: 42px; text-align: right; }
@media (max-width: 600px) {
  .bd-row { grid-template-columns: 80px 1fr auto; gap: 8px; padding: 8px 10px; font-size: 0.78rem; }
  .bd-counts { display: none; }
}

.hist-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 12px;
  background: rgba(7, 9, 26, 0.45);
}
.hist-row {
  display: grid;
  grid-template-columns: auto auto auto 1fr auto auto;
  gap: 10px;
  align-items: center;
  padding: 10px 14px;
  font-size: 0.85rem;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.hist-row:last-child { border-bottom: none; }
.hist-row:hover { background: rgba(255,255,255,0.03); }
.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  font-size: 0.72rem;
  font-weight: 700;
  flex-shrink: 0;
}
.badge-win  { background: rgba(52, 211, 153, 0.18); color: #34d399; }
.badge-loss { background: rgba(251, 113, 133, 0.18); color: #fb7185; }
.badge-draw { background: rgba(167, 139, 250, 0.18); color: #a78bfa; }
.hist-game {
  font-weight: 600;
  white-space: nowrap;
}
.hist-vs {
  color: var(--text-muted);
  font-size: 0.78rem;
}
.hist-opp {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.hist-meta, .hist-time {
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.hist-replay {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: rgba(56, 189, 248, 0.12);
  border: 1px solid rgba(56, 189, 248, 0.35);
  color: #38bdf8;
  text-decoration: none;
  font-size: 0.7rem;
  margin-left: 8px;
  transition: all 0.15s ease;
  flex-shrink: 0;
}
.hist-replay:hover {
  background: rgba(56, 189, 248, 0.25);
  border-color: rgba(56, 189, 248, 0.6);
  transform: scale(1.08);
}

.profile-footer {
  margin-top: 8px;
  padding-top: 20px;
  border-top: 1px solid rgba(255,255,255,0.06);
  display: flex;
  justify-content: flex-end;
}
/* The sign-out row is now layered on top of .fbp-row which already provides
   the row chrome (padding, hover bg, etc). Only override the colours so the
   row reads as destructive — no extra border, otherwise it doubles the
   card border and shows the red outline the user reported. */
.btn-signout-large {
  color: #fca5a5;
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
}
.btn-signout-large:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px rgba(239, 68, 68, 0.4);
}

/* Compact game-history rows on phones — fold the 6-col desktop layout
   into a 2-row grid (badge | game/opp | time/meta) so each entry fits
   without horizontal overflow inside the profile modal. */
@media (max-width: 600px) {
  .hist-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "badge game time"
      "badge opp meta";
    gap: 4px 10px;
  }
  .hist-row .badge { grid-area: badge; }
  .hist-row .hist-game { grid-area: game; }
  .hist-row .hist-vs { display: none; }
  .hist-row .hist-opp { grid-area: opp; }
  .hist-row .hist-meta { grid-area: meta; text-align: right; }
  .hist-row .hist-time { grid-area: time; text-align: right; }
}
.board-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: var(--board-w, 100%);
  max-width: 100%;
  min-height: 28px;
  padding: 0 2px;
  box-sizing: border-box;
}
.board-topbar .status-bar {
  order: 1;
  flex: 1 1 auto;
  text-align: left;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.board-topbar .btn-watch { order: 2; flex-shrink: 0; }
.btn-watch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(56, 189, 248, 0.08);
  border: 1px solid rgba(56, 189, 248, 0.3);
  color: #7dd3fc;
  padding: 4px 12px;
  border-radius: 16px;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s, transform 0.15s;
  white-space: nowrap;
  letter-spacing: 0.3px;
}
.btn-watch svg { opacity: 0.9; flex-shrink: 0; }
.btn-watch:hover {
  background: rgba(56, 189, 248, 0.18);
  border-color: rgba(56, 189, 248, 0.55);
  transform: translateY(-1px);
}
.btn-watch.has-viewers { color: #fbbf24; border-color: rgba(251, 191, 36, 0.45); background: rgba(251, 191, 36, 0.08); }
.btn-watch.has-viewers:hover { background: rgba(251, 191, 36, 0.18); }

/* Spectator read-only mode */
body.spectator-mode #boardContainer { pointer-events: none; }
body.spectator-mode .btn-resign,
body.spectator-mode .action-btns,
body.spectator-mode #rematchBtn,
body.spectator-mode #declineBtn,
body.spectator-mode .me-avatar { pointer-events: none; }
body.spectator-mode .btn-resign,
body.spectator-mode #rematchBtn,
body.spectator-mode #declineBtn { display: none !important; }
body.spectator-mode .me-avatar { cursor: default; }

/* ── Game Layout ── */
.game-main {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  padding: 6px 12px;
  /* Center the column-stack horizontally so its width can shrink-wrap
     to whatever the board needs. */
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* All game types: timers relocated next to the LEFT edge of the
   board instead of inside the player panels. Anchored INSIDE
   .board-wrapper (which has position:relative already + spans the
   layout). The wrapper is wider than the board — board is centered
   inside it via align-items:center — so a `right: 100%` would sit
   against the wrapper's edge (viewport edge), not the board's edge.
   Use a calc() that derives sidebar's right position from the board
   width: distance from wrapper's right edge to board's left edge =
   (wrapper.width + board.width) / 2, expressed in CSS as
   `50% + var(--board-w) / 2` plus a small gap.
   Vertically centered against .board-wrapper so the column sits
   dead between the wrapper's top/bottom (above/below the actual
   board surface). */
.side-clocks {
  position: absolute;
  top: 50%;
  right: calc(50% + var(--board-w, 600px) / 2 + clamp(8px, 1.6vw, 20px));
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: clamp(8px, calc(var(--board-w, 600px) * 0.025), 18px);
  z-index: 5;
  pointer-events: none; /* clocks are purely informational */
}
/* When the timer is in the sidebar, keep BOTH player clocks visible
   (vs. the in-panel "hide the inactive side" pattern) — players want
   to see the opponent's remaining time at a glance. Dim the inactive
   side so it's clear whose turn it is. */
.side-clocks .move-timer.hidden {
  display: inline-flex !important;
  opacity: 0.35;
  filter: grayscale(0.5);
}

.game-layout {
  display: flex;
  flex-direction: column;
  /* Center children so the column itself spans the full game-main width
     (preserving the board's container-query width = cells stay big),
     and individual panels can shrink themselves down to the board's
     measured width via the --board-w variable. */
  align-items: center;
  width: 100%;
  gap: 6px;
  height: 100%;
}

/* ── Player Panels ── */
/* Panels match the board's measured width via --board-w (set by JS on
   the .game-layout). game-layout is align-items:center so the panel
   self-centers under the board. Falls back to 100% before the first
   measurement so the panel isn't 0-width on the initial paint.
   3-column grid: left = player-info (avatar + name), middle = move-timer
   (justify-self:center keeps it dead-centered regardless of how wide
   left/right grow), right = action-btns (resign on the me-panel,
   empty on the opponent-panel). */
.player-panel {
  display: grid;
  /* minmax(0, 1fr) instead of plain 1fr: the default `1fr` is
     `minmax(auto, 1fr)`, so the track refuses to shrink below its
     content's min-content size. With long Vietnamese nicknames in
     col 1 ("Trương Văn Thiện") on narrow viewports the name pushed
     col 2 to the right and the move-timer overlapped the nickname.
     minmax(0, 1fr) lets col 1 collapse so the player-name ellipsis
     kicks in cleanly. Same trick on col 3 keeps the action buttons
     anchored to the right edge regardless of how wide col 1 grew. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(8px, 2.5%, 16px);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 14px;
  width: var(--board-w, 100%);
  max-width: 100%;
  flex-shrink: 0;
  /* Fixed height so captured pieces or a varying timer width can never
     reflow the column above (the board would resize through container
     queries). Children that would otherwise grow taller scroll instead. */
  min-height: 64px;
  height: 64px;
}
/* Pin every child to a specific grid column so hiding the move-timer
   (display:none on the inactive side's clock) doesn't let auto-flow
   slide the resign button leftward into the middle slot. The button
   stays anchored to column 3 / the right edge regardless of which
   side's turn it is. */
.player-panel .player-info { grid-column: 1; justify-self: start; min-width: 0; }
.player-panel .move-timer  { grid-column: 2; justify-self: center; }
.player-panel .action-btns { grid-column: 3; justify-self: end; }
.player-info { display: flex; align-items: center; gap: 12px; }
.player-avatar {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: transform 0.15s, border-color 0.15s;
}
.player-avatar img { width: 100%; height: 100%; display: block; }
.me-avatar { border-color: var(--accent2); cursor: pointer; }
.me-avatar:hover { transform: scale(1.06); border-color: #fff; }
/* font-size here drives every text child via em — .player-name uses
   1em, .total-clock uses 0.72em, etc. Single source of truth keeps
   the chip + nickname proportionally locked across all breakpoints. */
.player-text { display: flex; flex-direction: column; min-width: 0; gap: 4px; font-size: 0.95rem; }
.player-row { display: flex; align-items: baseline; gap: 6px; flex-wrap: nowrap; min-width: 0; }
/* Secondary info line: total clock · captured pieces. Sits directly
   under the name. nowrap + overflow hidden so a long captured list
   can never spill onto a second line and reflow the panel height. */
.player-row-secondary {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
  min-width: 0;
  overflow: hidden;
}
.player-row-secondary .total-clock { margin-top: 0; }
/* Nickname stays on a single line — when the panel column is narrow
   (MacBook, tablet, anything tighter than the desktop ideal) the name
   is truncated with an ellipsis instead of wrapping into a second
   row that would push the captured pieces off the panel. The flag
   prefix is kept inline via display:inline-block on .player-flag. */
.player-name {
  font-weight: 600;
  /* 1em = .player-text's font-size (the panel-text scale). Lets
     breakpoints adjust the whole panel typography via .player-text
     instead of per-element overrides. */
  font-size: 1em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  max-width: 100%;
  display: block;
}
/* The "Trắng / Đen" color label was redundant — the avatar position
   above/below the board already makes it obvious which side the player
   is on, and the dot separator was visual clutter. Hide both. */
.player-color { display: none; }
.player-row .player-color:not(:empty)::before {
  content: '•';
  margin-right: 4px;
  opacity: 0.55;
}
/* Total-game clock under each player's name. Styled as a compact
   tonal pill (border + slight inner darken) so it reads as
   "scoreboard time remaining" at a glance instead of muted body
   text that the previous design buried. Sizes scale off var
   (--board-w) so the pill keeps the same proportional weight on
   chess / xiangqi / gomoku across viewports. */
.total-clock {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-variant-numeric: tabular-nums;
  /* em-based: total-clock ALWAYS stays ~72% of the surrounding
     player-text font-size, so the chip's visual weight relative to
     the nickname is identical on every breakpoint. Padding +
     border-radius scale the same way so the pill keeps its
     proportions when the panel typography shrinks for mobile. */
  font-size: 0.72em;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #e5e7eb;
  background: rgba(15, 23, 42, 0.55);
  border: 1px solid rgba(148, 163, 184, 0.25);
  border-radius: 0.32em;
  padding: 0.18em 0.55em;
  margin-top: 3px;
  display: inline-flex;
  align-items: center;
  line-height: 1;
  width: max-content;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.total-clock.warning {
  color: #fef2f2;
  background: rgba(220, 38, 38, 0.32);
  border-color: rgba(248, 113, 113, 0.7);
  font-weight: 700;
  animation: totalClockWarn 0.9s ease-in-out infinite alternate;
}
@keyframes totalClockWarn {
  from { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 0 0 rgba(239, 68, 68, 0.0); }
  to   { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 0 2px rgba(239, 68, 68, 0.32); }
}
.score-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 8px auto 20px;
  padding: 14px 24px;
  background: linear-gradient(135deg, rgba(56,189,248,0.1), rgba(167,139,250,0.1));
  border: 1px solid rgba(56,189,248,0.25);
  border-radius: 14px;
  max-width: 320px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 4px 14px rgba(0,0,0,0.2);
}
.score-card-row {
  display: flex;
  align-items: center;
  gap: 14px;
  font-variant-numeric: tabular-nums;
}
.score-side {
  font-size: 0.78rem;
  color: var(--text-muted);
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  min-width: 60px;
}
.score-side.me { text-align: right; color: var(--accent2); }
.score-side.opp { text-align: left; color: var(--accent2); }
.score-number {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-size: 2rem;
  font-weight: 800;
  color: var(--text);
  min-width: 32px;
  text-align: center;
  transition: color 0.3s, transform 0.3s;
}
.score-number.winner {
  color: #38bdf8;
  text-shadow: 0 0 12px rgba(56,189,248,0.6);
  animation: scoreWinPop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes scoreWinPop {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.3); }
  100% { transform: scale(1); }
}
.score-separator {
  font-size: 1.5rem;
  color: var(--text-muted);
  font-weight: 700;
}
.score-card-label {
  font-size: 0.72rem;
  color: var(--text-muted);
  letter-spacing: 1px;
  text-transform: uppercase;
  opacity: 0.75;
}
.captured-pieces {
  display: inline-flex;
  /* Lock to a single row so the panel's fixed height never grows with
     each capture. Excess captures scroll horizontally inside the row. */
  flex-wrap: nowrap;
  align-items: center;
  font-size: 1.1rem;
  line-height: 1.2;
  min-height: 1.2em;
  max-width: 30vw;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}
.captured-pieces::-webkit-scrollbar { display: none; }
.captured-pieces:empty { display: none; }
/* Tournament-grade chess clock. Sharp corners, monospaced LCD-style
   digits, deep slate body that stays dark in both themes (real chess
   clocks don't change color with the room lighting). The active
   side's clock is the only one rendered (the other is hidden via
   .hidden), so this single rule covers both panels. */
.move-timer {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', 'Courier New', monospace;
  font-weight: 700;
  /* Scale font + padding + min-width with the rendered board width
     (var --board-w set by initBoardWidthSync). Narrow gomoku boards
     no longer leave a 96px-wide timer crammed against a long
     nickname in col 1. Clamps preserve readability on tiny phones +
     prevent the timer ballooning on ultrawide. */
  font-size: clamp(0.9rem, calc(var(--board-w, 600px) * 0.034), 1.5rem);
  font-variant-numeric: tabular-nums;
  letter-spacing: 1px;
  color: #e5e7eb;
  background: #0f172a;
  border: 1px solid rgba(148, 163, 184, 0.28);
  padding: clamp(3px, calc(var(--board-w, 600px) * 0.012), 7px)
           clamp(10px, calc(var(--board-w, 600px) * 0.028), 18px);
  border-radius: 6px;
  min-width: clamp(60px, calc(var(--board-w, 600px) * 0.18), 96px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.04),
    0 2px 4px rgba(0, 0, 0, 0.45);
  transition: color 0.25s ease, background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
}
/* Thin progress strip at the bottom — tracks the current move's time
   remaining via the --timer-progress var written from JS. */
.move-timer::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  height: 2px;
  width: var(--timer-progress, 100%);
  background: currentColor;
  opacity: 0.55;
  transition: width 0.3s linear;
}
.move-timer.warning {
  color: #fca5a5;
  background: #2a0f0f;
  border-color: rgba(239, 68, 68, 0.5);
  animation: timerWarningPulse 0.6s ease-in-out infinite alternate;
}
@keyframes timerWarningPulse {
  from {
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.04),
      0 2px 4px rgba(0, 0, 0, 0.45);
  }
  to {
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.04),
      0 0 0 3px rgba(239, 68, 68, 0.32),
      0 2px 8px rgba(239, 68, 68, 0.45);
  }
}
/* Light mode: keep the clock dark — that's the "chess clock" look in
   both themes. Just adjust the outer shadow so it reads on white. */
[data-theme="light"] .move-timer {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.04),
    0 2px 6px rgba(15, 23, 42, 0.18);
}
/* Action button column on the me-panel. Always pinned to the right
   edge of the panel grid (justify-self:end on .player-panel above)
   and given a fixed width so the resign button doesn't shrink/grow
   when the timer text width changes. */
.action-btns {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex-shrink: 0;
}
.btn-resign {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
  padding: 6px 0;
  border-radius: 6px;
  font-size: 0.85rem;
  font-weight: 600;
  width: 110px;
  cursor: pointer;
  text-align: center;
  white-space: nowrap;
  transition: background 0.2s;
}
.btn-resign:hover { background: rgba(239,68,68,0.1); }
@media (max-width: 600px) { .btn-resign { width: 90px; font-size: 0.78rem; } }
@media (max-width: 380px) { .btn-resign { width: 76px; font-size: 0.72rem; padding: 4px 0; } }

/* Plain center-of-board status pill — small text on a translucent
   dark background, no gradients/sparkles. Sibling of the loud
   .new-game-banner; this one is for low-key updates like "opponent
   reconnected" where the celebratory treatment would feel wrong.
   Sized proportionally to the rendered board via --board-w (set by
   initBoardWidthSync in game.js) so the pill keeps the same visual
   weight across desktop / tablet / phone breakpoints. Clamped so
   tiny / huge viewports stay readable. */
.center-status {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0.96);
  background: rgba(15, 23, 42, 0.78);
  color: #f3f4f6;
  font-size: clamp(0.75rem, calc(var(--board-w, 600px) * 0.035), 1.4rem);
  font-weight: 500;
  letter-spacing: 0.2px;
  padding: clamp(6px, calc(var(--board-w, 600px) * 0.018), 14px)
           clamp(12px, calc(var(--board-w, 600px) * 0.036), 28px);
  border-radius: 8px;
  white-space: nowrap;
  max-width: 80%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 50;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}
.center-status.show {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
[data-theme="light"] .center-status {
  background: rgba(30, 41, 59, 0.85);
  color: #fff;
}

/* Custom CSS tooltip for elements with [data-tooltip]. Anchored ABOVE
   the trigger (the browser-native `title` tooltip floats below by
   default, which would get clipped by the bottom edge of the player
   panel here). Pure CSS — appears on :hover / :focus-visible after a
   short delay so the user doesn't get spammed for a passing cursor. */
[data-tooltip] { position: relative; }
[data-tooltip]::before {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: rgba(15, 23, 42, 0.95);
  color: #f3f4f6;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.1px;
  padding: 5px 10px;
  border-radius: 6px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s ease 0s, transform 0.18s ease 0s;
  z-index: 100;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
[data-tooltip]::after {
  content: '';
  position: absolute;
  bottom: calc(100% + 2px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  border: 5px solid transparent;
  border-top-color: rgba(15, 23, 42, 0.95);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s ease 0s, transform 0.18s ease 0s;
  z-index: 100;
}
[data-tooltip]:hover::before,
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::before,
[data-tooltip]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  transition-delay: 0.3s;
}
[data-theme="light"] [data-tooltip]::before { color: #fff; }

/* ── Action buttons (draw-offer + resign) ───────────────────────────
   Live inside the bottom (me-) player panel's column 3, horizontal
   pair pinned to the right edge of the panel and vertically centered
   in the panel row. Two icon-only "fabs": draw is neutral grey (lower-
   weight action), resign is danger red (high-weight action). Hidden
   when the game isn't in-progress so the icons don't sit dead next
   to a static name+clock row. */
.action-btns {
  display: inline-flex;
  align-items: center;
  /* Gap scales with the board so the two icons stay tight on a
     small board + roomier on a big board. */
  gap: clamp(4px, calc(var(--board-w, 600px) * 0.012), 10px);
}
.action-btns.hidden { display: none; }
.action-fab {
  /* Square button sized proportionally to the rendered board width
     (var --board-w is propagated to .game-layout by initBoardWidthSync,
     and inherits down to this button inside .player-panel). Clamped
     so tiny phones don't shrink it below tap-target size + ultrawide
     screens don't blow it up. SVG inside follows the same scale via
     a fixed ratio (~55% of the fab). */
  width:  clamp(30px, calc(var(--board-w, 600px) * 0.062), 48px);
  height: clamp(30px, calc(var(--board-w, 600px) * 0.062), 48px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border-radius: clamp(6px, calc(var(--board-w, 600px) * 0.014), 10px);
  cursor: pointer;
  padding: 0;
  transition: background 0.16s ease, transform 0.1s ease, border-color 0.16s ease, opacity 0.2s;
}
.action-fab svg {
  width:  clamp(16px, calc(var(--board-w, 600px) * 0.034), 26px);
  height: clamp(16px, calc(var(--board-w, 600px) * 0.034), 26px);
}
.action-fab--draw {
  color: var(--text-muted);
  border: 1.5px solid var(--text-muted);
}
.action-fab--draw:hover:not(:disabled) {
  background: rgba(148, 163, 184, 0.14);
  color: var(--text);
  border-color: var(--text);
}
.action-fab--resign {
  color: var(--danger);
  border: 1.5px solid var(--danger);
}
.action-fab--resign:hover:not(:disabled) {
  background: rgba(239, 68, 68, 0.12);
}
.action-fab:active:not(:disabled) { transform: scale(0.94); }
.action-fab:disabled { opacity: 0.4; cursor: not-allowed; }
.action-fab.is-pending {
  background: rgba(148, 163, 184, 0.18);
  animation: actionFabPulse 1.5s ease-in-out infinite;
}
@keyframes actionFabPulse {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}
/* Spectators never need draw/resign. */
body.spectator-mode .action-btns { display: none !important; }
/* Button/SVG sizing is fully driven by the clamp() formulas above via
   var(--board-w), so the old viewport-width @media breakpoints are no
   longer needed — small boards shrink the icon, large boards expand it. */

/* ── Board Wrapper ── */
.board-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-height: 0;
  width: 100%;
  position: relative;
  container-type: size;
}
.status-bar {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  min-height: 22px;
  text-align: left;
  letter-spacing: 0.3px;
}
/* Same accent blue for both your-turn + opp-turn so the status bar
   colour stays consistent across the whole game session — only the
   text changes ("Lượt của bạn" ↔ "Lượt của đối thủ"). */
.status-bar.your-turn,
.status-bar.opp-turn { color: var(--accent); }
.status-bar.in-check  { color: var(--danger); animation: pulse 0.6s ease-in-out infinite alternate; }
.status-bar.opp-disconnected {
  color: var(--danger);
  font-weight: 600;
  animation: oppDcPulse 1.4s ease-in-out infinite;
}
@keyframes oppDcPulse {
  0%, 100% { opacity: 0.65; }
  50%      { opacity: 1; }
}
@keyframes statusShimmer {
  from { background-position: 0% 50%; }
  to   { background-position: 300% 50%; }
}

/* Radial glow overlay on the board during win review */
body.game-ended-win .board-wrapper::before {
  content: '';
  position: absolute;
  inset: -20px;
  background: radial-gradient(ellipse at center, rgba(255,212,59,0.18), transparent 70%);
  pointer-events: none;
  z-index: 0;
  animation: winAura 1.2s ease-in-out infinite alternate;
}
@keyframes winAura {
  from { opacity: 0.4; }
  to   { opacity: 1; }
}

body.game-ended .chess-square.in-check {
  animation: checkmatePulse 0.5s ease-in-out infinite alternate;
}
@keyframes checkmatePulse {
  from { background: rgba(239,68,68,0.5) !important; box-shadow: inset 0 0 0 2px #ef4444; }
  to { background: rgba(239,68,68,0.9) !important; box-shadow: inset 0 0 0 4px #dc2626, 0 0 18px rgba(239,68,68,0.7); }
}
@keyframes pulse { from { opacity: 1; } to { opacity: 0.5; } }

/* ══════════════════════════════════════
   CHESS BOARD
══════════════════════════════════════ */
#boardContainer { user-select: none; }

.chess-board {
  --chess-cell: max(34px, min(calc((100cqw - 16px) / 8), calc((100cqh - 40px) / 8)));
  display: grid;
  grid-template-columns: repeat(8, var(--chess-cell));
  grid-template-rows: repeat(8, var(--chess-cell));
  border: 2px solid #5c3d1e;
  border-radius: 4px;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.chess-square {
  width: var(--chess-cell);
  height: var(--chess-cell);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  position: relative;
  transition: background 0.1s;
}
.chess-square.light { background: var(--chess-light); }
.chess-square.dark { background: var(--chess-dark); }
.chess-square.selected::before,
.chess-square.last-move::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  transition: opacity 0.25s ease;
}
/* Selected piece + last-move highlight share one blue glow so the
   "I picked this piece up" cue and the "this was the move" cue read
   as the same visual language. */
.chess-square.selected::before,
.chess-square.last-move::before {
  background: radial-gradient(circle at center, rgba(80, 170, 240, 0.85) 0%, rgba(80, 170, 240, 0.4) 70%, rgba(80, 170, 240, 0.16) 100%);
}
.chess-square.valid-move::after {
  content: '';
  position: absolute;
  width: 32%; height: 32%;
  background: rgba(20, 85, 30, 0.5);
  border-radius: 50%;
  pointer-events: none;
}
/* The capture-ring overlay around an attackable piece used to live
   here (.chess-square.valid-capture::after) but the user found it
   visually noisy. The piece itself + the .selected highlight on the
   moving piece are enough to communicate the legal capture. */
.chess-square.in-check { background: rgba(239,68,68,0.6) !important; }
.chess-piece {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 88%;
  height: 88%;
  cursor: pointer;
  z-index: 1;
  transition: transform 0.28s cubic-bezier(0.25, 0.85, 0.35, 1);
  pointer-events: none;
  filter: drop-shadow(0 2px 2px rgba(0,0,0,0.22));
}
.chess-piece-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}
.chess-square:hover .chess-piece { transform: scale(1.04); }
.coord-label {
  position: absolute;
  font-size: 0.6rem;
  font-weight: 700;
  color: rgba(0,0,0,0.35);
  pointer-events: none;
}
.coord-file { bottom: 2px; right: 4px; }
.coord-rank { top: 2px; left: 3px; }
.chess-square.dark .coord-label { color: rgba(240,217,181,0.5); }

/* ══════════════════════════════════════
   XIANGQI BOARD — wooden disc carved style
   ══════════════════════════════════════ */
.xiangqi-board {
  --xq-cell: max(26px, min(calc((100cqw - 30px) / 10), calc((100cqh - 70px) / 12)));

  /* Color tokens (paper/wood/walnut palette) */
  --xq-board-bg:      #eacb8d;
  --xq-board-bg-alt:  #e0bd7a;
  --xq-board-border:  #6b4423;
  --xq-grid:          #4a2f1a;
  --xq-grid-soft:     rgba(74, 47, 26, 0.65);
  --xq-label:         #5b3420;

  --xq-disc-face:     #f3e2bf;
  --xq-disc-face-2:   #e7cf9b;
  --xq-disc-rim-red:  #a61818;
  --xq-disc-rim-blk:  #1a1a1a;
  --xq-char-red:      #a61818;
  --xq-char-blk:      #151515;

  --xq-selected:  rgba(255, 213, 79, 0.50);
  --xq-lastmove:  rgba(155, 199, 0, 0.38);
  --xq-legal:     rgba(35, 35, 35, 0.28);

  position: relative;
  display: inline-block;
  background:
    /* Subtle paper-grain noise layer — tiny fractal specks blended over the wood */
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.25  0 0 0 0 0.15  0 0 0 0 0.06  0 0 0 0.06 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"),
    radial-gradient(120% 90% at 50% 0%, rgba(255, 255, 255, 0.18), transparent 60%),
    /* AI-generated cream wood texture (Gemini 3 Pro Image). Falls back
       to the gradient if the asset 404s. */
    url("/img/xiangqi/board-bg.jpg") center/cover,
    linear-gradient(160deg, var(--xq-board-bg) 0%, var(--xq-board-bg-alt) 100%);
  border: calc(var(--xq-cell) * 0.11) solid var(--xq-board-border);
  border-radius: 6px;
  box-shadow:
    var(--shadow),
    inset 0 0 0 2px rgba(255,255,255,0.18),
    inset 0 0 24px rgba(101, 62, 31, 0.22);
  padding: calc(var(--xq-cell) * 0.52) calc(var(--xq-cell) * 0.52);
}

.xq-col-labels {
  display: grid;
  grid-template-columns: repeat(9, var(--xq-cell));
  font-family: 'LXGW WenKai TC', 'Noto Serif SC', 'KaiTi', serif;
  font-weight: 700;
  font-size: calc(var(--xq-cell) * 0.26);
  color: var(--xq-label);
  text-align: center;
  pointer-events: none;
  user-select: none;
  opacity: 0.75;
}
.xq-col-labels.top    { margin-bottom: calc(var(--xq-cell) * 0.14); }
.xq-col-labels.bottom { margin-top:    calc(var(--xq-cell) * 0.14); }

.xiangqi-grid {
  display: grid;
  grid-template-columns: repeat(9, var(--xq-cell));
  grid-template-rows: repeat(10, var(--xq-cell));
  position: relative;
}
.xiangqi-cell {
  width: var(--xq-cell); height: var(--xq-cell);
  position: relative;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
/* Grid lines: main horizontals/verticals drawn by cell pseudo-elements.
   Slightly thicker than before for proper visual weight. */
.xiangqi-cell::before,
.xiangqi-cell::after {
  content: '';
  position: absolute;
  background: var(--xq-grid);
  pointer-events: none;
  z-index: 0;
}
.xiangqi-cell::before { height: 1.3px; width: 100%; top: calc(50% - 0.5px); left: 0; }
.xiangqi-cell::after  { width: 1.3px; height: 100%; left: calc(50% - 0.5px); top: 0; }
.xiangqi-cell.edge-top::before    { top: calc(50% - 0.5px); height: calc(50% + 1.3px); }
.xiangqi-cell.edge-bottom::before { height: 50%; }
.xiangqi-cell.edge-left::after    { left: calc(50% - 0.5px); width: calc(50% + 1.3px); }
.xiangqi-cell.edge-right::after   { width: 50%; }
/* River: no vertical line crossing through */
.xiangqi-cell.river-top::after    { height: 50%; }
.xiangqi-cell.river-bottom::after { top: 50%; height: 50%; }
.xiangqi-cell.river-top.edge-top::before { top: 50%; }

/* Palace X diagonals — ornament weight, slightly lighter than main grid */
.xq-palace {
  position: absolute;
  width: calc(2 * var(--xq-cell));
  height: calc(2 * var(--xq-cell));
  pointer-events: none;
  z-index: 0;
}
.xq-palace.top    { top: calc(0.5 * var(--xq-cell)); left: calc(3.5 * var(--xq-cell)); }
.xq-palace.bottom { top: calc(7.5 * var(--xq-cell)); left: calc(3.5 * var(--xq-cell)); }
.xq-palace::before,
.xq-palace::after {
  content: '';
  position: absolute;
  top: 50%; left: -20.71%;
  width: 141.42%; height: 1.1px;
  background: var(--xq-grid-soft);
  transform-origin: center;
}
.xq-palace::before { transform: rotate(45deg); }
.xq-palace::after  { transform: rotate(-45deg); }

/* ── Cross-mark brackets at cannon/pawn start positions (丁 ornament) ── */
.xq-mark {
  position: absolute;
  inset: 18%;
  z-index: 1;
  pointer-events: none;
  color: var(--xq-grid);
  opacity: 0.85;
}
.xiangqi-cell.edge-left  .xq-mark .m-tl,
.xiangqi-cell.edge-left  .xq-mark .m-bl { display: none; }
.xiangqi-cell.edge-right .xq-mark .m-tr,
.xiangqi-cell.edge-right .xq-mark .m-br { display: none; }

/* ── Pieces ── AI-generated carved wooden discs (Gemini 3 Pro Image).
   The container is a circle clip; the <img> inside is scaled slightly
   larger so the painted "checkerboard" border around each disc (the
   model's fake-transparency artifact) gets pushed outside the visible
   circle. Drop-shadow on the container instead of box-shadow because
   the disc art already carries its own carved/engraved relief. */
.xiangqi-piece {
  width: calc(var(--xq-cell) * 0.92);
  height: calc(var(--xq-cell) * 0.92);
  border-radius: 50%;
  overflow: hidden;
  cursor: pointer;
  position: relative;
  z-index: 2;
  transition: transform 0.28s cubic-bezier(0.25,0.85,0.35,1), box-shadow 0.15s;
  user-select: none;
  /* Subtle ground shadow so the piece reads as raised off the board. */
  box-shadow:
    0 calc(var(--xq-cell) * 0.04) calc(var(--xq-cell) * 0.07) rgba(0, 0, 0, 0.32),
    0 calc(var(--xq-cell) * 0.01) 0 rgba(0, 0, 0, 0.18);
}
.xq-piece-img {
  width: 100%;
  height: 100%;
  display: block;
  /* The AI paints a ~4-6% "transparent" checkerboard ring around each
     disc; nudge the img up by ~10% so the actual disc edge meets the
     circular clip cleanly instead of revealing that ring at the
     cardinal points. */
  transform: scale(1.10);
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}
.xiangqi-piece:hover { transform: scale(1.05); }
.xiangqi-piece:hover .xq-piece-img { filter: brightness(1.06); }

/* River banner running across the 5th row (between black + red halves).
   Pure CSS text rather than an AI image — the model bakes its own
   fake-transparency checkerboard into PNG outputs which can't be
   stripped without a real alpha channel, and 楚河 / 漢界 render
   crisply enough from the system kaishu fonts already loaded. */
.xq-river {
  position: absolute;
  left: 0; right: 0;
  top: calc(4 * var(--xq-cell));
  height: var(--xq-cell);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 calc(var(--xq-cell) * 0.9);
  pointer-events: none;
  z-index: 1;
  font-family: 'LXGW WenKai TC', 'Noto Serif SC', 'KaiTi', 'STKaiti', 'SimSun', serif;
  font-weight: 700;
  font-size: calc(var(--xq-cell) * 0.62);
  color: #7a4a1f;
  letter-spacing: calc(var(--xq-cell) * 0.08);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
  user-select: none;
}
.xiangqi-board.flipped .xq-river { transform: rotate(180deg); }

/* ── Highlights ── two-layer: soft square tint + piece emphasis ── */
.xiangqi-cell.xq-selected {
  background: radial-gradient(circle at center, var(--xq-selected) 0%, rgba(255, 213, 79, 0.18) 70%, transparent 100%);
}
.xiangqi-cell.xq-selected::before,
.xiangqi-cell.xq-selected::after { z-index: 1; }
.xiangqi-cell.xq-selected .xiangqi-piece {
  box-shadow:
    0 0 0 calc(var(--xq-cell) * 0.05) rgba(255, 213, 79, 0.85),
    0 calc(var(--xq-cell) * 0.06) calc(var(--xq-cell) * 0.12) rgba(0, 0, 0, 0.4);
}

.xiangqi-cell.xq-last-move {
  background: radial-gradient(circle at center, var(--xq-lastmove) 0%, rgba(155, 199, 0, 0.12) 70%, transparent 100%);
}
.xiangqi-cell.xq-last-move::before,
.xiangqi-cell.xq-last-move::after { z-index: 1; }

.xq-valid-dot {
  width: calc(var(--xq-cell) * 0.28);
  height: calc(var(--xq-cell) * 0.28);
  background: var(--xq-legal);
  border-radius: 50%;
  z-index: 1;
  pointer-events: none;
}
.xiangqi-cell.xq-valid-capture .xiangqi-piece {
  box-shadow:
    0 0 0 calc(var(--xq-cell) * 0.05) rgba(35, 35, 35, 0.45),
    0 calc(var(--xq-cell) * 0.045) calc(var(--xq-cell) * 0.09) rgba(0, 0, 0, 0.3);
}
.xiangqi-cell.xq-in-check .xiangqi-piece {
  box-shadow:
    0 0 0 calc(var(--xq-cell) * 0.06) rgba(239, 68, 68, 0.9),
    0 0 calc(var(--xq-cell) * 0.5) rgba(239, 68, 68, 0.55),
    0 calc(var(--xq-cell) * 0.05) calc(var(--xq-cell) * 0.12) rgba(0, 0, 0, 0.45);
  animation: xqCheckPulse 0.7s ease-in-out infinite alternate;
}
@keyframes xqCheckPulse {
  from { filter: brightness(1); }
  to   { filter: brightness(1.15); }
}

/* ══════════════════════════════════════
   GOMOKU / CARO BOARD (12 × 20 ô)
══════════════════════════════════════ */
:root {
  --caro-cell: 30px;
  --caro-line: #c8cdd4;
  --caro-bg: #f6f7f9;
  --caro-x: #2f7fff;
  --caro-o: #e63946;
}
.gomoku-board {
  --caro-cell: max(14px, min(calc((100cqw - 32px) / 12), calc((100cqh - 80px) / 20)));
  background: var(--caro-bg);
  border: 2px solid #9aa3ad;
  border-radius: 4px;
  box-shadow: var(--shadow);
  padding: 6px;
  display: inline-block;
}
.gomoku-grid {
  display: grid;
  background: var(--caro-line);
  gap: 1px;
  border: 1px solid var(--caro-line);
}
.gomoku-cell {
  background: var(--caro-bg);
  position: relative;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  user-select: none;
}
.caro-mark {
  position: relative;
  width: 60%;
  height: 60%;
  pointer-events: none;
  box-sizing: border-box;
}
.caro-x::before,
.caro-x::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: calc(var(--caro-cell) * 0.11);
  background: var(--caro-x);
  border-radius: calc(var(--caro-cell) * 0.04);
  transform-origin: center center;
}
.caro-x::before { transform: translateY(-50%) rotate(45deg); }
.caro-x::after  { transform: translateY(-50%) rotate(-45deg); }
.caro-o {
  box-sizing: border-box;
  border: calc(var(--caro-cell) * 0.11) solid var(--caro-o);
  border-radius: 50%;
  background: transparent;
}
.gomoku-cell:not(.occupied):hover { background: #e6eefb; }

/* Inline caro markers used in player color labels */
.caro-label-mark {
  display: inline-block;
  position: relative;
  width: 0.95em;
  height: 0.95em;
  vertical-align: -0.12em;
  box-sizing: border-box;
  margin-right: 3px;
}
.caro-label-x::before,
.caro-label-x::after {
  content: '';
  position: absolute;
  top: 50%; left: 0;
  width: 100%;
  height: 0.16em;
  background: #2f7fff;
  border-radius: 0.04em;
  transform-origin: center;
}
.caro-label-x::before { transform: translateY(-50%) rotate(45deg); }
.caro-label-x::after  { transform: translateY(-50%) rotate(-45deg); }
.caro-label-o {
  border: 0.14em solid #e63946;
  border-radius: 50%;
  background: transparent;
}
.gomoku-cell.last-move { background: #fff4c2; }
.gomoku-cell.last-move:hover { background: #fff4c2; }
.gomoku-cell.win-cell {
  position: relative;
  background: linear-gradient(135deg, #fff3a0 0%, #ffd43b 40%, #ff922b 60%, #ffd43b 100%);
  background-size: 300% 300%;
  box-shadow: inset 0 0 0 3px #f0b400;
  animation: winCellShine 1.6s ease-in-out infinite, winCellPulse 0.7s ease-in-out infinite alternate;
  z-index: 3;
}
.gomoku-cell.win-cell .caro-mark { z-index: 4; filter: brightness(1.2) saturate(1.3); }
.gomoku-cell.win-cell .caro-x::before,
.gomoku-cell.win-cell .caro-x::after {
  box-shadow: 0 0 14px var(--caro-x), 0 0 4px #fff;
}
.gomoku-cell.win-cell .caro-o {
  box-shadow: 0 0 14px var(--caro-o), inset 0 0 6px rgba(255,255,255,0.6);
}
@keyframes winCellShine {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}
@keyframes winCellPulse {
  from { box-shadow: inset 0 0 0 2px #f0b400, 0 0 10px rgba(255,180,0,0.6); }
  to   { box-shadow: inset 0 0 0 4px #ff6b00, 0 0 30px rgba(255,140,0,0.95); }
}

/* ══════════════════════════════════════
   OVERLAYS
══════════════════════════════════════ */
.waiting-overlay, .gameover-overlay {
  position: fixed; inset: 0;
  background: rgba(13,17,23,0.88);
  display: flex; align-items: center; justify-content: center;
  z-index: 200;
  backdrop-filter: blur(4px);
  /* Tall content (landscape phones with ~280-375px viewport height,
     plus a notch + home indicator stealing another 60-70px) can push
     the centered box past the viewport. overflow-y:auto turns the
     overlay into a scrollable container so the buttons are always
     reachable. Safe-area padding keeps the box clear of the notch
     and home-indicator zones. */
  overflow-y: auto;
  padding:
    calc(env(safe-area-inset-top, 0px) + 12px)
    env(safe-area-inset-right, 0px)
    calc(env(safe-area-inset-bottom, 0px) + 12px)
    env(safe-area-inset-left, 0px);
}
.waiting-box, .gameover-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 40px;
  text-align: center;
  max-width: 440px;
  width: 90%;
  box-shadow: var(--shadow);
}
.waiting-spinner {
  width: 48px; height: 48px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  margin: 0 auto 20px;
}
.waiting-box h2 { font-size: 1.4rem; margin-bottom: 12px; }
.waiting-box p { color: var(--text-muted); margin-bottom: 12px; }
.link-box {
  display: flex;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 12px;
}
.link-box span {
  flex: 1; padding: 10px 12px;
  font-size: 0.8rem; color: var(--text-muted);
  word-break: break-all; text-align: left;
}
.link-box button {
  background: var(--accent);
  color: white; border: none;
  padding: 0 16px;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
  transition: background 0.2s;
}
.link-box button:hover { background: var(--accent-hover); }
.room-hint { color: var(--text-muted); font-size: 0.85rem; margin: 0; }
/* Footer row inside the "waiting for opponent" overlay: a discreet
   "← Trang chủ" escape hatch on the left + the room code on the
   right. Lets the user bail without having to use the browser back
   button while the room is still spawning. */
.waiting-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 14px;
  flex-wrap: wrap;
}
.waiting-home-link {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-muted);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.10);
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.waiting-home-link:hover {
  background: rgba(255, 255, 255, 0.10);
  color: var(--text);
  border-color: rgba(255, 255, 255, 0.18);
}
[data-theme="light"] .waiting-home-link {
  background: rgba(15, 23, 42, 0.04);
  border-color: rgba(15, 23, 42, 0.12);
}
[data-theme="light"] .waiting-home-link:hover {
  background: rgba(15, 23, 42, 0.08);
  border-color: rgba(15, 23, 42, 0.20);
}

.gameover-overlay:not(.hidden) .gameover-box {
  animation: gameOverPopIn 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes gameOverPopIn {
  0% { opacity: 0; transform: scale(0.7) translateY(30px); }
  60% { opacity: 1; transform: scale(1.04) translateY(-4px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
.gameover-icon {
  font-size: 3rem;
  margin-bottom: 12px;
  display: inline-block;
  animation: iconSpinIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes iconSpinIn {
  0% { transform: scale(0) rotate(-540deg); opacity: 0; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}
.gameover-box h2 {
  font-size: 1.8rem;
  margin-bottom: 8px;
  background: linear-gradient(135deg, #ffd43b, #ff6b6b, #c084fc, #60a5fa);
  background-size: 200% auto;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  animation: statusShimmer 3s linear infinite;
}
.gameover-box p { color: var(--text-muted); margin-bottom: 24px; }
.gameover-btns { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; align-items: stretch; }
.gameover-btns .btn-primary,
.gameover-btns .btn-secondary,
.gameover-btns .btn-decline {
  padding: 0 22px;
  font-size: 0.95rem;
  min-width: 150px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  line-height: 1;
  box-sizing: border-box;
}
.gameover-btns .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.btn-decline {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 20px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 600;
  transition: background 0.15s, border-color 0.15s;
}
.btn-decline:hover { background: rgba(239, 68, 68, 0.12); border-color: var(--danger); color: var(--danger); }
.rematch-hint {
  color: #f0b400 !important;
  font-weight: 600;
  margin-bottom: 14px !important;
  font-size: 0.9rem;
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }

/* ── Promotion Dialog ── */
.promotion-dialog {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.7);
  display: flex; align-items: center; justify-content: center;
  z-index: 300;
}
.promotion-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px;
  text-align: center;
}
.promotion-box h3 { margin-bottom: 20px; font-size: 1.1rem; }
.promotion-pieces { display: flex; gap: 12px; justify-content: center; }
.promo-piece {
  width: 72px; height: 72px;
  background: var(--chess-light);
  border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: transform 0.15s, box-shadow 0.15s;
  border: 2px solid transparent;
  padding: 4px;
}
.promo-piece img { width: 100%; height: 100%; object-fit: contain; }
.promo-piece:hover { transform: scale(1.1); border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.promotion-cancel {
  margin-top: 14px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  color: var(--text-muted);
  padding: 7px 18px;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
  font-size: 0.86rem;
  transition: all 0.15s ease;
}
.promotion-cancel:hover { color: var(--text); background: rgba(255,255,255,0.08); }

/* ── Toast ── */
.toast {
  position: fixed;
  /* position:fixed ignores ancestor padding, so re-apply the iPhone
     home-indicator inset here. max() keeps the toast at 24px on
     devices with no inset and pushes it above the indicator (~34px)
     on iPhone X+ so the message never sits under the gesture bar. */
  bottom: max(24px, calc(env(safe-area-inset-bottom, 0px) + 16px));
  left: 50%;
  transform: translateX(-50%);
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 0.9rem;
  z-index: 500;
  box-shadow: var(--shadow);
  animation: fadeInUp 0.2s ease;
}
@keyframes fadeInUp {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* ── Responsive ── */
/* Compact layout for 13-inch laptops and other small viewports —
   shrink surrounding UI so the board gets as much room as possible. */
@media (max-height: 820px), (max-width: 1100px) {
  .game-header { padding: calc(5px + env(safe-area-inset-top, 0px)) 14px 5px 14px; }
  .logo-small { font-size: 0.95rem; }
  .logo-icon-sm { width: 0.81em; height: 0.81em; }
  .game-title-badge { font-size: 0.78rem; padding: 2px 10px; }
  .room-label { font-size: 0.78rem; }
  .room-code { font-size: 0.95rem; letter-spacing: 2px; }
  .btn-copy { font-size: 0.76rem; padding: 4px 10px; }
  .lang-btn { padding: 4px 10px; font-size: 0.82rem; }

  .game-main { padding: 3px 8px; }
  .game-layout { gap: 3px; }

  .player-panel { padding: 4px 10px; gap: 10px; border-radius: 8px; height: 56px; min-height: 56px; }
  .player-avatar { width: 34px; height: 34px; border-width: 2px; }
  /* Drive .player-name + .total-clock together via the panel-text
     font-size cascade so both shrink in lockstep. */
  .player-text { font-size: 0.82rem; }
  .player-color { font-size: 0.7rem; margin-top: 1px; }
  .total-clock { margin-top: 1px; }
  /* .move-timer size now scales off var(--board-w) via clamp() in the
     main rule — no need to hardcode it per breakpoint anymore. Keep
     just the letter-spacing tweak since the formula doesn't address it. */
  .move-timer { letter-spacing: 0.5px; }
  .btn-resign { font-size: 0.76rem; padding: 4px 10px; }

  .board-topbar { min-height: 22px; }
  .status-bar { font-size: 0.72rem; min-height: 18px; }
  .btn-watch { font-size: 0.72rem; padding: 3px 10px; }

  .board-wrapper { gap: 3px; }
}

@media (max-width: 600px) {
  /* Header */
  .game-header { gap: 4px; padding: calc(4px + env(safe-area-inset-top, 0px)) 8px 4px 8px; flex-wrap: nowrap; }
  .logo-small { font-size: 0.85rem; gap: 5px; }
  .logo-icon-sm { width: 0.75em; height: 0.75em; }
  .game-title-badge { display: none; }
  .room-label { display: none; }
  .room-code { font-size: 0.82rem; letter-spacing: 1px; }
  .btn-copy { font-size: 0.68rem; padding: 3px 7px; }
  .lang-btn { padding: 3px 6px; font-size: 0.75rem; gap: 4px; }
  .lang-name { display: none; }
  .lang-flag.fi { font-size: 1rem; }
  .game-header-right { gap: 6px; }
  .game-header-left { gap: 6px; }

  /* Main game area */
  .game-main { padding: 3px 6px; }
  .game-layout { gap: 3px; }
  .board-wrapper { gap: 3px; }

  /* Topbar */
  .board-topbar { min-height: 20px; gap: 6px; }
  .status-bar { font-size: 0.7rem; min-height: 16px; }
  .btn-watch { font-size: 0.68rem; padding: 2px 8px; border-radius: 12px; gap: 4px; }
  .btn-watch svg { width: 12px; height: 12px; }

  /* Player panels — ultra compact */
  .player-panel { padding: 4px 8px; gap: 8px; border-radius: 8px; height: 50px; min-height: 50px; }
  .player-info { gap: 8px; }
  .player-avatar { width: 30px; height: 30px; border-width: 1.5px; }
  .player-text { font-size: 0.78rem; }
  .player-color { font-size: 0.68rem; margin-top: 0; }
  .total-clock { margin-top: 0; }
  /* Tiny-phone letter-spacing only — font/padding/min-width now drive
     off var(--board-w) via the main .move-timer clamp() rule. */
  .move-timer { letter-spacing: 0.4px; }
  .move-timer::after { height: 2px; }
  /* .btn-resign sizing is handled by the locked-width rule near the
     desktop block — no per-breakpoint override needed here. */
  /* Captured pieces stay visible on mobile (in the new under-name row);
     just shrink the glyphs so they don't dominate the panel. */
  .captured-pieces { font-size: 0.85rem; line-height: 1; }
  .action-btns { margin-left: auto; }

  /* Boards — let container queries shrink cells further on phones */
  .gomoku-board { padding: 3px; }
  .chess-board { --chess-cell: max(28px, min(calc((100cqw - 10px) / 8), calc((100cqh - 32px) / 8))); }
  .xiangqi-board {
    --xq-cell: max(22px, min(calc((100cqw - 20px) / 10), calc((100cqh - 60px) / 12)));
    padding: calc(var(--xq-cell) * 0.35);
  }

  /* Gameover overlay */
  .gameover-box { padding: 20px 16px; width: calc(100% - 28px); max-width: 420px; }
  .gameover-btns .btn-primary,
  .gameover-btns .btn-secondary,
  .gameover-btns .btn-decline {
    min-width: 120px; height: 40px; font-size: 0.88rem; padding: 0 14px;
  }
  .score-number { font-size: 1.6rem; }

  /* Homepage tweaks */
  .join-form { flex-direction: column; }
  .hero h1 { font-size: 1.2rem; }
  .hero-sub { font-size: 0.85rem; flex-wrap: wrap; }
  .game-card { padding: 14px; }
}

/* On phones narrow enough that three 120px-min buttons would wrap into
   an ugly 2+1 grid (≤ 380px), stack the game-over actions vertically
   full-width inside the box. Full-bleed buttons give a much bigger
   touch target than two side-by-side at 120px, and avoid the orphaned
   third button sitting alone on its own row. */
@media (max-width: 380px) {
  .gameover-btns { flex-direction: column; gap: 8px; }
  .gameover-btns .btn-primary,
  .gameover-btns .btn-secondary,
  .gameover-btns .btn-decline {
    width: 100%;
    min-width: 0;
    height: 44px;
  }
}

/* Very narrow phones (≤ 360px) — last-mile tightening */
@media (max-width: 360px) {
  .game-header { padding: calc(3px + env(safe-area-inset-top, 0px)) 6px 3px 6px; }
  .logo-small { font-size: 0.8rem; }
  .player-panel { padding: 3px 6px; gap: 6px; height: 46px; min-height: 46px; }
  .player-avatar { width: 26px; height: 26px; }
  .player-text { font-size: 0.72rem; }
  .player-color { font-size: 0.64rem; }
  .status-bar { font-size: 0.66rem; }
  .btn-watch { font-size: 0.64rem; padding: 2px 6px; }
  .btn-resign { font-size: 0.64rem; padding: 2px 7px; }
}

/* === Profile v2: Bio / Privacy / Friends ================================ */

.profile-bio-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.profile-bio-count {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
.profile-bio-save {
  flex-shrink: 0;
}
/* Save feedback pills — animated check / cross with auto-fade. Targets every
   profile section's *-feedback element via a single attribute selector so all
   save flows share one motion vocabulary. */
[class$="-feedback"][class*="profile-"],
.profile-nick-feedback,
.profile-gender-feedback {
  margin-top: 6px; min-height: 16px; font-size: 0.82rem;
}
.profile-nick-feedback.ok,
.profile-gender-feedback.ok,
[class$="-feedback"][class*="profile-"].ok,
.profile-nick-feedback.err,
.profile-gender-feedback.err,
[class$="-feedback"][class*="profile-"].err {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  font-weight: 600;
  border: 1px solid transparent;
  animation: savePillIn 0.28s cubic-bezier(.2, .8, .2, 1.2);
}
.profile-nick-feedback.ok,
.profile-gender-feedback.ok,
[class$="-feedback"][class*="profile-"].ok {
  color: #6ee7b7;
  background: rgba(110, 231, 183, 0.14);
  border-color: rgba(110, 231, 183, 0.35);
}
.profile-nick-feedback.ok::before,
.profile-gender-feedback.ok::before,
[class$="-feedback"][class*="profile-"].ok::before {
  content: '✓';
  font-weight: 900;
  display: inline-block;
  animation: savePillCheck 0.45s cubic-bezier(.2, .8, .2, 1.4);
}
.profile-nick-feedback.err,
.profile-gender-feedback.err,
[class$="-feedback"][class*="profile-"].err {
  color: #fca5a5;
  background: rgba(252, 165, 165, 0.14);
  border-color: rgba(252, 165, 165, 0.35);
}
.profile-nick-feedback.err::before,
.profile-gender-feedback.err::before,
[class$="-feedback"][class*="profile-"].err::before {
  content: '✕';
  font-weight: 900;
}
.profile-nick-feedback.fading,
.profile-gender-feedback.fading,
[class$="-feedback"][class*="profile-"].fading {
  animation: savePillOut 0.5s ease forwards;
}
@keyframes savePillIn {
  0%   { opacity: 0; transform: translateY(-6px) scale(0.9); }
  60%  { transform: translateY(0) scale(1.04); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes savePillCheck {
  0%   { transform: scale(0.3); opacity: 0; }
  60%  { transform: scale(1.35); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes savePillOut {
  to { opacity: 0; transform: translateY(-3px); }
}

/* Save button — spinner during request, brief green ✓ flash on success. */
.profile-nick-save {
  position: relative;
  overflow: hidden;
  transition: background 0.18s ease, transform 0.12s ease, box-shadow 0.18s ease;
}
.profile-nick-save:active { transform: scale(0.96); }
.profile-nick-save.is-saving {
  pointer-events: none;
  color: transparent !important;
}
.profile-nick-save.is-saving::after {
  content: '';
  position: absolute;
  width: 14px; height: 14px;
  top: 50%; left: 50%;
  margin: -8px 0 0 -8px;
  border: 2px solid #07091a;
  border-top-color: transparent;
  border-radius: 50%;
  animation: saveBtnSpin 0.7s linear infinite;
}
@keyframes saveBtnSpin { to { transform: rotate(360deg); } }
.profile-nick-save.is-success {
  background: linear-gradient(135deg, #34d399, #22c55e) !important;
  color: white !important;
  box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.18);
  animation: saveBtnSuccessPop 0.6s cubic-bezier(.2, .8, .2, 1.2);
}
@keyframes saveBtnSuccessPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

.profile-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  background: rgba(7, 9, 26, 0.4);
  cursor: pointer;
  user-select: none;
  font-size: 0.88rem;
  font-weight: 500;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.profile-toggle:hover {
  background: rgba(7, 9, 26, 0.6);
  border-color: rgba(255, 255, 255, 0.16);
}
.profile-toggle input[type="checkbox"] {
  width: 18px; height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
  flex-shrink: 0;
}

/* iOS-style on/off switch — replaces the checkbox for binary settings
   (privacy, future toggles). Native checkbox stays for a11y/state but
   is visually hidden; the visible track/thumb is driven by :checked. */
.profile-switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  background: rgba(7, 9, 26, 0.4);
  cursor: pointer;
  user-select: none;
  font-size: 0.9rem;
  font-weight: 500;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.profile-switch-row:hover {
  background: rgba(7, 9, 26, 0.6);
  border-color: rgba(255, 255, 255, 0.16);
}
.profile-switch-text { flex: 1; min-width: 0; }
.profile-switch {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 44px;
  height: 24px;
}
.profile-switch input {
  /* Hide visually but keep keyboard/screen-reader access */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.profile-switch-track {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.4);
  transition: background 0.18s ease;
}
.profile-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  transition: transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.profile-switch input:checked ~ .profile-switch-track {
  background: var(--accent, #38bdf8);
}
.profile-switch input:checked ~ .profile-switch-track .profile-switch-thumb {
  transform: translateX(20px);
}
.profile-switch input:focus-visible ~ .profile-switch-track {
  box-shadow: 0 0 0 3px var(--accent-glow, rgba(56, 189, 248, 0.35));
}
.profile-privacy-hint {
  font-size: 0.78rem;
  color: var(--text-muted);
  line-height: 1.45;
  font-style: italic;
}


/* Friends */
.profile-friends-search { margin-bottom: 4px; }

/* Search input wrapper — magnifying glass icon sits inside the field. */
.friend-search-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.friend-search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  display: inline-flex;
  pointer-events: none;
}
.friend-search-input {
  padding-left: 38px !important;
}
.friend-search-input:focus + .friend-search-icon,
.friend-search-wrap:focus-within .friend-search-icon {
  color: var(--accent);
}

.friend-search-results {
  max-height: 260px;
  overflow-y: auto;
  margin-top: 8px;
  border-radius: 10px;
}
.friend-search-results:empty { display: none; }
.friends-section-title {
  font-size: 0.78rem; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.04em;
  margin: 12px 0 6px;
}
.friends-section-title:first-child { margin-top: 0; }
.friends-list {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.friend-row, .friend-search-row {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 8px;
  border-radius: 8px;
  background: var(--row-bg);
  cursor: pointer;
  transition: background 0.15s ease, transform 0.05s ease;
}
.friend-row:hover, .friend-search-row:hover { background: var(--hover-strong); }
.friend-row:active, .friend-search-row:active { transform: scale(0.995); }
/* Buttons inside the row stay non-clickable for the row-level handler — they
   handle their own actions. The cursor on the buttons is already pointer. */
.friend-row button, .friend-search-row button { cursor: pointer; }

/* Empty state — friendly icon + two-line copy, centred inside a softly
   tinted card so it reads as intentional rather than missing data. */
.friends-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 22px 12px 18px;
  margin-top: 4px;
  border-radius: 12px;
  background: rgba(56, 189, 248, 0.04);
  border: 1px dashed rgba(56, 189, 248, 0.18);
}
.friends-empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin-bottom: 10px;
  border-radius: 50%;
  color: #38bdf8;
  background: rgba(56, 189, 248, 0.10);
}
.friends-empty-title {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--text);
  margin-bottom: 4px;
}
.friends-empty-hint {
  font-size: 0.8rem;
  color: var(--text-muted);
  line-height: 1.45;
  max-width: 280px;
}

/* ── LoL-style rank badges ──────────────────────────────────────────── */

.rank-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.rank-hex {
  position: relative;
  width: 30px;
  height: 30px;
  display: inline-flex;
  filter: drop-shadow(0 0 3px var(--tier-glow));
  animation: rankPulse 2.6s ease-in-out infinite;
}
.rank-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}
.rank-name {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--tier-color);
  text-shadow: 0 0 8px var(--tier-glow);
  white-space: nowrap;
}
.rank-elo-num {
  font-size: 0.86rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* Larger version for the profile user-card top-right corner. */
.rank-badge.rank-big .rank-hex { width: 44px; height: 44px; }
.rank-badge.rank-big .rank-name { font-size: 0.88rem; }
.rank-badge.rank-big .rank-elo-num { font-size: 1.05rem; }

/* Top tiers get a stronger pulsing glow + a slow rotating sheen. */
.rank-master .rank-hex,
.rank-grandmaster .rank-hex,
.rank-challenger .rank-hex {
  animation: rankPulseStrong 2.2s ease-in-out infinite;
}
.rank-challenger .rank-hex::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--tier-glow) 0%, transparent 65%);
  opacity: 0.7;
  animation: rankSheen 3.5s linear infinite;
  pointer-events: none;
  z-index: -1;
}
/* Challenger gets a slightly brighter pulse so the bronze glow really
   reads as "élite" against the rank text colour. */
.rank-challenger .rank-hex {
  animation: rankPulseChallenger 2s ease-in-out infinite !important;
}
@keyframes rankPulseChallenger {
  0%, 100% {
    filter:
      drop-shadow(0 0 4px var(--tier-glow))
      drop-shadow(0 0 1px rgba(255, 220, 180, 0.4));
  }
  50% {
    filter:
      drop-shadow(0 0 18px var(--tier-glow))
      drop-shadow(0 0 6px rgba(255, 200, 140, 0.7));
  }
}

@keyframes rankPulse {
  0%, 100% { filter: drop-shadow(0 0 3px var(--tier-glow)); }
  50%      { filter: drop-shadow(0 0 8px var(--tier-glow)); }
}
@keyframes rankPulseStrong {
  0%, 100% { filter: drop-shadow(0 0 4px var(--tier-glow)) drop-shadow(0 0 1px rgba(255,255,255,0.3)); }
  50%      { filter: drop-shadow(0 0 14px var(--tier-glow)) drop-shadow(0 0 3px rgba(255,255,255,0.5)); }
}
@keyframes rankSheen {
  0%   { transform: rotate(0deg) scale(1); opacity: 0.4; }
  50%  { transform: rotate(180deg) scale(1.15); opacity: 0.6; }
  100% { transform: rotate(360deg) scale(1); opacity: 0.4; }
}
.friend-pic {
  width: 32px; height: 32px;
  border-radius: 50%;
  object-fit: cover;
  background: rgba(255,255,255,0.06);
}
.friend-avatar-wrap {
  position: relative;
  display: inline-flex;
  flex: none;
}
.friend-online-dot {
  position: absolute;
  /* Inset slightly so the dot sits inside the avatar circle, not clipped
     by its border-radius. Avatar is 32px → dot center lands ~8px from edge. */
  bottom: 1px; right: 1px;
  width: 11px; height: 11px;
  border-radius: 50%;
  background: #22c55e;
  border: 2px solid rgba(36, 37, 38, 1);
  box-shadow: 0 0 6px rgba(34, 197, 94, 0.55);
  animation: friend-online-pulse 2.4s ease-in-out infinite;
  /* Hard-stop the dot from being clipped by an ancestor with overflow:
     hidden — friend-row is rounded but content is not clipped. */
  z-index: 1;
}
@keyframes friend-online-pulse {
  0%, 100% { box-shadow: 0 0 4px rgba(34, 197, 94, 0.4); }
  50%      { box-shadow: 0 0 10px rgba(34, 197, 94, 0.85); }
}
.friend-name {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-weight: 500;
}
.friend-add-btn, .friend-accept, .friend-decline, .friend-remove {
  border: none; cursor: pointer;
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 0.82rem;
  font-weight: 600;
}
.friend-add-btn { background: rgba(56, 189, 248, 0.18); color: #38bdf8; display: inline-flex; align-items: center; }
.friend-add-btn:disabled { opacity: 0.6; cursor: default; }
/* Already-friends badge: green, non-clickable */
.friend-add-btn.is-friend {
  background: rgba(110, 231, 183, 0.16);
  color: #6ee7b7;
  cursor: default;
}
/* Outgoing pending: amber, non-clickable */
.friend-add-btn.is-pending {
  background: rgba(251, 191, 36, 0.14);
  color: #fbbf24;
  cursor: default;
}
/* Incoming pending: stays clickable (shows ✓ Đồng ý) */
.friend-add-btn.is-incoming {
  background: rgba(110, 231, 183, 0.18);
  color: #6ee7b7;
}
.friend-accept { background: rgba(110, 231, 183, 0.18); color: #6ee7b7; }
.friend-decline { background: rgba(252, 165, 165, 0.14); color: #fca5a5; }
.friend-remove {
  background: transparent; color: var(--text-muted);
  font-size: 1.1rem; line-height: 1;
  padding: 2px 8px;
}
.friend-remove:hover { color: #fca5a5; background: rgba(252, 165, 165, 0.12); }

.profile-loading-small {
  padding: 10px;
  color: var(--text-muted);
  font-size: 0.86rem;
  text-align: center;
}

/* History pagination + filter */
.profile-history-filter {
  display: flex; gap: 6px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.hist-filter {
  border: 1px solid rgba(255,255,255,0.08);
  background: rgba(255,255,255,0.03);
  color: var(--text-muted);
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 0.82rem;
  cursor: pointer;
  font-weight: 500;
}
.hist-filter:hover { background: rgba(255,255,255,0.07); color: var(--text); }
.hist-filter.active {
  background: rgba(56, 189, 248, 0.18);
  border-color: rgba(56, 189, 248, 0.35);
  color: #38bdf8;
}
.profile-history-more {
  display: block;
  margin: 10px auto 0;
  border: none;
  background: rgba(56, 189, 248, 0.14);
  color: #38bdf8;
  padding: 6px 16px;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  font-size: 0.86rem;
}
.profile-history-more:hover { background: rgba(56, 189, 248, 0.22); }
.profile-history-more:disabled { opacity: 0.5; cursor: default; }
.profile-history-more[hidden] { display: none; }

/* ── Mobile pass ─────────────────────────────────────────────────────────
   Final tightening for phones. Earlier breakpoints handle individual
   components; this block trims the global frame (container, header,
   hero, footer) so the page feels native on a 360–420px viewport. */
@media (max-width: 600px) {
  .container { padding: 0 12px; }
  .header { padding: 12px 0; }
  .header-tools { gap: 8px; }
  .logo { font-size: 1.2rem; }
  .logo .logo-icon { width: 24px; height: 24px; }
  .hero { padding: 14px 0 20px; }
  .hero h1 { white-space: normal; word-break: break-word; }
  .hero-sub { text-align: center; font-size: 0.78rem; margin-top: 8px; }
  .step-arrow { margin: 0 2px; }
  .games-section { padding: 0 0 22px; }
  .game-cards { gap: 12px; }
  .game-card { padding: 14px 16px; gap: 6px 16px; }
  .game-card h2 { font-size: 1.15rem; justify-self: center; text-align: center; }
  .game-card .btn-primary { padding: 7px 14px; font-size: 0.84rem; justify-self: center; }
  .game-icon { height: 4.4rem; width: 4.4rem; font-size: 2.2rem; }
  .home-lb-section { padding: 16px 0 28px; }
  .footer { padding: 14px 0; font-size: 0.78rem; }
}

@media (max-width: 380px) {
  .container { padding: 0 10px; }
  .logo span { display: none; }
  .header-tools { gap: 6px; }
  .game-card { padding: 12px 14px; }
  .game-card h2 { font-size: 1.05rem; }
  .game-icon { height: 3.6rem; width: 3.6rem; font-size: 1.9rem; }
}

/* ── Notifications bell + dropdown ───────────────────────────────────────
   Bell sits between language picker and the auth chip in the header.
   Red dot when there's at least one unread/active notification; brief
   shake animation when a fresh notification arrives. */
.notif-wrap { position: relative; display: inline-flex; align-items: center; }
.notif-bell {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: transparent;
  border: none;
  color: var(--text);
  cursor: pointer;
  position: relative;
  padding: 0;
  transition: background 0.15s ease;
}
.notif-bell:hover { background: rgba(255, 255, 255, 0.06); }
.notif-bell:focus-visible { outline: none; background: rgba(56, 189, 248, 0.12); }
.notif-bell svg { display: block; }
.notif-dot {
  position: absolute;
  top: 6px; right: 6px;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: #ef4444;
  border: 2px solid var(--bg, #07091a);
  opacity: 0;
  transform: scale(0.5);
  transition: opacity 0.18s ease, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.notif-dot.is-active { opacity: 1; transform: scale(1); }

/* Shake animation: gentle attention pulse — small angle, smooth easing,
   2 cycles. Pure transform so it stays on the compositor. */
.notif-bell.is-shaking {
  animation: bellShake 0.75s ease-in-out 2;
  transform-origin: top center;
}
@keyframes bellShake {
  0%, 100% { transform: rotate(0); }
  25% { transform: rotate(-6deg); }
  75% { transform: rotate(6deg); }
}

/* ── Notification panel (Facebook-style dropdown) ─────────────────────
   Wide rounded card, soft shadow, large clickable rows with circular
   avatar + multi-line text. The bell icon itself isn't touched here —
   only the dropdown body that opens beneath it. */
.notif-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 380px;
  max-width: calc(100vw - 16px);
  max-height: min(560px, 80vh);
  overflow: hidden;          /* outer wrapper rounds the corners */
  background: var(--card, #141b3d);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 16px;        /* all 4 corners rounded, FB-style */
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.35);
  z-index: 250;
  display: flex;
  flex-direction: column;
  animation: notifPanelOpen 0.18s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.notif-panel.hidden { display: none; }
@keyframes notifPanelOpen {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.notif-panel-head {
  padding: 16px 18px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  flex-shrink: 0;
}
.notif-panel-title {
  font-size: 1.25rem;        /* larger title like FB */
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.01em;
}
/* Scroll region — the rows scroll, the header stays. Slim FB-style
   scrollbar that only appears on hover and never grows the layout. */
.notif-panel-list {
  padding: 6px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
.notif-panel-list::-webkit-scrollbar { width: 8px; }
.notif-panel-list::-webkit-scrollbar-track { background: transparent; }
.notif-panel-list::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.10);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
.notif-panel-list:hover::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.22);
  background-clip: padding-box;
}
.notif-empty {
  padding: 36px 12px;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.95rem;
}

.notif-item {
  display: flex;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  align-items: flex-start;
  position: relative;
}
.notif-item + .notif-item { margin-top: 2px; }
.notif-item:hover { background: rgba(255, 255, 255, 0.06); }
.notif-item.notif-clickable { cursor: pointer; }
.notif-item.notif-clickable:hover { background: rgba(56, 189, 248, 0.08); }
/* Unread blue dot at the right, FB-style. Server marks isRead via
   /api/me/notifications/read-all when the dropdown opens, so the
   dot only stays visible briefly on the first render before the
   read-all flushes -- enough to draw the eye to the new item. */
.notif-item.notif-unread::after {
  content: '';
  position: absolute;
  right: 14px;
  top: 50%;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--accent, #38bdf8);
  box-shadow: 0 0 0 2px var(--card, #141b3d);
}

.notif-avatar, .notif-avatar-fallback {
  width: 56px;               /* FB-size avatar */
  height: 56px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.06);
}
.notif-avatar-fallback { background: linear-gradient(135deg, rgba(56,189,248,0.18), rgba(167,139,250,0.18)); }

.notif-icon {
  width: 56px; height: 56px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  font-size: 1.4rem;
}
.notif-icon-promo { background: rgba(251, 191, 36, 0.15); }
.notif-icon-admin { background: rgba(56, 189, 248, 0.15); }
.notif-icon-game  { background: rgba(167, 139, 250, 0.15); }

.notif-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-right: 18px; }
.notif-text { font-size: 0.92rem; color: var(--text); line-height: 1.35; }
.notif-text strong { font-weight: 700; }
.notif-time { font-size: 0.76rem; color: var(--text-muted); margin-top: 2px; }
.notif-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
/* Room code chip next to the "Vào bàn" button on game_invite_accepted
   notifications. Visually subdued so it doesn't compete with the
   primary action, but readable enough that the user can read or copy
   the code at a glance. */
.notif-room-chip {
  display: inline-flex;
  align-items: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(56, 189, 248, 0.10);
  border: 1px solid rgba(56, 189, 248, 0.25);
  color: var(--accent, #38bdf8);
  user-select: all;
}
[data-theme="light"] .notif-room-chip {
  background: rgba(14, 165, 233, 0.10);
  border-color: rgba(14, 165, 233, 0.30);
  color: #0284c7;
}
.notif-btn {
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid transparent;
  font-family: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s ease;
}
.notif-btn-primary {
  background: var(--accent, #38bdf8);
  color: #07091a;
}
.notif-btn-primary:hover { filter: brightness(1.08); }
.notif-btn-secondary {
  background: transparent;
  color: var(--text-muted);
  border-color: rgba(255, 255, 255, 0.12);
}
.notif-btn-secondary:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.2); }
.notif-btn:disabled { opacity: 0.55; cursor: default; }

/* ── Challenge picker (popover from friend row) ──────────────────────── */
.challenge-picker {
  position: absolute;
  z-index: 260;
  background: var(--card, #141b3d);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 10px;
  padding: 6px;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 160px;
  animation: notifPanelOpen 0.16s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.challenge-picker button {
  display: flex; align-items: center; gap: 10px;
  background: transparent;
  border: none;
  color: var(--text);
  font-family: inherit;
  font-size: 0.88rem;
  font-weight: 500;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
}
.challenge-picker button:hover { background: rgba(255, 255, 255, 0.06); }

/* Friend row: little crossed-swords challenge button */
.friend-challenge-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 28px;
  padding: 0 12px;
  margin-right: 6px;
  border-radius: 999px;
  background: rgba(167, 139, 250, 0.14);
  border: 1px solid rgba(167, 139, 250, 0.32);
  color: #c4b5fd;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
  transition: background 0.15s ease, transform 0.05s ease, border-color 0.15s ease;
}
.friend-challenge-btn:hover { background: rgba(167, 139, 250, 0.26); border-color: rgba(167, 139, 250, 0.5); }
.friend-challenge-btn:active { transform: scale(0.97); }
.friend-challenge-btn .fc-icon { font-size: 0.95rem; line-height: 1; }
@media (max-width: 480px) {
  .friend-challenge-btn { height: 26px; padding: 0 10px; font-size: 0.74rem; gap: 4px; }
  .friend-challenge-btn .fc-label { display: none; }
}
[data-theme="light"] .friend-challenge-btn {
  background: rgba(124, 58, 237, 0.10);
  border-color: rgba(124, 58, 237, 0.30);
  color: #6d28d9;
}
[data-theme="light"] .friend-challenge-btn:hover {
  background: rgba(124, 58, 237, 0.18);
}

@media (max-width: 600px) {
  .notif-panel { width: calc(100vw - 16px); right: -8px; }
}

/* ── Mobile scroll perf ──────────────────────────────────────────────────
   backdrop-filter and background-attachment: fixed are cheap on desktop GPUs
   but cripple scrolling on phones — every scroll frame triggers a full
   repaint of the regions behind blurred elements + the fixed background.
   On mobile we trade a little visual polish for buttery 60fps scrolling. */
@media (max-width: 768px) {
  body {
    /* Re-anchor the gradient to the document so it scrolls with content
       instead of forcing a full repaint each frame. */
    background-attachment: scroll;
  }
  .game-card,
  .home-lb-table-wrap,
  .auth-user,
  .btn-google-signin,
  .footer {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
  /* Lower-opacity sheen + radial halo on cards still works without
     the heavy blur — the card just becomes a flat translucent panel. */
  .game-card {
    background: rgba(20, 27, 61, 0.55);
  }
}

/* ── Replay viewer (replay.html) ─────────────────────────────────────── */
.replay-page main { padding: 24px 0 60px; }
.replay-loading, .replay-error {
  padding: 80px 16px;
  text-align: center;
  color: var(--text-muted);
  font-size: 1.05rem;
}
.replay-error { color: #fca5a5; }
.replay-content { display: flex; flex-direction: column; gap: 16px; }

.replay-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  padding-bottom: 12px;
}
.replay-meta { display: flex; gap: 14px; align-items: baseline; }
.replay-game-type { font-size: 1.4rem; font-weight: 700; color: var(--text); }
.replay-date { font-size: 0.85rem; color: var(--text-muted); }
.replay-result {
  font-size: 0.95rem;
  font-weight: 600;
  color: #c89045;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(200, 144, 69, 0.3);
  background: rgba(200, 144, 69, 0.08);
}

.replay-players {
  display: flex;
  align-items: center;
  justify-content: space-around;
  gap: 18px;
  background: rgba(13, 18, 48, 0.5);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 12px;
  padding: 14px 18px;
}
.replay-player { display: flex; align-items: center; gap: 10px; flex: 1 1 0; min-width: 0; }
.replay-player:last-child { justify-content: flex-end; }
.replay-player-color { font-size: 1.4rem; }
.replay-player-name {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.replay-vs { color: var(--text-muted); font-size: 0.85rem; }

.replay-board-wrap {
  display: flex;
  justify-content: center;
  padding: 8px 0;
}
.replay-board { /* per-game-type layout in children */
  max-width: 100%;
}
.rp-grid {
  display: grid;
  background: rgba(7, 9, 26, 0.6);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 8px;
  overflow: hidden;
}

/* Chess: 8x8 light/dark squares */
.rp-grid-chess {
  grid-template-columns: repeat(8, 56px);
  grid-template-rows: repeat(8, 56px);
}
.rp-sq-light { background: #f0d9b5; }
.rp-sq-dark  { background: #b58863; }
.rp-sq {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  font-size: 2.1rem;
  line-height: 1;
}
.rp-sq-from { box-shadow: inset 0 0 0 3px rgba(255, 215, 0, 0.55); }
.rp-sq-to   { box-shadow: inset 0 0 0 3px rgba(255, 215, 0, 0.85); }
.rp-piece-white { color: #ffffff; text-shadow: 0 0 1px #000, 0 1px 0 #000; }
.rp-piece-black { color: #1a1a1a; text-shadow: 0 0 1px #fff; }

/* Xiangqi: 9x10 grid, river highlighted */
.rp-grid-xiangqi {
  grid-template-columns: repeat(9, 50px);
  grid-template-rows: repeat(10, 50px);
  background: #f4e0a4;
}
.rp-sq-xq {
  border: 1px solid rgba(0, 0, 0, 0.2);
  font-size: 1.6rem;
  font-weight: 700;
}
.rp-sq-river { background: rgba(60, 158, 222, 0.10); }
.rp-piece-xq.rp-piece-red   { color: #c0392b; }
.rp-piece-xq.rp-piece-black { color: #2c3e50; }

/* Gomoku: 12 cols × 20 rows */
.rp-grid-gomoku {
  grid-template-rows: repeat(20, 28px);
  background: #c89045;
  gap: 0;
}
.rp-grid-gomoku .rp-sq {
  width: 28px; height: 28px;
  border: 1px solid rgba(0, 0, 0, 0.25);
}
.rp-stone {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: block;
}
.rp-stone-black { background: radial-gradient(circle at 35% 35%, #555, #000); box-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.rp-stone-white { background: radial-gradient(circle at 35% 35%, #fff, #ccc); box-shadow: 0 1px 2px rgba(0,0,0,0.4); }

/* Controls */
.replay-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}
.replay-btn {
  width: 44px; height: 44px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.12);
  background: rgba(20, 27, 61, 0.55);
  color: var(--text);
  font-size: 1.1rem;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.15s ease;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  font-family: inherit;
}
#rpPrev, #rpNext { font-size: 1.7rem; padding-bottom: 2px; }
.replay-btn:hover { background: rgba(56, 189, 248, 0.16); border-color: rgba(56, 189, 248, 0.45); }
.replay-btn-play { background: rgba(56, 189, 248, 0.18); border-color: rgba(56, 189, 248, 0.5); }
.replay-progress {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text-muted);
  min-width: 80px;
  text-align: center;
}
.replay-speed {
  background: rgba(20, 27, 61, 0.55);
  color: var(--text);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 0.9rem;
  cursor: pointer;
}
.replay-slider {
  width: 100%;
  appearance: none;
  height: 6px;
  border-radius: 999px;
  background: rgba(255,255,255,0.12);
  outline: none;
  cursor: pointer;
}
.replay-slider::-webkit-slider-thumb {
  appearance: none;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #38bdf8;
  border: 2px solid #fff;
  cursor: pointer;
}
.replay-slider::-moz-range-thumb {
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #38bdf8;
  border: 2px solid #fff;
  cursor: pointer;
}

.replay-movelist-wrap {
  background: rgba(13, 18, 48, 0.5);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 12px;
  padding: 14px 18px;
  max-height: 280px;
  overflow-y: auto;
}
.replay-movelist-title { margin: 0 0 10px; font-size: 1rem; color: var(--text); }
.replay-movelist {
  margin: 0; padding: 0; list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 4px;
}
.rp-move {
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  color: var(--text);
  transition: background 0.12s;
  display: flex;
  justify-content: space-between;
  gap: 8px;
}
.rp-move:hover { background: rgba(56, 189, 248, 0.12); }
.rp-move-current {
  background: rgba(200, 144, 69, 0.2);
  color: #f6e3c0;
  font-weight: 600;
}
.rp-move-ts { color: var(--text-muted); font-size: 0.78rem; }

@media (max-width: 600px) {
  .rp-grid-chess { grid-template-columns: repeat(8, 38px); grid-template-rows: repeat(8, 38px); }
  .rp-grid-chess .rp-sq { font-size: 1.5rem; }
  .rp-grid-xiangqi { grid-template-columns: repeat(9, 34px); grid-template-rows: repeat(10, 34px); }
  .rp-grid-xiangqi .rp-sq { font-size: 1.05rem; }
  .rp-grid-gomoku .rp-sq { width: 22px; height: 22px; }
  .rp-grid-gomoku { grid-template-rows: repeat(20, 22px); }
  .rp-stone { width: 16px; height: 16px; }
  .replay-btn { width: 40px; height: 40px; }
  .replay-game-type { font-size: 1.2rem; }
}

/* ── Light-mode overrides ────────────────────────────────────────────────
   Most components already render through theme tokens (--card, --surface,
   --text, --border, --hover, --row-bg, etc.) so they flip automatically.
   The block below targets the hand-tuned dark surfaces that hard-coded
   their colors and need explicit values in light mode (game cards, the
   leaderboard table, profile separators, notification dropdown, etc.). */
[data-theme="light"] body {
  /* Slightly lift the page so cards read as elevated surfaces. */
  background:
    radial-gradient(ellipse at top left, var(--bg-grad-1), transparent 55%),
    radial-gradient(ellipse at bottom right, var(--bg-grad-2), transparent 60%),
    var(--bg);
  color: var(--text);
}

/* Header + game-header surface needs more contrast in light mode so it
   doesn't disappear into the page. We add a soft shadow under it instead
   of relying on a tinted dark background. */
[data-theme="light"] .header,
[data-theme="light"] .game-header {
  background: rgba(255, 255, 255, 0.85);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
[data-theme="light"] .header-link,
[data-theme="light"] .btn-google-signin {
  color: var(--text);
}

/* Auth chip: dark-navy translucent background was inherited from dark
   mode, which left the user's name unreadable on light. Re-skin to a
   white pill with the standard light border + visible text. */
[data-theme="light"] .auth-user {
  background: #ffffff;
  border-color: var(--border);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
[data-theme="light"] .auth-user:hover {
  background: #f5f8ff;
  border-color: rgba(14, 165, 233, 0.45);
}
[data-theme="light"] .auth-user-name { color: var(--text); }
[data-theme="light"] .auth-user-caret { color: var(--text-muted); }
[data-theme="light"] .auth-user img {
  background: var(--border);
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.08);
}

/* Logo: the dark-mode gradient + transparent text-fill produces an
   invisible "Chesslizar" wordmark on a white header (the pale-blue
   colors blend with the page bg, and -webkit-text-fill-color: transparent
   doesn't fall back gracefully). Force a solid deep-navy fill so the
   wordmark is unmissable, and disable the cyan drop-shadow halo that
   would otherwise smear it. */
[data-theme="light"] .logo {
  filter: none;
}
[data-theme="light"] .logo span {
  background: none;
  -webkit-text-fill-color: #0b3a66;
  color: #0b3a66;
}

/* Online badge: dark-mode used #7dc6f0 + #e0f2fe (light blue + near-
   white) which both vanish on a white surface. Switch the count + label
   to deep slate-blue so 77 / Online stay readable. */
[data-theme="light"] .header-online { color: #0369a1; }
[data-theme="light"] .header-online strong { color: #0b3a66; }
[data-theme="light"] .online-dot {
  background: #0284c7;
  box-shadow: 0 0 0 0 rgba(2, 132, 199, 0.45);
}

/* Game cards on the home page were a translucent dark navy. Re-tint them
   white and give the icon halos enough lift to stay visible. */
[data-theme="light"] .game-card {
  background: rgba(255, 255, 255, 0.92);
  border-color: var(--border);
  box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
[data-theme="light"] .game-card:hover {
  background: #fff;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.10);
}

/* Drop the grey halo behind the icon column — it was reading as a
   washed-out background blob. The icon glyphs themselves carry their
   own chrome. Also drop the chess-piece text-shadow (a cream halo that
   makes the silhouette look fuzzy on a white card). */
[data-theme="light"] .game-icon { background: transparent; }
[data-theme="light"] .chess-icon {
  color: #0f172a;
  text-shadow: none;
}
[data-theme="light"] .xiangqi-icon { color: #1e293b; }
[data-theme="light"] .gomoku-icon span { color: #1e293b; }

/* Hero subhead + step arrows */
[data-theme="light"] .hero-sub { color: var(--text-muted); }

/* Home leaderboard table — the table-wrap had a dark glassy bg in dark
   mode; switch it to a clean white card with a thin border. Row hovers
   reuse the global --hover-strong token so they actually show up. */
[data-theme="light"] .home-lb-table-wrap {
  background: #fff;
  border: 1px solid var(--border);
  box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
}
[data-theme="light"] .home-lb-table thead th {
  color: var(--text-muted);
  background: #f7faff;
  border-bottom: 1px solid var(--border);
}
[data-theme="light"] .home-lb-table tbody tr {
  border-bottom: 1px solid rgba(15, 23, 42, 0.05);
}
[data-theme="light"] .hlb-row.is-interactive:hover { background: var(--hover); }
/* Light mode: kill the 1px black ring around every country-flag span.
   The ring reads as a soft grey shadow halo on a white background. */
[data-theme="light"] .lang-flag.fi,
[data-theme="light"] .fbp-country .fi,
[data-theme="light"] .profile-country .fi,
[data-theme="light"] .hlb-country .fi,
[data-theme="light"] .lb-country .fi {
  box-shadow: none;
}

/* Leaderboard segmented tabs: dark-mode track was rgba(7,9,26,0.55)
   which on a white page reads as a heavy dark blob. Re-skin to a
   subtle slate fill with the standard light border, and brighten the
   inactive tab text so "Cờ Tướng / Cờ Caro" are legible. The active
   tab keeps the gold gradient + dark text (already high contrast). */
[data-theme="light"] .home-lb-segmented {
  background: rgba(15, 23, 42, 0.05);
  border-color: var(--border);
}
[data-theme="light"] .lb-seg {
  color: var(--text);
  border-color: var(--border);
}
[data-theme="light"] .lb-seg:hover { color: var(--accent-hover); }

/* Profile modal — the dark sheet behind the card already uses rgba()
   directly. Make the card itself white and convert the section dividers
   from white-alpha (invisible on white) to dark-alpha. The actual sheet
   class is .profile-modal (not .profile-card); both are listed for
   defensive coverage in case the class moves. */
[data-theme="light"] .profile-overlay { background: rgba(15, 23, 42, 0.45); }
[data-theme="light"] .profile-modal,
[data-theme="light"] .profile-card {
  background: #fff;
  border-color: var(--border);
  box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
  color: var(--text);
}
[data-theme="light"] .profile-close {
  background: rgba(15, 23, 42, 0.05);
  border-color: var(--border);
  color: var(--text-muted);
}
[data-theme="light"] .profile-close:hover {
  background: rgba(15, 23, 42, 0.10);
  color: var(--text);
  border-color: rgba(15, 23, 42, 0.18);
}

/* Achievement / ranking rows inside the profile (Cờ Vua / Cờ Tướng /
   Cờ Caro) had a dark navy fill that read as a heavy grey strip on
   light backgrounds. Re-tint to a soft slate fill that matches the
   rest of the light-mode panels. */
[data-theme="light"] .rk-row {
  background: rgba(15, 23, 42, 0.04);
  border-color: var(--border);
}
[data-theme="light"] .rk-row:hover {
  background: rgba(15, 23, 42, 0.08);
  border-color: rgba(200, 144, 69, 0.35);
}
[data-theme="light"] .fbp-card { background: #fff; border: 1px solid var(--border); }
[data-theme="light"] .fbp-row,
[data-theme="light"] .fbp-row-body { border-bottom-color: rgba(15, 23, 42, 0.06); }
[data-theme="light"] .fbp-user-card { background: linear-gradient(180deg, #f7faff, #fff); }
[data-theme="light"] .fbp-stats-card { background: #f7faff; border: 1px solid var(--border); }
[data-theme="light"] .fbp-bio-text { color: var(--text); }
[data-theme="light"] .fbp-name-row .fbp-name { color: var(--text); }

/* Notifications dropdown */
[data-theme="light"] .notif-panel {
  background: #fff;
  border: 1px solid var(--border);
  box-shadow: 0 12px 36px rgba(15, 23, 42, 0.14);
}
[data-theme="light"] .notif-item { border-bottom-color: rgba(15, 23, 42, 0.06); }
[data-theme="light"] .notif-item:hover { background: var(--hover); }
[data-theme="light"] .notif-item.notif-unread { background: rgba(14, 165, 233, 0.06); }

/* Confirm dialog */
[data-theme="light"] .cl-confirm-overlay { background: rgba(15, 23, 42, 0.55); }
[data-theme="light"] .cl-confirm-box { background: #fff; border-color: var(--border); }

/* Language picker — flip the menu to a white surface */
[data-theme="light"] .lang-menu {
  background: #fff;
  border: 1px solid var(--border);
  box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
}
[data-theme="light"] .lang-option:hover { background: var(--hover); }
[data-theme="light"] .lang-selector.in-footer .lang-btn {
  background: rgba(15, 23, 42, 0.04) !important;
}
[data-theme="light"] .lang-selector.in-footer .lang-btn:hover {
  background: rgba(15, 23, 42, 0.08) !important;
}

/* Light-mode overrides for the theme toggle live alongside the main
   .theme-toggle rule further down so they stay in sync; nothing to add
   here. */

/* Game page panels (player cards, captured pieces, action buttons) */
[data-theme="light"] .player-panel {
  background: #fff;
  border: 1px solid var(--border);
}
/* .move-timer light-mode override lives next to the main rule above so
   the tournament-clock dark body stays consistent in both themes. */
[data-theme="light"] .total-clock {
  color: #0f172a;
  background: rgba(15, 23, 42, 0.07);
  border-color: rgba(15, 23, 42, 0.18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
[data-theme="light"] .total-clock.warning {
  color: #fff;
  background: #dc2626;
  border-color: #b91c1c;
}

/* Replay viewer */
[data-theme="light"] .replay-content { color: var(--text); }
[data-theme="light"] .replay-board-wrap { background: #fff; border: 1px solid var(--border); }
[data-theme="light"] .replay-btn {
  background: rgba(15, 23, 42, 0.05);
  color: var(--text);
}
[data-theme="light"] .replay-btn:hover { background: rgba(15, 23, 42, 0.10); }
[data-theme="light"] .replay-movelist li { border-bottom-color: rgba(15, 23, 42, 0.06); }

/* ── Theme toggle (sliding outline switch) ─────────────────────────────
   A pill-shaped track with two outline icons (sun on the left, moon on
   the right) and a round thumb that slides to the matching side. All
   strokes use currentColor so the switch reads as a thin monochrome
   sketch in BOTH themes — no yellow sun, no grey moon, no accent fill.

   Layout:                 thumb covers the side that matches the active
                            theme; the OPPOSITE icon stays visible on
                            the other side as the destination hint.
     Light mode →   [ ☀●  ☾ ]        thumb left, sun under thumb
     Dark mode  →   [ ☀  ●☾ ]        thumb right, moon under thumb
*/
.theme-toggle {
  /* Compact pill — ~1/3 of the lang pill width. Wide enough to show the
     slider thumb sliding from one side to the other; short enough that
     it reads as a tight icon-switch next to the longer language pill.
     The static side-icons (sun/moon hints) are hidden at this width
     since they no longer fit — the thumb itself swaps icons based on
     the active theme, so the affordance still reads. */
  --track-w: 48px;
  --track-h: 32px;
  --thumb: 24px;
  --pad: 3px;
  position: relative;
  width: var(--track-w);
  height: var(--track-h);
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
  box-sizing: border-box;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.theme-toggle:hover { background: rgba(255, 255, 255, 0.10); color: var(--text); }
[data-theme="light"] .theme-toggle { background: rgba(15, 23, 42, 0.04); }
[data-theme="light"] .theme-toggle:hover { background: rgba(15, 23, 42, 0.08); color: var(--text); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Static side icons sitting flush in each end of the track. They are
   just visual hints — the thumb actually moves between them. */
.theme-toggle-icon {
  position: absolute;
  top: 0;
  height: 100%;
  width: var(--thumb);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0.55;
}
.theme-toggle-icon-sun  { left: var(--pad); }
.theme-toggle-icon-moon { right: var(--pad); }
/* The static side-icon hints (sun on the left edge, moon on the right
   edge) only fit when the chassis is wide. At the new compact 48px
   width the thumb itself fills most of the track, leaving no room for
   them, so we just hide them — the thumb shows the active glyph. */
.theme-toggle .theme-toggle-icon { display: none; }

/* The sliding thumb itself — outline-only, transparent fill, contains
   one icon that swaps between sun (light mode) and moon (dark mode).
   Translation distance = track width − thumb − 2× padding. */
.theme-toggle-thumb {
  position: absolute;
  top: 50%;
  left: var(--pad);
  width: var(--thumb);
  height: var(--thumb);
  margin-top: calc(var(--thumb) / -2);
  border-radius: 50%;
  border: 1px solid currentColor;
  background: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
[data-theme="dark"] .theme-toggle-thumb {
  transform: translateX(calc(var(--track-w) - var(--thumb) - var(--pad) * 2 - 2px));
}
[data-theme="light"] .theme-toggle-thumb { transform: translateX(0); }

/* The thumb-icon container holds BOTH SVGs stacked; only the active
   theme's icon is visible. Keeps the markup symmetric so swapping
   themes never relayouts. */
.theme-toggle-thumb-icon {
  position: relative;
  width: 14px;
  height: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.theme-toggle-thumb-icon svg { position: absolute; width: 14px; height: 14px; opacity: 0; transition: opacity 0.15s ease; }
[data-theme="light"] .theme-toggle-thumb-icon svg:first-child { opacity: 1; }   /* sun */
[data-theme="dark"]  .theme-toggle-thumb-icon svg:last-child  { opacity: 1; }   /* moon */

@media (max-width: 600px) {
  /* Stay 32px tall on mobile so the pair sits on one baseline. Mobile
     keeps the same compact 48px chassis as desktop — the toggle is
     already as small as it can be while still reading as a switch. */
  .theme-toggle {
    --track-w: 44px;
    --track-h: 30px;
    --thumb: 22px;
  }
  .theme-toggle-thumb-icon { width: 11px; height: 11px; }
  .theme-toggle-thumb-icon svg { width: 11px; height: 11px; }
}

/* Shared wrapper used on the game page — the lang picker and theme
   toggle both sit inside one floating pill anchored to bottom-right.
   They are rendered as floating-pill variants of their normal inline
   markup, so we just need a fixed-position container. */
.footer-floating {
  position: fixed;
  /* Same iPhone home-indicator + landscape side-notch protection as
     .toast. max() degrades to the original offsets on devices without
     a non-zero safe-area-inset. */
  bottom: max(10px, calc(env(safe-area-inset-bottom, 0px) + 4px));
  right: max(12px, calc(env(safe-area-inset-right, 0px) + 6px));
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 8px;
  line-height: 0;
}
.footer-floating > * { display: inline-flex; align-items: center; }
@media (max-width: 600px) {
  .footer-floating {
    bottom: max(8px, calc(env(safe-area-inset-bottom, 0px) + 4px));
    right: max(8px, calc(env(safe-area-inset-right, 0px) + 4px));
    gap: 6px;
  }
}

/* ── "Ván mới bắt đầu" banner — animated overlay centered over the
   board after a successful rematch. The board itself stays interactive
   underneath; the banner just floats in for ~1.5s, then fades out. */
.new-game-banner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 30;
  opacity: 0;
  transition: opacity 0.32s ease;
}
.new-game-banner.show { opacity: 1; }
.new-game-banner-inner {
  display: inline-flex;
  align-items: center;
  gap: clamp(8px, calc(var(--board-w, 600px) * 0.022), 18px);
  padding: clamp(8px, calc(var(--board-w, 600px) * 0.022), 18px)
           clamp(14px, calc(var(--board-w, 600px) * 0.04), 32px);
  background: linear-gradient(135deg,
    rgba(15, 23, 42, 0.78) 0%,
    rgba(30, 41, 59, 0.78) 100%);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 22px;
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow:
    0 12px 48px rgba(0, 0, 0, 0.45),
    0 0 60px rgba(124, 92, 248, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.08);
  transform: scale(0.78) translateY(8px);
  transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}
.new-game-banner.show .new-game-banner-inner {
  transform: scale(1) translateY(0);
}
.new-game-banner-text {
  font-size: clamp(0.9rem, calc(var(--board-w, 600px) * 0.045), 1.8rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  white-space: nowrap;
  background: linear-gradient(100deg,
    #ff2bd6 0%,
    #ff5ab0 18%,
    #c04af0 38%,
    #7a5af8 58%,
    #3b82f6 78%,
    #22d3ee 100%);
  background-size: 220% auto;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  filter: drop-shadow(0 0 14px rgba(255, 64, 192, 0.35))
          drop-shadow(0 0 22px rgba(61, 130, 246, 0.30));
  animation: ngbShimmer 2.4s ease-in-out infinite alternate;
}
@keyframes ngbShimmer {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}
.new-game-banner-spark {
  font-size: clamp(0.75rem, calc(var(--board-w, 600px) * 0.035), 1.4rem);
  color: #f5d479;
  filter: drop-shadow(0 0 8px rgba(255, 212, 121, 0.55));
  animation: ngbSparkle 1.6s ease-in-out infinite alternate;
}
.new-game-banner-spark:last-child { animation-delay: 0.4s; }
@keyframes ngbSparkle {
  from { transform: rotate(-8deg) scale(0.92); opacity: 0.75; }
  to   { transform: rotate(8deg)  scale(1.08); opacity: 1; }
}
[data-theme="light"] .new-game-banner-inner {
  background: linear-gradient(135deg,
    rgba(255, 255, 255, 0.92) 0%,
    rgba(247, 250, 255, 0.92) 100%);
  border-color: rgba(15, 23, 42, 0.10);
  box-shadow:
    0 12px 48px rgba(15, 23, 42, 0.18),
    0 0 60px rgba(124, 92, 248, 0.20),
    inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
@media (max-width: 600px) {
  .new-game-banner-inner {
    gap: 12px;
    padding: 14px 20px;
    border-radius: 18px;
  }
}
