/* ── Reset & Variables ── */
/* Design tokens.
   Paper-and-ink identity: a warm off-white ground, ink-brown text, seal red
   as the single accent, gold and jade as the only other hues. Boards are the
   loudest thing on any page, so everything around them is kept quiet — that
   is the whole idea, and it is why nothing here is a saturated UI blue.

   Light is the default because the lobby, the leaderboard and the profile
   are reading surfaces. Dark is a real second theme, not an inversion: the
   ground goes to lacquer-black-green and the accent shifts to a lighter red
   that survives on it.

   Component CSS must reach for these tokens, never a literal — a colour
   that only exists inside one theme block is the classic unreadable-page
   bug. */
:root, [data-theme="light"] {
  /* Paper — the ground and every raised surface on it. */
  --paper-0:   #fffdf8;
  --paper-50:  #fbf6eb;
  --paper-100: #f5ecdc;
  --paper-200: #ebdcc4;

  /* Ink — text, from headline down to the faintest caption. */
  --ink-950: #201b17;
  --ink-900: #2e2721;
  --ink-800: #40372f;
  --ink-700: #554a41;
  --ink-600: #6b5f55;
  --ink-500: #82756a;
  --ink-400: #9b8e82;
  --ink-300: #b8aca1;

  /* Seal red — the accent. Spent on one thing per view. */
  --red-50:  #fff1ee;
  --red-100: #f7d8d2;
  --red-300: #d98c83;
  --red-600: #bd4439;
  --red-700: #a6332b;
  --red-800: #87271f;
  --red-900: #651e19;
  --on-red:  #fffaf0;

  /* Gold — rank, honours, the glow behind the hero. Never body text. */
  --gold-50:  #fcf7e9;
  --gold-100: #f7ebcb;
  --gold-200: #ecd49a;
  --gold-300: #dfbd73;
  --gold-500: #c9963e;
  --gold-600: #aa7729;
  --gold-700: #855a20;

  /* Jade — semantic "good / your turn / online". Separate from the accent. */
  --jade-50:  #eef7f2;
  --jade-100: #dcece5;
  --jade-200: #bfd9cd;
  --jade-600: #35705d;
  --jade-700: #285847;
  --jade-800: #1d4437;

  /* Mapped to the names the rest of the stylesheet already uses. */
  --bg: var(--paper-100);
  --surface: var(--paper-50);
  --card: var(--paper-0);
  --border: rgba(71, 53, 39, 0.14);
  --border-strong: rgba(71, 53, 39, 0.28);
  --accent: var(--red-700);
  --accent-hover: var(--red-800);
  --accent2: var(--gold-500);
  --accent-glow: rgba(166, 51, 43, 0.20);
  --accent2-glow: rgba(201, 150, 62, 0.22);
  --text: var(--ink-950);
  --text-muted: var(--ink-600);
  --danger: var(--red-600);
  --ok: var(--jade-600);
  --hover: rgba(71, 53, 39, 0.05);
  --hover-strong: rgba(71, 53, 39, 0.09);
  --row-bg: rgba(71, 53, 39, 0.028);
  --bg-grad-1: rgba(201, 150, 62, 0.16);
  --bg-grad-2: rgba(166, 51, 43, 0.06);
  /* The base the body gradient starts from, and the header's veil. Both
     used to reference paper tokens directly, which leaked light paper
     into dark mode — they must swap with the theme. */
  --body-base: var(--paper-50);
  --header-veil: color-mix(in srgb, var(--paper-0) 90%, transparent);
  /* Chip = recessed track/tag surfaces (segmented controls, status tags);
     thumb = the raised element sitting on a chip. Referencing paper-*
     directly leaked light surfaces into dark mode. */
  /* Label colour for a control filled with --accent. */
  --on-accent: var(--on-red);
  /* "Someone is watching" blue — see .btn-watch.has-viewers. Deeper than
     the dark theme's sky so it still carries on paper. */
  --watch-blue: #0284c7;
  /* Colour of the two rules flanking the hero kicker. */
  --eyebrow-rule: var(--gold-500);
  --chip-bg: var(--paper-100);
  --chip-bg-strong: var(--paper-200);
  --thumb-bg: var(--paper-0);

  /* Boards keep their own wood palette in both themes — a chessboard does
     not change colour when the room lights go off. */
  --board-face-light: #e2bc82;
  --board-face: #d3a162;
  --board-face-deep: #b97843;
  --board-edge: #7a4828;
  --board-edge-deep: #4f2b1a;
  --board-line: #5b351f;
  --chess-light: #efe4cd;
  --chess-dark: #7d9b62;
  --chess-selected: rgba(201, 150, 62, 0.30);
  --chess-move: rgba(53, 112, 93, 0.24);
  --board-wood: var(--board-face);

  /* The dark shell a board sits in, on any theme. */
  --stage-950: #101512;
  --stage-900: #17201c;
  --stage-850: #202a24;
  --stage-800: #28332d;
  --stage-text: #f4ebdd;
  --stage-text-muted: #c7b9a6;
  --stage-edge: rgba(218, 180, 103, 0.24);

  /* Shape, depth, motion. */
  --radius-sm: 0.5rem;
  --radius-md: 0.75rem;
  --radius-lg: 1rem;
  --radius-xl: 1.5rem;
  --radius-pill: 999px;
  --radius: var(--radius-md);
  --shadow-sm: 0 8px 22px rgba(60, 40, 24, 0.08);
  --shadow-lg: 0 28px 65px rgba(52, 34, 20, 0.18);
  --shadow-xl: 0 35px 90px rgba(45, 29, 18, 0.28);
  --shadow-red: 0 10px 30px rgba(166, 51, 43, 0.20);
  --shadow: var(--shadow-sm);
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* Type. Inter carries the interface; Black Ops One is reserved for the
     two or three places a match wants to feel like a match; the Kai face is
     for CJK piece glyphs only. */
  --font-ui: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-battle: "Black Ops One", Impact, var(--font-ui);
  --tracking-heading: -0.025em;
  --tracking-label: 0.14em;
  --weight-heading: 760;
  --leading-heading: 1.08;
  --leading-body: 1.65;
  --shell-width: 1080px;

  color-scheme: light;
}

/* Dark: the original Chess Lizar night — deep navy ground, cyan + violet
   accents, the classic green chess board. Transplanted from the
   lizar-chess monolith (the pre-split dark theme) at the owner's request;
   the cosmos/starfield experiment was retired. Light mode stays the paper
   design and is untouched. */
:root[data-theme="dark"] {
  --bg: #07091a;
  --surface: #0d1230;
  --card: #141b3d;
  --border: #2a325d;
  --border-strong: #3b4677;
  --accent: #38bdf8;
  /* Same value as --accent here on purpose: in the night theme the room
     code and the watcher badge are literally the same blue. */
  --watch-blue: #38bdf8;
  --eyebrow-rule: var(--accent);
  --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;
  --ok: #34d399;
  --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);
  --shadow-sm: 0 8px 32px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 28px 65px rgba(0, 0, 0, 0.5);
  --shadow-xl: 0 35px 90px rgba(0, 0, 0, 0.55);
  --shadow-red: 0 10px 30px rgba(56, 189, 248, 0.25);
  --shadow: var(--shadow-sm);
  --body-base: var(--surface);
  --header-veil: color-mix(in srgb, #0d1230 90%, transparent);
  --on-accent: #07091a;
  --chip-bg: rgba(255, 255, 255, 0.06);
  --chip-bg-strong: rgba(255, 255, 255, 0.10);
  --thumb-bg: #1b2350;
  /* The old night theme's boards: chess.com-green squares and warm wood
     xiangqi lines. Light mode keeps its own board palette below. */
  --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;
  color-scheme: dark;
}

* { 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: var(--font-ui);
  /* One warm light source, high on the right, over paper. Two opposing
     corner glows read as a gradient mesh; one reads as a room. */
  background:
    radial-gradient(circle at 78% 12%, var(--bg-grad-1), transparent 460px),
    radial-gradient(circle at 8% 88%, var(--bg-grad-2), transparent 520px),
    linear-gradient(135deg, var(--body-base) 0%, var(--bg) 100%);
  background-attachment: scroll;
  font-size: 16px;
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  /* 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: 14px 0;
  border-bottom: 1px solid var(--border);
  /* Translucent so the page's warm light shows through as you scroll under
     it, with a shadow instead of a hard rule doing the separating. */
  background: var(--header-veil);
  backdrop-filter: saturate(1.4) blur(10px);
  box-shadow: 0 8px 26px rgba(58, 41, 27, 0.05);
  position: sticky;
  top: 0;
  z-index: 40;
}
.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 {
  /* One flat colour, not a gradient (owner 2026-09-15). The old
     #e2f4ff → #d98c83 → #a6332b ramp was drawn for the dark theme and
     read as a washed smear on the paper background. --accent keeps the
     wordmark on-brand in both themes: seal red on light, sky on dark. */
  color: var(--accent);
  line-height: 1;
}
/* The new emblem (crown + knight + 將 + caro grid, 2026-09-14) carries far
   more detail than the old monogram coin, so it needs a bigger box to read
   as anything at header size — at 1.06em it was a smudge. */
.logo-icon {
  width: 1.45em;
  height: 1.45em;
  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: var(--chip-bg) !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: var(--hover) !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: var(--chip-bg-strong) !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: 44px 0 34px;
  text-align: center;
}
.hero .container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
/* Headline set in uniform text color for both light and dark mode.
   line-height 1.25 gives room for Vietnamese diacritics and descenders. */
.hero h1 {
  font-size: clamp(1.9rem, 4.4vw, 3.05rem);
  font-weight: var(--weight-heading);
  line-height: 1.25;
  letter-spacing: var(--tracking-heading);
  word-spacing: 0.035em;
  color: var(--text);
  text-wrap: balance;
  max-width: 30ch;
  margin-inline: auto;
}
.hero h1 em,
.hero h1 .accent {
  font-style: normal;
  color: inherit;
}
/* The line above the headline names the product category — small, spaced,
   in the accent, the way a masthead kicker is set. */
.hero-eyebrow {
  font-size: 0.72rem;
  font-weight: 850;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--accent);
  /* Flanking rules so the masthead line reads deliberate. They used to
     be a light-theme-only flourish; the owner wants them in the night
     theme too (2026-09-16: "thêm 2 cái line này cho darkmode"). The
     colour is a token — gold beside the red kicker on paper, the cyan
     accent beside the cyan kicker at night. */
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.hero-eyebrow::before,
.hero-eyebrow::after {
  content: '';
  width: 34px;
  height: 1.5px;
  border-radius: 1px;
  background: linear-gradient(90deg, transparent, var(--eyebrow-rule));
}
.hero-eyebrow::after {
  background: linear-gradient(90deg, var(--eyebrow-rule), transparent);
}
.hero-sub {
  color: var(--text-muted);
  font-size: 0.9rem;
  letter-spacing: 0.2px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
}
.step-arrow { color: var(--accent); margin: 0 4px; font-weight: 700; }

/* ── Game Cards ── */
.games-section { padding: 0 0 32px; }
/* "1. Tạo phòng → 2. Gửi link → 3. Bắt đầu chơi ngay", centred under the cards. */
.how-to-steps { display: flex; justify-content: center; margin-top: 26px; }
.how-to-steps .hero-sub { margin: 0; }
.game-cards {
  display: grid;
  /* Fixed three-up on desktop, one-up below 900px. auto-fit used to land
     on two columns between roughly 560 and 960px, which left the third
     card alone on its own row — the layout looked broken rather than
     responsive. */
  grid-template-columns: repeat(3, 1fr);
  gap: 18px;
}
@media (max-width: 900px) {
  /* One column, but capped — a 900px-wide row tile leaves a canyon between
     the game name and its button. 560px keeps the name, the icon and the
     CTA in one comfortable reading group at every tablet width. */
  .game-cards { grid-template-columns: 1fr; gap: 12px; max-width: 560px; margin-inline: auto; }
}
/* Apple-style Liquid Glass card — horizontal compact layout, ~80% the
   height of the original square cards so the leaderboard below has more
   vertical room. */
/* A raised sheet of paper, not a pane of glass. Frosted glass over a light
   ground has nothing to frost, and the per-card neon halo was three more
   hues competing with the boards. What separates a card here is a hairline
   and a soft shadow. */
.game-card {
  position: relative;
  /* One tile pattern at every width: icon · name · CTA on a single row.
     The old two-row grid pinned the icon far left and floated the name
     and the button in the middle, leaving a dead column on the right at
     every size between phone and desktop (owner 2026-09-14). The icon
     box is driven by --icon-size so breakpoints change one number
     instead of re-declaring three glyph rules each. */
  --icon-size: 66px;
  display: flex;
  align-items: center;
  gap: clamp(12px, 2.2vw, 18px);
  padding: clamp(14px, 2.2vw, 22px) clamp(16px, 2.4vw, 24px);
  text-align: left;
  cursor: default;
  border-radius: var(--radius-xl);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  transition: transform 0.3s var(--ease-out),
              border-color 0.25s var(--ease-out),
              box-shadow 0.3s var(--ease-out);
  overflow: hidden;
}
.game-card > * { position: relative; z-index: 1; }

.game-card:hover {
  transform: translateY(-4px);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-lg);
}
.game-icon {
  margin-bottom: 0;
  line-height: 1;
  height: var(--icon-size);
  width: var(--icon-size);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
/* All three glyphs share ONE ink treatment — a set of carved pieces, not
   three logos. Owner 2026-09-14: "cờ vua và caro đang đen trắng thì cờ
   tướng cũng phải như vậy", so the xiangqi disc lost its gold lacquer
   and red seal; the gomoku marks lost their blue/red for the same
   reason. Every size is derived from --icon-size on the card. */
.chess-icon {
  color: var(--piece-ink, #f0d9b5);
  text-shadow: none;
  font-size: var(--icon-size);
  line-height: 1;
}
.xiangqi-icon {
  color: var(--piece-ink, #f0d9b5);
  font-family: 'Noto Serif', 'Songti SC', serif;
  font-weight: 900;
  font-size: calc(var(--icon-size) * 0.42);
  width: calc(var(--icon-size) * 0.86);
  height: calc(var(--icon-size) * 0.86);
  border-radius: 50%;
  border: 2px solid currentColor;
  background: transparent;
  box-shadow: none;
  text-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gomoku-icon {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(var(--icon-size) * 0.03);
  font-size: calc(var(--icon-size) * 0.42);
  font-weight: 900;
  line-height: 0.9;
  width: fit-content;
  text-shadow: none;
  color: var(--piece-ink, #f0d9b5);
}
.gomoku-icon span {
  width: calc(var(--icon-size) * 0.3);
  height: calc(var(--icon-size) * 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
}
.gomoku-icon span:nth-child(2),
.gomoku-icon span:nth-child(3) { font-size: calc(var(--icon-size) * 0.36); }

/* 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 {
  flex: 1 1 auto;
  min-width: 0;
  font-size: clamp(1.05rem, 1.1rem + 0.5vw, 1.35rem);
  font-weight: 700;
  margin: 0;
  text-align: left;
  /* "Cờ tướng" broke onto two lines once the clock button joined the row.
     The tile is a single line by design — the CTA and the icon give way
     first (see the phone block below). */
  white-space: nowrap;
}
.game-card .btn-primary,
.game-card .btn-secondary {
  flex: 0 0 auto;
  padding: 9px 18px;
  font-size: clamp(0.82rem, 0.78rem + 0.3vw, 0.92rem);
  white-space: nowrap;
}
/* "Chơi ngay" (primary) and "Tạo phòng" (secondary) share the CTA slot as
   one control group. Rules for every width:
   - the two buttons always have the same box (min-height, line-height 1 on
     the "+" glyph, stretch) so the pair never reads as two sizes;
   - a label never gets clipped: the pair stacks where it cannot sit side by
     side, and as a last resort the label wraps (ru/fr "Создать комнату",
     "Créer une salle" are the longest of the eight languages);
   - touch widths (≤900px) get the 44px target, desktop 40px.
   The secondary takes the primary's pill shape and type, so the pair is one
   group rather than a pill next to a form button. */
.game-card-actions {
  flex: 0 1 auto;
  min-width: 0;
  /* Grid, not flex: equal rows (1fr) keep both buttons the height of the
     taller one when a long label wraps — flex columns cannot. */
  display: grid;
  grid-auto-rows: 1fr;
  gap: 8px;
}
.game-card-actions .btn-primary,
.game-card-actions .btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 40px;
  min-width: 0;
  line-height: 1.2;
  text-align: center;
  white-space: normal;
  border-radius: var(--radius-pill);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.game-card-actions .btn-icon { line-height: 1; }
/* Quick match switched off (lib/config.js stamps <html>): no "Chơi ngay",
   and "Tạo phòng" carries the primary look again — done here, before the
   first paint, rather than by script after it. */
html[data-quickmatch="off"] .game-card .btn-quick { display: none; }
html[data-quickmatch="off"] .game-card .btn-create {
  background: var(--accent);
  color: var(--on-red);
  border-color: transparent;
  box-shadow: var(--shadow-red);
}
html[data-quickmatch="off"] .game-card .btn-create:hover { background: var(--accent-hover); }

/* ── Buttons ── */
/* Solid seal red, pill, no gradient. The accent is the loud thing; giving
   it a second hue to fade into only weakens it. */
.btn-primary {
  background: var(--accent);
  color: var(--on-red);
  border: 1px solid transparent;
  padding: 11px 26px;
  border-radius: var(--radius-pill);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition: background 0.2s var(--ease-out), box-shadow 0.25s var(--ease-out), transform 0.1s;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  box-shadow: var(--shadow-red);
}
.btn-primary:hover {
  background: var(--accent-hover);
  transform: translateY(-1px);
  box-shadow: 0 14px 34px rgba(166, 51, 43, 0.28);
}
.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; }
/* The copyright line is the site's only ownership notice — it was
   display:none for a while and the footer read as a bare row of
   controls with nothing to anchor them. It stays. */
.footer-copy { display: block; flex: 1 1 auto; text-align: left; }
/* Licence notices (THIRD_PARTY_NOTICES.txt): quiet, in the copyright line. */
.footer-legal-link { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.footer-legal-link:hover { color: var(--accent); }
/* "All rights reserved." stays at every width (owner 2026-09-16: "thiếu
   All rights reserved.") — the line only shrinks. */
@media (max-width: 1024px) {
  .footer-copy { font-size: 0.78rem; }
}
.footer-row {
  display: flex;
  align-items: center;
  /* Copyright left, controls right — the row has content at both ends,
     so space-between anchors each cluster to its own edge. The 600px
     block below re-centres both, where they stack into two rows. */
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.footer-lang { flex: 0 0 auto; }
.footer-controls {
  /* Flex baseline — adapts to both index.html (3 children: training
     entry + theme + lang) and replay.html (2 children: theme + lang)
     without locking grid columns to a particular child count. Each
     child sets its own width (training btn auto-fits its label,
     theme toggle ~48 px, lang ~140 px) and 32 px height aligns them. */
  display: inline-flex;
  align-items: center;
  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; }
  /* Four controls (tournaments · practice · theme · language) on ONE
     row (owner 2026-09-16: "để 1 hàng thôi, cái ngôn ngữ kia để lá cờ
     thôi"): tighter buttons, and the language chip is just its flag —
     the full list is one tap away. ~300 px all told, so it fits 360. */
  /* One row when it fits, a second row when it does not — never off the
     edge. Measured 2026-10-11 (Chromium, default type): Vietnamese and
     Korean stay on one row at 360px; French and Russian labels are longer
     and put the language chip on a second row at 360 and below; every
     language wraps at 320. nowrap here ran the chip 3–14px past the edge
     at 320 and scrolled the whole page sideways. */
  .footer-controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 100%;
    gap: 8px 6px;
  }
  .footer-nav-btn, .footer-training-btn { padding: 0 10px; font-size: 0.8rem; }
  .footer-lang .lang-selector.in-footer .lang-btn { width: auto; padding: 0 9px; gap: 4px; }
  .footer-lang .lang-selector.in-footer .lang-name-sm { display: none !important; }
}

/* 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 var(--chip-bg-strong);
  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 training page (no real footer there).
   The game page dropped both controls — see the note in game.html.
   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, #f5ecdc);
  border: 1px solid var(--chip-bg-strong);
  border-radius: 16px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--hover) 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: var(--chip-bg);
  border: none;
  color: var(--text-muted, #82756a);
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
  padding: 0;
}
.cl-confirm-close:hover { background: var(--chip-bg-strong); color: var(--text, #f5ecdc); }
[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: color-mix(in srgb, var(--danger) 16%, transparent);
  color: #d98c83;
}
.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, #f5ecdc);
  word-break: break-word;
}
.cl-confirm-msg {
  margin: 0 0 18px;
  color: var(--text-muted, #82756a);
  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 var(--chip-bg-strong);
  background: var(--hover);
  color: var(--text, #f5ecdc);
  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: var(--chip-bg-strong); }
.cl-confirm-actions button:active { transform: scale(0.98); }
.cl-confirm-actions .cl-confirm-ok {
  background: var(--accent, #855a20);
  border-color: transparent;
  color: #fff;
}
.cl-confirm-actions .cl-confirm-ok:hover {
  background: color-mix(in srgb, var(--accent, #855a20) 86%, white 14%);
}
.cl-confirm-actions .cl-confirm-ok.is-danger {
  background: linear-gradient(180deg, #bd4439, #a6332b);
  border-color: rgba(0, 0, 0, 0.2);
  box-shadow: 0 4px 12px color-mix(in srgb, var(--danger) 28%, transparent);
}
.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 color-mix(in srgb, var(--accent) 30%, transparent);
}

/* ── 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: color-mix(in srgb, var(--accent2) 45%, transparent);
  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: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  color: #d98c83;
  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; }
/* Present for screen readers and the document outline (one <h1> a page),
   invisible on screen — for pages whose visible heading is the board. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Utility: left-align one element inside a centered card (e.g. the
   "Send this link" label above .link-box in .waiting-box). */
.text-start { text-align: start; }

/* ══════════════════════════════════════
   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: 1.3em;
  height: 1.3em;
  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);
}
/* The practice room's title badge is that page's <h1>: it is the only
   heading there, and an indexable page without one reads as structureless to
   a crawler (scripts/seo/audit.js fails the build over it). Keep the badge
   looking exactly like the <span> it used to be — no heading margins, no
   bold, still inline. */
h1.game-title-badge {
  display: inline-block;
  margin: 0;
  font-weight: inherit;
}

.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 (#bd4439 / 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: #86efac;
  font-size: 0.84rem;
  font-weight: 600;
  white-space: nowrap;
  user-select: none;
}
.header-online strong {
  color: #4ade80;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.online-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #22c55e;
  box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55);
  animation: online-pulse 1.6s ease-out infinite;
  flex-shrink: 0;
}
@keyframes online-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6); }
  70%  { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 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 var(--border);
  background: var(--card);
  transition: all 0.18s ease;
  white-space: nowrap;
}
.header-link:hover {
  border-color: var(--accent);
  background: var(--red-50);
  color: var(--accent);
}
.header-link.is-current {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-red);
}
.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;
  /* Ink pill. Frosted glass needs something behind it to frost; on paper it
     just reads as a smudge, and the sign-in control is the one thing in the
     header that has to look pressable. */
  border: 1px solid var(--stage-950);
  background: var(--stage-900);
  color: #ffffff;
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  user-select: none;
  transition: transform 0.18s var(--ease-out), box-shadow 0.22s var(--ease-out), background 0.22s var(--ease-out);
  box-shadow: 0 7px 18px rgba(16, 24, 20, 0.18);
}
.btn-google-signin:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--accent2) 6%, transparent)),
    rgba(20, 27, 61, 0.7);
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 6px 20px color-mix(in srgb, var(--accent) 22%, transparent);
  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 var(--chip-bg-strong);
  background: var(--surface);
  -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: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--hover-strong);
}
.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 var(--chip-bg-strong);
  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;
  /* Pin to top so the modal NEVER vertically re-centers as it grows.
     The old margin:auto-0 trick centered short modals and then snapped
     to flex-start when content overflowed the viewport — that snap was
     visible as a popup shift when a user expanded a long accordion
     section. Top-pinning keeps the header stable through every state. */
  align-items: flex-start;
  padding: 5vh 16px;
  overflow-y: auto;
  /* Reserve gutter for the overlay's own scrollbar so the centered
     modal doesn't shift horizontally when an accordion section opens
     and the overlay content crosses the viewport-height threshold. */
  scrollbar-gutter: stable;
  animation: profile-fade 0.18s ease both;
}
@keyframes profile-fade { from { opacity: 0; } to { opacity: 1; } }
/* Reserve the page scrollbar gutter even when the modal hides
   body overflow, so toggling the modal open/closed doesn't reflow
   the rest of the page horizontally (and so the modal itself
   doesn't perceive a width change mid-open). */
body { scrollbar-gutter: stable; }
body.profile-open { overflow: hidden; }

.profile-modal {
  position: relative;
  width: min(545px, 100%);
  /* No vertical margin — the overlay's `align-items: flex-start` +
     `padding: 5vh` give the spacing, and the modal stays put while
     accordion sections expand/collapse. */
  margin: 0;
  background: rgba(36, 37, 38, 0.98);
  border: 1px solid var(--chip-bg);
  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: var(--hover);
  border: 1px solid var(--chip-bg-strong);
  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: var(--chip-bg-strong); 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 var(--chip-bg-strong);
  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 color-mix(in srgb, var(--accent) 45%, transparent);
}
.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, #a6332b, #87271f);
  color: var(--on-accent);
}
.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: #c9963e;
  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: #78b39c;
  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: #78b39c;
  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: #d98c83;
  border: 1px solid rgba(252, 165, 165, 0.25);
}
.fbp-friend-secondary {
  background: var(--chip-bg);
  color: var(--text-muted);
  border: 1px solid var(--chip-bg-strong);
}
.fbp-friend-secondary:hover {
  background: rgba(252, 165, 165, 0.12);
  color: #d98c83;
  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 var(--hover);
  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: #35705d; }
.fbp-stat-l .fbp-stat-num { color: #bd4439; }
.fbp-stat-d .fbp-stat-num { color: #c9963e; }
.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 var(--hover);
}
.fbp-card .fbp-row:last-of-type { border-bottom: none; }
.fbp-row:hover { background: var(--hover); }
.fbp-row:focus-visible { outline: none; background: color-mix(in srgb, var(--accent) 8%, transparent); }
.fbp-row-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--chip-bg);
  flex-shrink: 0;
}
.fbp-row-icon svg { width: 18px; height: 18px; }
.fbp-row-icon.icon-trophy  { color: #c9963e; background: rgba(251, 191, 36, 0.10); }
.fbp-row-icon.icon-edit    { color: #a6332b; background: color-mix(in srgb, var(--accent) 10%, transparent); }
.fbp-row-icon.icon-gender  { color: #c9963e; background: color-mix(in srgb, var(--accent2) 10%, transparent); }
.fbp-row-icon.icon-history { color: #78b39c; background: rgba(110, 231, 183, 0.10); }
.fbp-row-icon.icon-out     { color: #d98c83; background: rgba(252, 165, 165, 0.12); }
.fbp-row-icon.icon-bio     { color: #ecd49a; background: rgba(196, 181, 253, 0.10); }
.fbp-row-icon.icon-cake    { color: #bd4439; background: rgba(244, 114, 182, 0.10); }
.fbp-row-icon.icon-camera  { color: #bd4439; 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: #35705d; background: color-mix(in srgb, var(--ok) 10%, transparent); }
.fbp-row-icon.icon-medal   { color: #c9963e; 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);
  /* Match the body-collapse duration + easing so the chevron lands at
     exactly the same moment the body finishes opening. */
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.fbp-row.is-open .fbp-row-chev { transform: rotate(90deg); }

/* Accordion body — JS-driven height transition. The outer element
   carries overflow:hidden + animated `height` (0 ↔ scrollHeight px
   ↔ auto), the inner wrapper carries the padding. This works in
   every browser (the modern grid-template-rows 0fr↔1fr trick only
   animates in Chrome 117+/Safari 17.4+/Firefox 132+ so older
   browsers were still snapping). */
.fbp-row-body {
  overflow: hidden;
  height: 0;
  transition: height 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  border-bottom: 1px solid var(--hover);
  /* No `will-change: height` — promoting a text-heavy box to its own
     layer makes some browsers re-rasterise the fonts during the
     transition, which reads as a "shimmer" / jitter. */
}
.fbp-row-body-inner {
  /* Padding lives here, NOT on the animated outer box — otherwise the
     padding would prevent the outer from collapsing fully to 0. */
  padding: 14px 16px;
}
.fbp-card .fbp-row-body:last-child { border-bottom: none; }
/* Fallback for the brief window before JS strips [hidden]: render as
   display:none so closed bodies don't flash open during first paint. */
.fbp-row-body[hidden] { display: none; }
.fbp-row-body .profile-section { margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .fbp-row-body { transition: none; }
}
/* 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. The extra
   .fbp-row-body-inner layer (added so we can animate the collapse)
   sits between body + section, hence the descendant selector. */
.fbp-row-body .fbp-no-section {
  padding: 0 !important;
}

.fbp-signout-row { color: #d98c83; }
.fbp-signout-row:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); color: var(--danger); }

/* 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: #d98c83; }

.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;
  /* Token-based, NOT hardcoded navy: the old rgba(7,9,26,.6) ground put
     dark ink on a dark well in light mode — users literally couldn't
     read what they were typing (tester report 2026-09-13). */
  background: var(--surface);
  border: 1px solid var(--border-strong);
  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);
  /* Focus LIFTS the field off the panel — card is one step lighter than the
     --surface ground it sits on in light mode, one step lighter than the
     panel in dark mode. Never a literal: this rule had its own hardcoded
     navy and survived the first retokenise pass. */
  background: var(--card);
}
.profile-nick-save {
  flex-shrink: 0;
  min-width: 84px;
  height: 40px;
  padding: 0 18px;
  background: var(--accent);
  border: none;
  border-radius: 10px;
  color: var(--on-accent);
  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: var(--surface);
  border: 1px solid var(--chip-bg-strong);
  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: #bd4439; background: rgba(96, 165, 250, 0.10); }
.gender-pick.active.g-female { color: #f9a8d4; border-color: #bd4439; background: rgba(244, 114, 182, 0.10); }
.gender-pick.active.g-other  { color: #ecd49a; border-color: #c9963e; background: color-mix(in srgb, var(--accent2) 10%, transparent); }
.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: var(--surface);
  border: 1px solid var(--chip-bg);
  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: #35705d; }
.stat-card.stat-loss .stat-num { color: #bd4439; }
.stat-card.stat-draw .stat-num { color: #c9963e; }
.stat-card.stat-rate .stat-num { color: var(--accent); }

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

/* Ink, not gold. Gold on paper is a low-contrast headline, and the accent
   here should be spent on the leader's row, not on the word "leaderboard".
   A short gold rule underneath carries the honours idea instead. */
.home-lb-title {
  text-align: center;
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  font-weight: var(--weight-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--text);
  margin: 0 0 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.home-lb-title::after {
  content: '';
  width: 46px;
  height: 2px;
  border-radius: 2px;
  /* Same colour as the two rules flanking the hero kicker — gold on paper,
     the cyan accent at night (owner 2026-09-16: "màu xanh cho đồng bộ cái
     trên chứ"). Was hard-coded gold in both themes. */
  background: linear-gradient(90deg, var(--eyebrow-rule), color-mix(in srgb, var(--eyebrow-rule) 55%, 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: var(--chip-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  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); }
/* The selected tab is a raised paper chip, not a gold lozenge — three of
   these side by side in gold read as three badges, not one control. */
.lb-seg.active {
  background: var(--thumb-bg);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(60, 40, 24, 0.10), 0 3px 10px rgba(60, 40, 24, 0.08);
}
.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); }

/* Dark theme: the raised-paper chip above is a light-theme device and it
   does not survive the palette swap. --thumb-bg (#1b2350) against a
   --chip-bg track of rgba(255,255,255,0.06) over a #07091a page is a few
   points of luminance, and a drop shadow tinted rgba(60,40,24,…) is
   invisible on near-black — so both strips read as four flat labels
   rather than a control with one of them selected, and nothing said
   "these are tabs" (owner, 2026-09-16: "nhìn vào khó biết là tabs ý
   nhưng ở darkmode thôi").
   Dark can't lift a chip with light, so it separates them by colour
   instead: the track sinks to a well with a visible rim, the selected
   tab becomes a filled gold lozenge with near-black text, and the
   unselected ones sit at muted weight. Gold, not --accent, because the
   accent blue is the page's button colour — a blue lozenge here would
   read as a third "Tạo phòng" — and because the leaderboard already
   spends gold on rank (the rule under the heading, the medals). */
[data-theme="dark"] .home-lb-segmented {
  background: rgba(3, 5, 16, 0.6);
  border-color: rgba(255, 255, 255, 0.10);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .lb-seg { color: var(--text-muted); }
[data-theme="dark"] .lb-seg:hover:not(.active) {
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}
[data-theme="dark"] .lb-seg.active {
  background: linear-gradient(135deg, var(--gold-500), var(--gold-300));
  color: #1a1106;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 10px rgba(201, 150, 62, 0.30);
}

.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%),
    var(--surface);
  border: 1px solid rgba(200, 144, 69, 0.15);
  border-radius: 14px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
/* Sort/tab switches swap the tbody in place. The dim used to sit on
   .home-lb-table-wrap together with a 0.18s opacity transition — but that
   element carries backdrop-filter: blur(10px), and transitioning opacity
   on a backdrop-filtered box makes the browser re-sample and re-blur the
   backdrop on every frame of the transition, twice per switch, while the
   rows underneath are being rebuilt. Dropping the dip from 0.55 to 0.85
   hid the flash but kept the cost, so testers still saw "bị giật".
   Dim the tbody instead: no blur underneath it and no transition, so the
   loading affordance survives and the per-frame blur work does not. */
.home-lb-section { overflow-anchor: none; }
.home-lb-section.is-loading .home-lb-table tbody { opacity: 0.85; }

.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: var(--surface);
}
.home-lb-table tbody td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--hover);
  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: var(--hover); }

.hlb-row.is-interactive { cursor: pointer; }
.hlb-row.is-interactive:hover td {
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.hlb-row.is-interactive:hover .hlb-name {
  color: #a6332b;
}

.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, #bd4439 0%, #87271f 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%, #c9963e 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%, #b8aca1 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 var(--chip-bg-strong);
  background: var(--border);
  flex-shrink: 0;
}
.hlb-avatar-fallback { background: linear-gradient(135deg, #aa7729, #dfbd73); }
.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;
}
/* Glowing text belongs on a dark ground. On paper a text-shadow halo reads
   as a printing defect, so light theme keeps the tier colour and drops the
   aura entirely; dark theme keeps both. */
:root[data-theme="light"] .hlb-row .hlb-tier-text,
:root[data-theme="light"] .hlb-row .hlb-metric-cell,
:root:not([data-theme="dark"]) .hlb-row .hlb-tier-text,
:root:not([data-theme="dark"]) .hlb-row .hlb-metric-cell {
  animation: none !important;
  text-shadow: none !important;
}

@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, #dfbd73);
  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: #bd4439; }
.hlb-gender.g-female { color: #bd4439; }
.hlb-gender.g-other  { color: #c9963e; }
.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 var(--chip-bg-strong);
  background: var(--surface);
  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: var(--surface);
  border: 1px solid var(--chip-bg);
  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: var(--on-accent);
}

.lb-table-wrap {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--chip-bg);
  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 var(--chip-bg);
}
.lb-table tbody td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--hover);
  vertical-align: middle;
}
.lb-table tbody tr:last-child td { border-bottom: none; }
.lb-table tbody tr:hover td { background: var(--hover); }
.lb-rank {
  width: 60px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
tr.rank-1 .lb-rank { color: #c9963e; font-size: 1.1rem; }
tr.rank-2 .lb-rank { color: #b8aca1; 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 var(--chip-bg-strong);
  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: var(--surface);
  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: #dfbd73; display: inline-flex; }
.rk-icon-chess   { color: #c9963e; }
.rk-icon-xiangqi { color: #bd4439; }
.rk-icon-gomoku  { color: #bd4439; }
.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, #dfbd73);
  font-size: 0.92rem;
  letter-spacing: 0.02em;
  padding: 2px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--row-tier-color, #dfbd73) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--row-tier-color, #dfbd73) 30%, transparent);
}
.rk-rank-none {
  color: var(--text-muted);
  background: transparent;
  border-color: var(--chip-bg-strong);
  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: #35705d; }
.rk-wld .rk-l { color: #bd4439; }
.rk-wld .rk-d { color: #c9963e; }
.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, #78b39c);
  background: color-mix(in srgb, var(--row-tier-color, #78b39c) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--row-tier-color, #78b39c) 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: #78b39c; background: rgba(110, 231, 183, 0.10); border-color: rgba(110, 231, 183, 0.25); }
.rk-pct-mid   { color: #c9963e; background: rgba(251, 191, 36, 0.10); border-color: rgba(251, 191, 36, 0.25); }
.rk-pct-cold  { color: #d98c83; 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) {
  /* Content-sized data columns, not fixed px. The old
     20 + 60 + 44 + 78 + 56 + gaps + padding = 310 px did not fit the
     281 px a 400 px phone leaves for these rows once the overlay,
     modal, card and body insets are paid — the grid overflowed 29 px to
     the right and the accordion's overflow:hidden ate the win-rate pill
     and the row's right padding (owner 2026-09-16: "để ý padding và
     responsive"). `auto` tracks hug the pills, the name gets the rest
     and ellipsises, so the row now fits any width down to 320 px. */
  .rk-row {
    grid-template-columns: 18px minmax(0, 1fr) auto auto auto;
    gap: 6px;
    padding: 8px 8px;
  }
  .rk-game { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rk-elo { justify-self: end; }
  /* 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: var(--surface);
  border: 1px solid var(--chip-bg);
  border-radius: 10px;
  font-size: 0.85rem;
  transition: border-color 0.15s ease;
}
.bd-row:hover { border-color: var(--chip-bg-strong); }
.bd-game { font-weight: 600; }
.bd-bar {
  display: flex;
  height: 8px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--hover);
}
.bd-bar-w { background: #35705d; }
.bd-bar-d { background: #c9963e; }
.bd-bar-l { background: #bd4439; }
.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: #35705d; font-weight: 600; }
.bd-counts .bd-l { color: #bd4439; font-weight: 600; }
.bd-counts .bd-d { color: #c9963e; 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; }
}

/* Scroll container moved up to .profile-history-body so the load-more
   button below the list is INSIDE the scrollable area — user scrolls
   to the bottom of the history box to reveal it, instead of seeing a
   bare button hanging beneath. The list itself is now a plain <ul>. */
.profile-history-body {
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--chip-bg);
  border-radius: 12px;
  background: var(--surface);
}
.hist-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Three-column dense card:
   [W/L/D mark]  [body — game-chip + opponent on top, meta + date on bottom]  [▶]
   One row = one game, no horizontal wrap. Mobile keeps the same shape
   but trims the marks + replay icon down 4 px so 320 px viewports fit. */
.hist-row {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--hover);
  transition: background 0.15s ease;
}
.hist-row:last-child { border-bottom: none; }
.hist-row:hover { background: var(--hover); }

.hist-mark {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.85rem;
  flex-shrink: 0;
}
.hist-mark--win  { background: color-mix(in srgb, var(--ok) 18%, transparent); color: #35705d; border: 1px solid color-mix(in srgb, var(--ok) 40%, transparent); }
.hist-mark--loss { background: rgba(251, 113, 133, 0.18); color: #bd4439; border: 1px solid rgba(251, 113, 133, 0.4); }
.hist-mark--draw { background: color-mix(in srgb, var(--accent2) 18%, transparent); color: #c9963e; border: 1px solid color-mix(in srgb, var(--accent2) 40%, transparent); }

.hist-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hist-line-top {
  display: flex; align-items: center; gap: 8px; min-width: 0;
}
.hist-game-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  background: var(--chip-bg);
  border: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
  flex-shrink: 0;
  white-space: nowrap;
}
.hist-game-icon {
  font-family: 'LXGW WenKai TC', 'Noto Serif SC', 'KaiTi', 'STKaiti', serif;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text);
}
.hist-opp {
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.hist-line-low {
  font-size: 0.72rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.hist-dot { opacity: 0.45; flex-shrink: 0; }
.hist-meta, .hist-time { flex-shrink: 0; }

.hist-replay {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  color: #a6332b;
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
  flex-shrink: 0;
}
.hist-replay:hover {
  background: color-mix(in srgb, var(--accent) 25%, transparent);
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  transform: scale(1.08);
}
.hist-replay svg { display: block; }

.profile-footer {
  margin-top: 8px;
  padding-top: 20px;
  border-top: 1px solid var(--chip-bg);
  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: #d98c83;
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
}
.btn-signout-large:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--danger) 40%, transparent);
}

/* Phone-tight history rows: trim the W/L/D mark, replay icon, and
   font sizes so the same dense 3-col card fits 320 px viewports. */
@media (max-width: 600px) {
  .hist-row {
    grid-template-columns: 28px minmax(0, 1fr) auto;
    gap: 10px;
    padding: 9px 12px;
  }
  .hist-mark { width: 28px; height: 28px; font-size: 0.78rem; }
  .hist-opp { font-size: 0.88rem; }
  .hist-game-chip { font-size: 0.68rem; padding: 1px 7px 1px 5px; }
  .hist-line-low { font-size: 0.68rem; }
  .hist-replay { width: 28px; height: 28px; }
}
.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: color-mix(in srgb, var(--accent) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--watch-blue) 45%, transparent);
  /* Label, glyph and border all the same blue (owner 2026-09-16: "chữ
     cũng đổi sang màu xanh"). */
  color: var(--watch-blue);
  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: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  transform: translateY(-1px);
}
/* Someone is on the rail. Amber read as a warning — the room being
   watched is not a problem, it's the point of sharing the link. Blue,
   the same one the room code wears in the night theme (owner
   2026-09-16: "khán giả để màu xanh biển như" + a shot of the room
   code). A token rather than var(--accent) because accent is red in the
   paper theme and the ask was for blue in so many words. The eye glyph
   is fill=currentColor, so it turns with the label. */
.btn-watch.has-viewers {
  color: var(--watch-blue);
  border-color: color-mix(in srgb, var(--watch-blue) 45%, transparent);
  background: color-mix(in srgb, var(--watch-blue) 10%, transparent);
}
.btn-watch.has-viewers:hover { background: color-mix(in srgb, var(--watch-blue) 20%, transparent); }

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

/* ── Phones + small tablets (< 768 px) ─────────────────────────────
   Same breakpoint SIDE_CLOCKS_MIN_VW uses in game.js, i.e. exactly
   the widths where setupSideClocks() parks the move timers back
   INSIDE the panels. Each panel becomes two clusters:

     [avatar · name · total clock] [move clock]  …  [draw resign theme lang]

   Two things were wrong before. (1) The move timer sat in a
   justify-self:center column of a 1fr/auto/1fr grid, so the
   opponent's clock floated in the middle of the top panel while the
   total clock underneath the name was flush left — the two clocks
   read as belonging to opposite sides of the row. (2)
   .footer-floating (theme + language) is position:fixed to the
   bottom-right corner, and on a phone the bottom panel spans nearly
   the whole viewport, so the pill landed directly ON TOP of the
   draw + resign buttons and hid them almost entirely.
   Laying the row out as flex pulls the clock up against the name,
   and .panel-tools gives all four right-hand controls one shared
   right-aligned row instead of two layers fighting over the same
   65 px. */
@media (max-width: 767.98px) {
  /* Flex, not the desktop 3-column grid. The grid's whole point is a
     centred middle column, which is exactly what we don't want here,
     and no grid template gives the behaviour this row needs: the name
     block shrinks (and ellipsises) while the clock and the controls
     keep their natural width. `minmax(0, auto)` on column 1 sizes to
     max-content, so a long nickname — "Trương Văn Thiện Quốc" on a
     320 px phone — grew the track until the buttons overlapped it.
     flex: 0 1 auto + min-width: 0 is the rule that actually expresses
     "give this up first". */
  .player-panel {
    display: flex;
    align-items: center;
  }
  /* The only shrinkable item in the row. min-width: 0 is what lets it
     go below min-content so .player-name's ellipsis can engage. */
  .player-panel .player-info { flex: 0 1 auto; min-width: 0; }
  .player-panel .move-timer { flex: 0 0 auto; }
  /* Everything from here is pinned to the right edge. Both selectors:
     .panel-tools once panel-tools.js has wrapped the controls, bare
     .action-btns before that (and if the script never runs).
     flex: 0 0 auto matters as much as the margin — left shrinkable,
     the controls gave up width alongside the nickname and the pill
     spilled out of its own wrapper past the panel edge. The name is
     the only thing in this row that may shrink. */
  .player-panel .panel-tools,
  .player-panel > .action-btns { margin-left: auto; flex: 0 0 auto; }

  /* Right-hand cluster: game actions first, then the two settings
     controls in the corner they already occupy on desktop. The pill
     is deliberately a SIBLING of .action-btns rather than a child —
     .action-btns is display:none before the game starts and for
     spectators, and the theme + language controls have to outlive
     both of those states. */
  .panel-tools {
    grid-column: 3;
    justify-self: end;
    display: inline-flex;
    align-items: center;
    gap: clamp(6px, calc(var(--board-w, 600px) * 0.018), 12px);
    min-width: 0;
  }
  /* setupPanelTools() moved the pill out of the body and into the
     grid cell — drop the viewport anchoring so it flows inline. */
  .footer-floating.in-panel {
    position: static;
    bottom: auto;
    right: auto;
    z-index: auto;
  }
  /* Flag + caret only. Six controls, a nickname and two clocks share
     one 50 px row down to 320 px, and the 120 px wide language label
     is the cheapest 74 px to give back — the flag still names the
     language, and the full list is one tap away. */
  .footer-floating.in-panel .lang-selector.in-footer .lang-btn {
    width: auto;
    padding: 0 8px;
    gap: 4px;
  }
  .footer-floating.in-panel .lang-selector.in-footer .lang-name-sm { display: none; }
}

/* ── Post-game review card (chess.com "Xem lại ván đấu") ──────────────
   One element, #postGame, rendered by game.js: under the board inside
   the dock on phones, in the #gameSide column beside the board on wide
   viewports (setupPhoneDock moves it). Rows are a 4-column grid —
   label · first side · badge · second side — so the two players' numbers
   line up under their avatars. Owner 2026-09-16, with chess.com's
   mobile + desktop screenshots. */
.post-game {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px 14px;
  margin: 8px;
  color: var(--text);
}
.post-game[hidden] { display: none; }
.pg-head {
  text-align: center;
  font-weight: 800;
  font-size: 1rem;
  padding-bottom: 10px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.pg-players, .pg-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 72px 28px 72px;
  align-items: center;
  gap: 4px;
  padding: 6px 0;
}
.pg-players { grid-template-columns: minmax(0, 1fr) 84px 4px 84px; }
.pg-label { color: var(--text); font-weight: 600; font-size: 0.86rem; }
.pg-player { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }
.pg-player:nth-child(2) { grid-column: 2; }
.pg-player:nth-child(3) { grid-column: 4; }
.pg-avatar {
  width: 56px; height: 56px;
  border-radius: 8px;
  border: 3px solid var(--border-strong);
  overflow: hidden;
  background: var(--surface);
  display: flex; align-items: center; justify-content: center;
}
.pg-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pg-player.is-winner .pg-avatar { border-color: var(--ok); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ok) 35%, transparent); }
.pg-name {
  font-size: 0.78rem;
  font-weight: 700;
  max-width: 84px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center;
}
.pg-val { text-align: center; font-weight: 800; font-variant-numeric: tabular-nums; }
.pg-muted { color: var(--text-muted); font-weight: 600; }
.pg-acc {
  display: inline-block;
  justify-self: center;
  min-width: 56px;
  padding: 5px 8px;
  border-radius: 6px;
  background: var(--chip-bg-strong);
  color: var(--text);
}
.pg-row-acc .pg-acc:first-of-type { background: var(--chip-bg-strong); }
.pg-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  justify-self: center;
  border-radius: 50%;
  font-size: 0.7rem;
  font-weight: 900;
  color: #fff;
  background: var(--text-muted);
}
.pg-row-acc .pg-badge, .pg-row-elo .pg-badge { background: transparent; }
.pg-row-brilliant  { --pg: #26c2a3; }
.pg-row-great      { --pg: #5b9bd5; }
.pg-row-best       { --pg: #81b64c; }
.pg-row-inaccuracy { --pg: #f0c15c; }
.pg-row-mistake    { --pg: #e6912c; }
.pg-row-blunder    { --pg: #fa412d; }
.pg-row-brilliant .pg-badge, .pg-row-great .pg-badge, .pg-row-best .pg-badge,
.pg-row-inaccuracy .pg-badge, .pg-row-mistake .pg-badge, .pg-row-blunder .pg-badge { background: var(--pg); }
.pg-row-brilliant .pg-val, .pg-row-great .pg-val, .pg-row-best .pg-val,
.pg-row-inaccuracy .pg-val, .pg-row-mistake .pg-val, .pg-row-blunder .pg-val { color: var(--pg); }
.pg-row-acc, .pg-row-elo { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 10px; }
.pg-row-acc { border-top: 0; margin-top: 0; }
.pg-elo b { font-weight: 800; }
.pg-elo i { font-style: normal; font-size: 0.72rem; margin-left: 2px; }
.pg-elo .elo-up { color: var(--ok); }
.pg-elo .elo-down { color: var(--danger); }
.pg-elo .elo-flat { color: var(--text-muted); }
.pg-note { text-align: center; color: var(--text-muted); font-size: 0.78rem; padding: 6px 0; }
.pg-btns { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.pg-btn {
  display: flex; align-items: center; justify-content: center;
  height: 46px;
  border-radius: 8px;
  border: 0;
  font: inherit;
  font-size: 1rem;
  font-weight: 800;
  cursor: pointer;
  text-decoration: none;
}
.pg-btn-secondary { background: var(--chip-bg-strong); color: var(--text); }
.pg-btn-primary { background: var(--accent); color: var(--on-accent); box-shadow: inset 0 -4px 0 rgba(0, 0, 0, 0.18); }
.pg-btn-primary:hover { background: var(--accent-hover); }

/* Clock glyph on the side on move — every viewport (owner 2026-09-16:
   "mobile có icon mà sao desktop không có?"). */
.player-panel .move-timer { gap: 8px; }
.player-panel .move-timer::before {
  content: '';
  width: 1.1em; height: 1.1em;
  flex-shrink: 0;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E") center / contain no-repeat;
}
.player-panel .move-timer::after { bottom: 0; }
.player-panel .move-timer.hidden::before { display: none; }

/* Wide viewports: the card lives in a column to the right of the board.
   .game-main becomes a two-column grid only while the card is open
   (.has-side, toggled by setupPhoneDock) so the board keeps its normal
   centred layout during play. */
.game-side { display: none; }
@media (min-width: 768px) {
  /* Beside the board: anchored inside .board-wrapper off the measured
     board width, exactly how .side-clocks did it, so the board keeps
     its own size. Width is whatever room is left to the right, capped. */
  .board-wrapper > .game-side {
    display: flex;
    flex-direction: column;
    gap: 10px;
    position: absolute;
    top: 0;               /* both overwritten inline by setupPhoneDock() */
    height: var(--board-w, 600px);   /* = the board's box */
    left: calc(50% + var(--board-w, 600px) / 2 + 14px);
    width: min(340px, calc(50% - var(--board-w, 600px) / 2 - 22px));
    min-width: 0;
    overflow: hidden;
    z-index: 5;
  }
  .board-wrapper > .game-side[hidden] { display: none; }
  .board-wrapper > .game-side .game-dock {
    flex: 1;
    min-height: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
  }
  .board-wrapper > .game-side .dock-pane { flex: 1; height: auto; min-height: 0; }
  .board-wrapper > .game-side .dock-actions { display: none; }
  /* Desktop bars: the clock pill is the right-most thing on BOTH bars
     (owner 2026-09-16, the phone bar as the model); the bottom bar's
     draw / resign slide into the middle column, just left of it. */
  .player-panel .move-timer {
    grid-column: 3; grid-row: 1; justify-self: end;
    /* A notch smaller than the board-scaled clamp (owner 2026-09-16:
       "cho nhỏ đi tí"). */
    font-size: 1.15rem;
    padding: 5px 10px 5px 12px;
    min-width: 96px;
  }
  /* Desktop: draw / resign sit right beside the clock pill (owner
     2026-09-16: "ở desktop thì căn sang phải cạnh cái khung đồng hồ"). */
  .panel-bottom .action-btns { grid-column: 2; grid-row: 1; justify-self: end; margin-right: 4px; }
  /* …but a room with no clocks — the training room — has no
     .move-timer to hold column 3 open, so the rule above stranded the
     buttons in the MIDDLE of the bar, and column 1 (sized to its own
     content by justify-self:start, not to the 1fr track) spilled the
     side label "× Xanh (đi trước)" straight over them. Two columns
     there instead: label on the left, free to ellipsis inside its
     track, buttons pinned to the right edge like every other room.
     game.html always ships both .move-timer divs — hidden between
     moves, never absent — so a real match keeps the 3-column bar. */
  .player-panel:not(:has(.move-timer)) {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .player-panel:not(:has(.move-timer)) .player-info { justify-self: stretch; }
  .player-panel:not(:has(.move-timer)) .action-btns { grid-column: 2; }
  .player-panel .move-timer.hidden { display: inline-flex !important; opacity: 0.4; filter: grayscale(0.5); }
  .game-main.has-side {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 12px;
    align-items: stretch;
  }
  .game-main.has-side .game-side {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-height: 0;
    overflow-y: auto;
  }
  .game-main.has-side .post-game { margin: 0; }
  /* The dock as a side panel: a card, a taller pane, no draw / resign
     row (those stay in the bottom player panel on a desktop). */
  .game-side .game-dock {
    flex: 1;
    min-height: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
  }
  .game-side .dock-pane { flex: 1; height: auto; min-height: 220px; }
  .game-side .dock-actions { display: none; }
  .game-side .post-game[hidden] { display: none; }
}

/* ✕ on the game-over modal — the review card sits behind it now, so the
   modal must be dismissable without leaving the room. The waiting box
   reuses the same glyph to back out of an unwanted table. */
.gameover-box, .waiting-box { position: relative; }
.waiting-close { display: flex; align-items: center; justify-content: center; text-decoration: none; z-index: 2; }
.gameover-close {
  position: absolute;
  top: 8px; right: 8px;
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--hover);
  color: var(--text-muted);
  font-size: 1rem;
  cursor: pointer;
}
.gameover-close:hover { color: var(--text); background: var(--hover-strong); }

/* ── Phone match layout, chess.com style (owner 2026-09-16: "giao diện
   làm giống cái này đi", chess.com mobile screenshots) ─────────────────
   Player bar · edge-to-edge board · player bar · dock (tabs, move list,
   draw/resign row). The page SCROLLS on phones — the dock can be taller
   than what is left under the board — so the board wrapper no longer
   stretches to the leftover height. It stays a size container (the cell
   formulas read 100cqh from it), so it needs a height of its own: a
   per-game aspect-ratio, the same trick replay.html uses, sized so the
   height arm always loses the min() to the width arm and the board is
   as wide as the phone. body[data-game] is set by setupUI(). Desktop
   (≥ 768 px) is untouched: the dock is display:none up there and the
   action buttons stay in the bottom panel. */
/* Phone match chrome palette — the site's OWN tokens in both themes.
   chess.com's layout was the ask, not its brown palette (owner
   2026-09-16: "bảo giao diện thôi còn màu sắc phải giữ màu cũ chứ"). */
:root {
  --dock-bg: var(--surface);
  --dock-pane-bg: var(--card);
  --dock-line: var(--border);
  --dock-btn: var(--chip-bg-strong);
  --dock-chip: var(--chip-bg-strong);
  --dock-row-alt: var(--row-bg);
}
@media (max-width: 767.98px) {
  /* The DOCUMENT scrolls, and the header is not pinned (owner
     2026-09-16: "em đừng ghim header nhé, để cho user scroll xuống").
     .game-body drops the 100svh + overflow:hidden straitjacket that
     made an inner box the scroller and kept the header nailed to the
     top: now the page itself scrolls, the header rolls off the top
     edge, and the match gets the whole screen. */
  .game-body {
    height: auto;
    min-height: 100vh; min-height: 100svh;
    overflow: visible;
  }
  .game-main { padding: 0; overflow: visible; flex: 0 0 auto; }
  .game-layout { height: auto; gap: 0; }
  .board-wrapper { flex: 0 0 auto; width: 100%; gap: 0; }
  body[data-game="chess"]   .board-wrapper { aspect-ratio: 8 / 9; }
  body[data-game="xiangqi"] .board-wrapper { aspect-ratio: 9 / 12; }
  body[data-game="gomoku"]  .board-wrapper { aspect-ratio: 12 / 21; }
  /* …but the three things a player watches — opponent bar, board, own
     bar — have to fit ONE screenful together (owner 2026-09-16: "nếu
     không hiển thị trong 1 màn hình sẽ rất khó theo dõi thời gian của
     mỗi bên"). Cap the board at whatever the viewport has left once
     both player bars are counted, so scrolling the header away puts
     both clocks and the board on screen at the same time. Under the
     cap the per-game aspect-ratio still decides the height; over it
     the wrapper just gets shorter and the boards — which read their
     cell size from this container's 100cqh — draw smaller instead of
     overflowing. The max() floor is the tallest board's own minimum
     (caro: 20 rows × 14 px + chrome), below which there is nothing
     left to shrink; it only engages on very short landscape phones.
     --match-bar-h mirrors .player-panel's height at each breakpoint. */
  .game-body { --match-bar-h: 56px; }
  .board-wrapper {
    max-height: max(340px, calc(100vh - var(--match-bar-h) * 2 - 6px));
    max-height: max(340px, calc(100svh - var(--match-bar-h) * 2 - 6px));
  }
  /* A modal owns the screen: don't let the page scroll behind it. */
  .game-body:has(.waiting-overlay:not(.hidden)),
  .game-body:has(.gameover-overlay:not(.hidden)),
  .game-body:has(.promotion-dialog:not(.hidden)) {
    overflow: hidden;
    height: 100vh; height: 100svh;
  }
  .board-topbar { padding: 4px 10px; }

  /* Player bars: flush, squared, one row — avatar · name/flag/material ·
     clock pill at the far right. */
  .player-panel {
    width: 100%;
    height: auto;
    min-height: 56px;
    padding: 6px 10px;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    gap: 10px;
  }
  .player-panel { border-left: 0; border-right: 0; }
  .player-avatar { width: 40px; height: 40px; }
  /* Clock pill at the right end of the bar (chess.com's placement) in
     the timer's own colours; the side on move shows a clock glyph, the
     other is dimmed. Both stay on screen so the bar never reflows. */
  /* Compact (owner 2026-09-16: "đồng hồ vẫn bự tổ chảng, cho nhỏ gọn"):
     the digits are a touch above the name's size, not a billboard. */
  .player-panel .move-timer {
    margin-left: auto;
    font-size: 1.05rem;
    letter-spacing: 0.3px;
    padding: 4px 10px 4px 10px;
    min-width: 86px;
    justify-content: flex-end;
    gap: 6px;
  }
  .player-panel .move-timer.hidden {
    display: inline-flex !important;
    opacity: 0.4;
    filter: grayscale(0.5);
  }

}
/* ── Dock: moves · chat · info, the walk buttons, draw / resign ────────
   Shared by phone (under the board) and desktop (the side column). */
/* Dock — chess.com's mobile chrome: warm charcoal surfaces, a tab
   strip with a white underline, one fixed-height pane (moves · chat ·
   info — switching tabs must not move the controls under them), the
   ⏮ ‹ ▶ › ⏭ row, then draw / resign. */
.game-dock {
  display: flex;
  flex-direction: column;
  width: 100%;
  background: var(--dock-bg);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.game-dock[hidden] { display: none; }
.dock-tabs { display: flex; flex-wrap: wrap; align-items: center; background: var(--dock-bg); }
/* Phones park #dockActions inside the tab strip (setupPhoneDock). When the
   tab and the two labelled buttons do not fit one row (French "Proposer la
   nulle" + "Abandonner" at 320px, or a larger browser font) the buttons go
   to a second row, right-aligned — never past the screen edge, which used
   to cut "Abandonner" in half and scroll the whole board sideways. */
.dock-tabs .dock-actions { margin-left: auto; padding: 4px 8px; max-width: 100%; min-width: 0; }
.dock-tabs .dock-actions .action-btns { flex-wrap: wrap; justify-content: flex-end; }
.dock-tabs .dock-actions .action-fab { height: 34px; padding: 0 10px; font-size: 0.85rem; }
.dock-tab {
  position: relative;
  flex: 1;
  background: none;
  border: 0;
  border-bottom: 3px solid transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  padding: 13px 8px 11px;
  cursor: pointer;
}
.dock-tab.active { color: var(--text); border-bottom-color: var(--text); }
.dock-tab-dot {
  position: absolute; top: 10px; margin-left: 4px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
}
.dock-tab-dot[hidden] { display: none; }
/* The one height every pane shares. */
.dock-pane {
  /* Twice the first cut (owner 2026-09-16: "kéo dài khung chat ra gấp
     đôi"); the box is fixed and only the transcript inside it scrolls. */
  height: 352px;
  display: flex;
  flex-direction: column;
  background: var(--dock-pane-bg);
  border-top: 1px solid var(--dock-line);
  border-bottom: 1px solid var(--dock-line);
}
.dock-pane[hidden] { display: none; }
.dock-moves-head {
  padding: 9px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  border-bottom: 1px solid var(--dock-line);
  flex-shrink: 0;
}
.dock-moves {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  font-variant-numeric: tabular-nums;
}
.dock-empty, .chat-empty { color: var(--text-muted); font-size: 0.85rem; padding: 12px 14px; }
.dock-move-row {
  display: grid;
  grid-template-columns: 44px 1fr 1fr 60px;
  align-items: center;
  padding: 3px 14px 3px 10px;
  font-size: 0.92rem;
}
.dock-move-row:nth-child(odd) { background: var(--dock-row-alt); }
.dock-move-no { color: var(--text-muted); font-weight: 600; }
.dock-move {
  justify-self: start;
  background: none;
  border: 0;
  color: var(--text);
  font: inherit;
  font-weight: 700;
  padding: 3px 6px;
  border-radius: 3px;
  cursor: pointer;
}
.dock-move.is-current { background: var(--dock-chip); }
.dock-move-ts {
  color: var(--text-muted);
  font-size: 0.64rem;
  font-weight: 700;
  line-height: 1.3;
  border-left: 3px solid var(--dock-line);
  padding-left: 4px;
}
.dock-info { font-size: 0.9rem; color: var(--text-muted); padding: 14px; text-align: center; justify-content: center; }
.dock-info strong { color: var(--text); }
.dock-info-score { margin-top: 6px; }

/* Chat — messenger bubbles: mine right, others left with avatar + name,
   consecutive lines from one person grouped. */
.chat-list {
  list-style: none; margin: 0; padding: 8px 10px;
  flex: 1; min-height: 0; overflow-y: auto;
  /* Reaching the end of the transcript must not start scrolling the page. */
  overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 6px;
  font-size: 0.9rem;
}
.chat-msg { display: flex; align-items: flex-end; gap: 8px; max-width: 100%; }
.chat-msg.is-mine { flex-direction: row-reverse; }
.chat-msg.is-cont { margin-top: -3px; }
.chat-avatar {
  width: 26px; height: 26px; flex: 0 0 26px;
  border-radius: 50%; overflow: hidden;
  background: var(--chip-bg-strong);
  display: flex; align-items: center; justify-content: center;
}
.chat-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Grouped lines keep the 26px gutter so their bubbles stay aligned, but
   the slot is empty — without this it paints a filled grey disc that
   reads as a broken avatar (owner 2026-09-21: "lỗi chat"). */
.chat-avatar:empty { background: none; }
.chat-initial { font-size: 0.72rem; font-weight: 800; color: var(--text-muted); }
.chat-body { display: flex; flex-direction: column; gap: 2px; max-width: min(78%, 320px); min-width: 0; }
.chat-msg.is-mine .chat-body { align-items: flex-end; }
.chat-who { font-size: 0.7rem; font-weight: 700; color: var(--text-muted); padding-left: 4px; }
.chat-bubble {
  display: inline-block;
  padding: 7px 11px;
  border-radius: 14px;
  border-bottom-left-radius: 4px;
  background: var(--chip-bg-strong);
  color: var(--text);
  line-height: 1.35;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.chat-msg.is-mine .chat-bubble {
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 14px;
  border-bottom-right-radius: 4px;
}
.chat-bubble.has-pic { padding: 4px; }
.chat-bubble.has-pic .chat-text { display: block; padding: 4px 7px 3px; }
.chat-pic { display: block; border: 0; padding: 0; background: none; cursor: zoom-in; }
.chat-pic img { display: block; max-width: 220px; max-height: 220px; border-radius: 10px; }
.chat-tag {
  margin-left: 6px;
  font-size: 0.62rem; font-weight: 600;
  color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 999px; padding: 0 6px;
}
.chat-empty { color: var(--text-muted); font-size: 0.85rem; padding: 12px 14px; }
.chat-form { display: flex; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--dock-line); flex-shrink: 0; }
.chat-form[hidden] { display: none; }
.chat-attach {
  width: 36px; height: 36px; flex: 0 0 36px;
  border-radius: 6px; border: 1px solid var(--dock-line);
  background: var(--dock-btn); color: var(--text-muted);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.chat-attach:hover { color: var(--text); }
.chat-input {
  flex: 1; min-width: 0;
  height: 36px;
  padding: 0 10px;
  border-radius: 6px;
  border: 1px solid var(--dock-line);
  background: var(--dock-btn);
  color: var(--text);
  font: inherit;
  font-size: 0.9rem;
}
.chat-input::placeholder { color: var(--text-muted); }
.chat-send {
  height: 36px; padding: 0 14px;
  border: 0; border-radius: 6px;
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-weight: 700; cursor: pointer;
}
.dock-pane.is-dropping { outline: 2px dashed var(--accent); outline-offset: -4px; }
.chat-lightbox {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(0, 0, 0, 0.82);
  display: flex; align-items: center; justify-content: center;
  cursor: zoom-out;
}
.chat-lightbox[hidden] { display: none; }
.chat-lightbox img { max-width: 92vw; max-height: 92vh; border-radius: 8px; }

/* ⏮ ‹ ▶ › ⏭ */
.dock-nav {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
  padding: 8px 8px 4px;
}
.dock-nav-btn {
  height: 46px;
  border: 0;
  border-radius: 6px;
  background: var(--dock-btn);
  color: var(--text);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.25);
}
.dock-nav-btn:disabled { opacity: 0.35; cursor: default; }
.dock-nav-play { font-size: 1.1rem; }
body.is-viewing .dock-nav-play { background: var(--accent); color: var(--on-accent); }

.dock-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;   /* draw / resign hug the right on phones */
  gap: 6px;
  padding: 6px 8px 8px;
}
.dock-actions .action-btns { display: flex; gap: 6px; }
.dock-actions .action-fab {
  width: auto;
  height: 40px;
  padding: 0 12px;
  gap: 8px;
  border-radius: 6px;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
}
.dock-actions .action-fab svg { width: 20px; height: 20px; }
.dock-actions .action-label { display: inline; }

/* Guest chat-name prompt (reuses the confirm-dialog chrome). */
.chat-name-input {
  width: 100%;
  height: 40px;
  margin: 8px 0 4px;
  padding: 0 12px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  box-sizing: border-box;
}
.chat-name-input.is-invalid { border-color: var(--danger); }
.chat-tag {
  margin-left: 6px;
  font-size: 0.66rem;
  font-weight: 600;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 6px;
}

/* Dock with only the chat on (DOCK_FEATURES in game.js). Everything else
   is hidden, not removed. */
body.dock-chat-only .dock-tabs { justify-content: center; }
/* Phones: the lone Chat tab sits at the left (owner 2026-09-16). */
@media (max-width: 767.98px) {
  body.dock-chat-only .dock-tabs { justify-content: flex-start; }
  body.dock-chat-only .dock-tab { padding-left: 14px; padding-right: 14px; }
}
body.dock-chat-only .dock-tab { flex: 0 0 auto; padding: 13px 24px 11px; cursor: default; }
body.dock-chat-only .dock-moves-head,
body.dock-chat-only .dock-nav,
body.dock-chat-only .dock-analysis,
body.dock-chat-only .dock-summary,
body.dock-chat-only .dock-post { display: none !important; }

/* ── After the game, in the dock (chess.com) — both layouts ──────────── */
.dock-analysis {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px;
  font-size: 0.85rem; font-weight: 600;
  color: var(--text);
  border-bottom: 1px solid var(--dock-line);
}
.dock-analysis[hidden] { display: none; }
.dock-analysis-label::before {
  content: ''; display: inline-block; width: 18px; height: 18px; margin-right: 8px;
  vertical-align: -4px; border-radius: 50%;
  background: var(--dock-btn);
}
.dock-analysis-progress { color: var(--text-muted); font-weight: 500; }
.dock-analysis-engine { margin-left: auto; color: var(--text-muted); font-weight: 500; font-size: 0.8rem; }
.dock-result { padding: 6px 14px 6px 54px; font-weight: 800; color: var(--text); }
.dock-move-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; padding: 0 3px; margin-left: 6px;
  border-radius: 50%;
  font-size: 0.6rem; font-weight: 900; line-height: 1;
  color: #fff; background: var(--text-muted);
  vertical-align: 1px;
}
.is-brilliant  { --pg: #26c2a3; }
.is-great      { --pg: #5b9bd5; }
.is-best       { --pg: #81b64c; }
.is-good       { --pg: #96af8b; }
.is-inaccuracy { --pg: #f0c15c; }
.is-mistake    { --pg: #e6912c; }
.is-blunder    { --pg: #fa412d; }
.dock-move-badge[class*="is-"] { background: var(--pg); }
.dock-summary {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 14px;
  padding: 8px 12px;
  font-size: 0.85rem; font-weight: 700;
  border-bottom: 1px solid var(--dock-line);
}
.dock-summary[hidden] { display: none; }
.dock-chip { color: var(--pg, var(--text)); display: inline-flex; align-items: center; gap: 5px; }
.dock-chip i {
  font-style: normal; display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--pg, var(--text-muted)); color: #fff; font-size: 0.62rem; font-weight: 900;
}
.dock-post { display: flex; gap: 8px; padding: 6px 8px 8px; }
.dock-post[hidden] { display: none; }
.dock-post .pg-btn { flex: 1; height: 44px; font-size: 0.95rem; }
.dock-post .pg-btn[hidden] { display: none; }
.dock-actions[hidden] { display: none; }
/* The review card stays folded in the dock until "Xem lại ván đấu". */
.game-dock .post-game:not(.is-open) { display: none; }

/* End-of-game king marks: the winner's king wears a green crown, the
   loser's a red cross, both a grey half on a draw (chess.com's board). */
.chess-square { position: relative; }
.king-badge {
  position: absolute; top: -3px; right: -3px;
  width: 38%; height: 38%;
  min-width: 16px; min-height: 16px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.7em; font-weight: 900; line-height: 1;
  color: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  z-index: 6;
  pointer-events: none;
}
.king-badge.is-win { background: var(--ok); }
.king-badge.is-lose { background: var(--danger); }
.king-badge.is-draw { background: var(--text-muted); }
.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: #ebdcc4;
  background: var(--surface);
  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 var(--hover);
  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 var(--chip-bg), 0 0 0 0 color-mix(in srgb, var(--danger) 0%, transparent); }
  to   { box-shadow: inset 0 1px 0 var(--chip-bg), 0 0 0 2px color-mix(in srgb, var(--danger) 32%, transparent); }
}
.score-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 8px auto 20px;
  padding: 14px 24px;
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--accent2) 10%, transparent));
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: 14px;
  max-width: 320px;
  box-shadow: inset 0 1px 0 var(--hover), 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); }
/* Avatar above the name, both centred under their number. */
.score-side {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  text-transform: none; letter-spacing: 0; text-align: center;
  max-width: 96px;
}
.score-avatar {
  width: 36px; height: 36px; border-radius: 50%;
  overflow: hidden; background: var(--surface);
  border: 2px solid var(--accent);
  display: flex; align-items: center; justify-content: center;
}
.score-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.score-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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: #a6332b;
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 60%, transparent);
  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;
}
/* Rating movement chip on the game-over overlay — "+12 Elo · 1200 → 1212".
   Same visual family as .score-card (rounded pill inside the overlay box);
   jade/red communicate direction, muted ink for a 0-point move. */
.elo-delta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 2px auto 6px;
  font-variant-numeric: tabular-nums;
}
.elo-delta-badge {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-weight: 800;
  font-size: 1.05rem;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
}
/* --ok / --danger are the two direction colors that are re-declared by
   both themes, so the chip stays legible on paper and on stage. */
.elo-delta-badge.elo-up   { color: var(--ok); }
.elo-delta-badge.elo-down { color: var(--danger); }
.elo-delta-badge.elo-flat { color: var(--text-muted); }
.elo-delta-detail {
  font-size: 0.85rem;
  color: var(--text-muted);
}

.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; }
/* Each captured piece keeps its own colour instead of inheriting the panel's
   muted grey, where a taken pawn was a smudge nobody noticed. The outline is
   what makes a white piece readable on a light panel and a black one on a
   dark panel, so both themes work with one rule. */
.cap-piece {
  line-height: 1;
  margin-right: -0.18em;   /* overlap slightly, the way a captured pile sits */
}
.cap-piece:last-of-type { margin-right: 0; }
.cap-white, .cap-red {
  color: #fffdf8;
  text-shadow: 0 0 1px rgba(32, 27, 23, 0.9), 0 1px 2px rgba(32, 27, 23, 0.45);
}
.cap-black {
  color: #201b17;
  text-shadow: 0 0 1px rgba(255, 253, 248, 0.85);
}
/* Material lead. Only the side that is ahead shows one, so the two trays
   never argue with each other. */
.cap-lead {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--chip-bg);
  color: var(--text-muted);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1.4;
  white-space: nowrap;
}
.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: #ebdcc4;
  background: #201b17;
  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 var(--hover),
    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: #d98c83;
  background: #2a0f0f;
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  animation: timerWarningPulse 0.6s ease-in-out infinite alternate;
}
@keyframes timerWarningPulse {
  from {
    box-shadow:
      inset 0 0 0 1px var(--hover),
      0 2px 4px rgba(0, 0, 0, 0.45);
  }
  to {
    box-shadow:
      inset 0 0 0 1px var(--hover),
      0 0 0 3px color-mix(in srgb, var(--danger) 32%, transparent),
      0 2px 8px color-mix(in srgb, var(--danger) 45%, transparent);
  }
}
/* 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: color-mix(in srgb, var(--danger) 10%, transparent); }
@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: #f5ecdc;
  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; }
/* A tooltip that is not showing must not exist for layout: at opacity 0 it
   still sat there, nowrap, centred on its button — on game.html the draw /
   resign buttons hug the right edge, so "Proposer la nulle (3/3)" stuck
   1–21px past the screen and the page scrolled sideways. So it is not
   rendered at rest (display:none) and fades in on hover/focus after 0.3s. */
[data-tooltip]::before,
[data-tooltip]::after {
  display: none;
  position: absolute;
  left: 50%;
  pointer-events: none;
  z-index: 100;
}
[data-tooltip]::before {
  content: attr(data-tooltip);
  bottom: calc(100% + 8px);
  background: rgba(15, 23, 42, 0.95);
  color: #f5ecdc;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.1px;
  padding: 5px 10px;
  border-radius: 6px;
  white-space: nowrap;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
[data-tooltip]::after {
  content: '';
  bottom: calc(100% + 2px);
  border: 5px solid transparent;
  border-top-color: rgba(15, 23, 42, 0.95);
}
[data-tooltip]:hover::before,
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::before,
[data-tooltip]:focus-visible::after {
  display: block;
  animation: cl-tip-in 0.18s ease 0.3s both;
}
@keyframes cl-tip-in {
  from { opacity: 0; transform: translateX(-50%) translateY(4px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
[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);
}
/* Text next to the icon — only the phone dock shows it (below). */
.action-label { display: none; }
.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: color-mix(in srgb, var(--danger) 12%, transparent);
}
.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: color-mix(in srgb, var(--danger) 50%, transparent) !important; box-shadow: inset 0 0 0 2px #bd4439; }
  to { background: color-mix(in srgb, var(--danger) 90%, transparent) !important; box-shadow: inset 0 0 0 4px #a6332b, 0 0 18px color-mix(in srgb, var(--danger) 70%, transparent); }
}
@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;
}
/* A piece you can take reads as a RING around the square, not a blob on
   top of it (owner 2026-09-14: "có thể ăn hay gì đó chưa có báo cho
   user"). The first attempt at this was a filled overlay and got pulled
   for covering the piece; a gradient ring hugs the square's edge, leaves
   the piece completely visible, and scales with the board because the
   stops are percentages. Same green as the empty-square dot, so "you may
   move here" and "you may take this" read as one language. */
.chess-square.valid-capture::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle,
    transparent 0 56%,
    rgba(20, 85, 30, 0.55) 58% 70%,
    transparent 72%);
  pointer-events: none;
}
.chess-square.in-check { background: color-mix(in srgb, var(--danger) 60%, transparent) !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 — lacquered wood, ivory discs
   ══════════════════════════════════════
   Geometry lives in xiangqi.js (XQ_CELL / XQ_MARGIN / XQ_W / XQ_H): a
   608x672 coordinate space with intersections every 64 units. The board art
   is an SVG in that space and every point above it is positioned by
   percentage of the same space, so the two stay locked together at any size
   and nothing here needs a pixel measurement of the grid.

   Sizes below are in cqi — percent of the board's own width — because the
   board is the container query context. That is what lets one rule set
   serve a 300px phone board and a 700px desktop one. */
.xq-board {
  /* Fit the wrapper, keep the 9x10 grid's proportions. The 44px comes off
     the height because .board-wrapper is a column that also holds the
     status bar and its gap — spend all of 100cqh on the board and it
     overhangs the player panel below by exactly that much. */
  width: min(100cqw, calc((100cqh - 44px) * 608 / 672));
  max-width: 100%;
  margin-inline: auto;
  aspect-ratio: 608 / 672;
  position: relative;
  container-type: inline-size;

  /* Kai (楷書) is the traditional face for chess characters. All of these
     are system fonts — nothing is downloaded, and the stack degrades to a
     serif rather than a sans if none is installed. */
  /* Kai (楷書) is the face chess characters are traditionally cut in. LXGW
     WenKai TC is loaded per page and comes first because Kai is NOT a font
     most machines ship — this Mac, for one, has only Song faces installed,
     so the system-font-only stack that used to be here silently rendered
     every piece in Songti and the board read as the wrong object entirely. */
  --xq-kai: "LXGW WenKai TC", "LXGW WenKai", "STKaiti", "Kaiti SC", KaiTi,
            "TW-Kai", "BiauKai", "Noto Serif CJK TC", "Songti SC", serif;
  --xq-ink: #5b351f;

  border: 1px solid #4f2b1a;
  border-radius: 18px;
  /* ONE flat colour (owner 2026-09-16: "bàn cờ nên để 1 màu nha, k chơi
     gradient"). The surface used to be a lacquer imitation — a diagonal
     highlight, two "grain streaks" and a 145° wood gradient — and on a
     phone the brighter of the two streaks read as a scratch running
     across the river ("có cái gạch ở giữa bàn cờ kìa"). The mid-tone of
     the old gradient is kept as the single colour so the board still
     sits in the same palette as the pieces and the --xq-ink lines.
     The inset shadow went with it: it darkened the bottom third, which
     is a gradient by another name. The outer shadow stays — that is
     depth AROUND the board, not shading ON it. */
  background: #d3a162;
  box-shadow: 0 26px 67px rgba(5, 8, 6, 0.42);
}

/* ── Board art ── */
.xq-board-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}
.xq-frame       { fill: none; stroke: #4f2b1a; stroke-width: 2.8; }
/* The river was a lighter band painted over the surface — the last
   second tone left after the board went flat (owner: "để 1 màu"). The
   river still reads: it is the one row with no vertical lines, and it
   carries 楚河 / 漢界. */
.xq-river-band  { fill: none; }
.xq-lines line  { stroke: var(--xq-ink); stroke-width: 1.45; }
.xq-palaces     { fill: none; stroke: var(--xq-ink); stroke-width: 1.2; }
.xq-marks path  { fill: none; stroke: var(--xq-ink); stroke-width: 1.1; stroke-linecap: round; }
.xq-file-coords text {
  fill: rgba(79, 43, 25, 0.66);
  font-size: 11px;          /* SVG user units — scales with the board */
  font-weight: 600;
}
.xq-river-labels text {
  fill: rgba(91, 53, 31, 0.82);
  font-family: var(--xq-kai);
  font-size: 23px;
  letter-spacing: 4.14px;
}

/* ── Intersections ── */
.xq-points { position: absolute; inset: 0; }
.xq-point {
  position: absolute;
  width: 11cqi;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  border-radius: 50%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Where the last move started and where it finished. Two colours, not one:
   amber-and-dashed for the square left behind, red-and-solid for the square
   landed on, so the direction of the move reads without replaying it. */
.xq-point.is-last-from::after,
.xq-point.is-last-to::after {
  content: "";
  position: absolute;
  width: 10.2cqi;
  aspect-ratio: 1;
  border-radius: 50%;
  pointer-events: none;
}
.xq-point.is-last-from::after {
  background: rgba(232, 178, 72, 0.28);
  border: 1px dashed rgba(135, 82, 25, 0.72);
  box-shadow: inset 0 0 0 2.4px rgba(255, 238, 177, 0.2);
}
.xq-point.is-last-to::after {
  background: rgba(191, 66, 52, 0.26);
  border: 1px solid rgba(167, 55, 43, 0.78);
  box-shadow: inset 0 0 0 2.4px rgba(255, 229, 177, 0.28), 0 0 0 1.3px rgba(116, 46, 34, 0.2);
}

/* ── Pieces ── ivory disc, raised rim, carved glyph ── */
.xq-piece {
  position: relative;
  width: 9.6cqi;
  aspect-ratio: 1;
  filter: drop-shadow(0 0.55cqi 1cqi rgba(48, 29, 16, 0.3));
  transition: transform 0.16s ease;
}
.xq-piece__rim {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid #805638;
  background:
    radial-gradient(circle at 34% 22%, rgba(255, 248, 224, 0.55), rgba(255, 248, 224, 0.55) 7%, transparent 28%),
    linear-gradient(145deg, #fff6df, #d7b985 72%, #b88b5b);
  box-shadow:
    inset 0 0 0 2px rgba(255, 247, 219, 0.38),
    inset 0 -1.9px 4.2px rgba(96, 54, 29, 0.18),
    inset 0 0 0 1px rgba(255, 250, 232, 0.58),
    0 4px 11px rgba(51, 27, 14, 0.34);
}
/* The inset face is what reads as a turned disc rather than a flat token. */
.xq-piece__face {
  position: absolute;
  inset: 7.5%;
  border-radius: 50%;
  background:
    radial-gradient(circle at 36% 25%, rgba(255, 255, 244, 0.64), rgba(255, 255, 244, 0.64) 8%, transparent 26%),
    radial-gradient(circle at 50% 46%, #fff6df, #f3e3c3 67%, #d7b985);
  box-shadow: inset 0 0 0 1px rgba(255, 253, 235, 0.42), 0 1px 1px rgba(89, 50, 28, 0.1);
}
.xq-piece__glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--xq-kai);
  font-size: 6.1cqi;
  font-weight: 400;
  line-height: 1;
  text-shadow: 0 0.5px 0 rgba(255, 252, 235, 0.18);
  user-select: none;
}
.xq-piece--red   .xq-piece__glyph { color: #b72722; }
.xq-piece--black .xq-piece__glyph { color: #171512; }
.xq-point:hover .xq-piece { transform: scale(1.04); }

/* ── Piece states ── one ring element, three looks ── */
.xq-piece__state {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 3px solid transparent;
  opacity: 0;
  pointer-events: none;
}
/* The piece that just moved keeps a warm ring for one turn, so a player
   coming back to the board knows what changed while they were away. */
.xq-piece.is-last-moved .xq-piece__state {
  opacity: 1;
  inset: -8%;
  border-width: 2px;
  border-color: rgba(180, 76, 55, 0.76);
  box-shadow: 0 0 0 2px rgba(255, 242, 202, 0.78), 0 0 0 5px rgba(187, 91, 61, 0.38);
}
.xq-piece.is-selected .xq-piece__state {
  opacity: 1;
  border-color: #78b39c;
  box-shadow: 0 0 0 2px rgba(244, 235, 221, 0.82), 0 0 0 5px rgba(79, 143, 120, 0.72);
}
/* Dotted, and a touch wider than the disc, so "you can take this" never
   reads as "this is selected". */
.xq-piece.is-capturable .xq-piece__state {
  opacity: 1;
  inset: -2.5%;
  border-style: dotted;
  border-color: rgba(34, 113, 89, 0.82);
  box-shadow: 0 0 0 1.4px rgba(225, 250, 231, 0.88), 0 0 0 3.7px rgba(39, 130, 99, 0.25);
}
/* A general in check is the one state that must beat the others visually. */
.xq-piece.is-check .xq-piece__state {
  opacity: 1;
  border-color: rgba(196, 58, 48, 0.92);
  box-shadow: 0 0 0 2px rgba(255, 236, 230, 0.72), 0 0 0 5px rgba(183, 39, 34, 0.45);
}

/* ── Move targets ── a dot on an empty point, a ring on a capture ── */
.xq-target {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  width: 1.8cqi;
  aspect-ratio: 1;
  background: rgba(36, 111, 88, 0.34);
  border: 1px solid rgba(255, 253, 235, 0.68);
  box-shadow: 0 0 0 3px rgba(39, 112, 93, 0.08);
}
.xq-target--capture {
  width: 9.7cqi;
  background: rgba(62, 139, 113, 0.09);
  border: 2px solid rgba(38, 113, 91, 0.68);
  box-shadow: 0 0 0 2px rgba(220, 248, 230, 0.36);
}

@media (prefers-reduced-motion: reduce) {
  .xq-piece { transition: none; }
}
/* ══════════════════════════════════════
   GOMOKU / CARO BOARD (12 × 20 ô)
══════════════════════════════════════ */
:root {
  /* No --caro-cell default here: .caro-mark only ever renders inside
     .gomoku-board, so the board's own fallback (and the px value
     gomoku.js freezes onto the element) is the single source. A second
     default up here just gave a stale number something to shadow. */
  --caro-line: #c8cdd4;
  --caro-bg: #f6f7f9;
  /* Owner rule (2026-09-13): caro is ALWAYS blue × vs red ○ — the i18n
     labels ("× Xanh (đi trước)" / "○ Đỏ") promised it, but both tokens
     had drifted to reds and the two sides became near-identical on the
     board. The board surface is the same light paper in both themes, so
     one blue works everywhere. */
  --caro-x: #1e5fbf;
  --caro-o: #bd4439;
}
.gomoku-board {
  /* NO container query here on purpose. The cell size used to be
     `max(14px, min((100cqw - 32px)/12, (100cqh - 80px)/20))` off
     .board-wrapper, which tied 20 rows of cells to the wrapper's LIVE
     height — and that height moves while a match is running:
     syncBoardWidth() publishes the board's measured width as --board-w,
     the player panels and the move-timer size themselves off that
     variable, their reflow changes the wrapper's height, the new 100cqh
     picks a different cell, the board's width changes, --board-w is
     republished… a loop the ResizeObserver kept feeding. On screen it
     read as the board resizing itself mid-game and juddering while the
     page was scrolled (owner 2026-09-17: "lúc thì bàn cờ nhỏ, lúc phóng
     to hơn … nó bị giật giật" → "fix cứng 1 khung cho nó luôn").
     GomokuGame.render() now measures the frame ONCE per page load and
     writes an integer px into --caro-cell on <html>; this value is only
     the pre-JS fallback. */
  --caro-cell: 26px;
  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: #a6332b;
  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 #bd4439;
  border-radius: 50%;
  background: transparent;
}
/* No last-move tint on caro — owner 2026-09-21 asked for the yellow
   cell background to go; the .last-move class stays as a styling hook. */
.gomoku-cell.win-cell {
  position: relative;
  background: linear-gradient(135deg, #fff3a0 0%, #dfbd73 40%, #ff922b 60%, #dfbd73 100%);
  background-size: 300% 300%;
  box-shadow: inset 0 0 0 3px #aa7729;
  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 #aa7729, 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);
  /* One number for the card's inset: the divider inside it spans edge to
     edge by negating exactly this. */
  --box-pad: 40px;
  padding: var(--box-pad);
  /* max-height + scroll: the box now carries the clock setup as well, and a
     landscape phone has less height than the two halves together. */
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  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); }
/* Mirror .waiting-home-link's pill so the footer's two ends sit on the
   same visual baseline — plain text next to a pill read as misaligned. */
.room-hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 38px;
  height: 38px;
  gap: 5px;
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1;
  margin: 0;
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px dashed var(--border);
}
.room-hint strong { color: var(--text); letter-spacing: 0.06em; }
.waiting-footer .room-hint,
.waiting-box .room-hint {
  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. */
/* Two chips of equal width, not a flex row pushed to the two edges: the
   card above them is centred, so a wide gap between a short button and a
   long code read as a layout accident (tester 2026-09-14: "chỗ này cần căn
   lề cho cân đối"). Same width, same height, same gap as everything else. */
.waiting-footer {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
}
.waiting-home-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 38px;
  height: 38px;
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1;
  margin: 0;
  color: var(--text-muted);
  background: var(--hover);
  border: 1px solid var(--chip-bg-strong);
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.waiting-home-link:hover {
  background: var(--chip-bg-strong);
  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, #dfbd73, #ff6b6b, #c084fc, #bd4439);
  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: color-mix(in srgb, var(--danger) 12%, transparent); border-color: var(--danger); color: var(--danger); }
.rematch-hint {
  color: #aa7729 !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;
  position: relative;
}
.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); }
/* Top-right X close button — mirrors .cl-confirm-close so the
   promotion dialog matches the visual language of other popups. */
.promotion-cancel {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--chip-bg);
  border: none;
  color: var(--text-muted, #82756a);
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
  padding: 0;
}
.promotion-cancel:hover { background: var(--chip-bg-strong); color: var(--text, #f5ecdc); }
[data-theme="light"] .promotion-cancel { background: rgba(15, 23, 42, 0.06); }
[data-theme="light"] .promotion-cancel:hover { background: rgba(15, 23, 42, 0.12); }

/* ── 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; }
  .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: 6px; }
  /* The icon does NOT shrink with the wordmark. It used to be 0.75em of
     an 0.85rem font — a 10 px logo (owner 2026-09-16: "logo mobile gì mà
     nhỏ xíu xiu"). Fixed at 24 px, the same visual weight the lobby
     header gives it. */
  .logo-icon-sm { width: 24px; height: 24px; border-radius: 5px; }
  .game-title-badge { display: none; }
  .room-label { display: none; }
  .room-code { font-size: 0.82rem; letter-spacing: 1px; }
  /* .btn-copy goes icon-only in the ≤ 600 px block further down. */
  .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; }
  /* Keep the one-screen board cap in step with the shorter bars. */
  .game-body { --match-bar-h: 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))); }

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

/* 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; }
  .game-body { --match-bar-h: 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; }
  /* Four controls, a clock and a nickname in a 308 px panel. Everything
     on the right is fixed-width, so every pixel trimmed here is a pixel
     the nickname gets back before the ellipsis eats it. Still ≥ 26 px
     of tap target on each control. */
  .player-panel .panel-tools { gap: 6px; }
  .footer-floating.in-panel { gap: 4px; }
  .footer-floating.in-panel .theme-toggle {
    --track-w: 38px;
    --track-h: 26px;
    --thumb: 20px;
  }
  .footer-floating.in-panel .lang-selector.in-footer .lang-btn {
    height: 26px;
    padding: 0 6px;
  }
}

/* ══════════════════════════════════════════════════════════════════
   MOBILE RESPONSIVE PASS — fixes for invite-link flow on phones
   (audit-driven: header overflow, waiting overlay link box, panel
   row overflow, promotion dialog, xiangqi labels too tiny, etc.)
══════════════════════════════════════════════════════════════════ */

/* ── ≤ 600 px (phones — portrait + landscape) ─────────────────── */
@media (max-width: 600px) {
  /* Header: ONE row, always (owner 2026-09-16: "đoạn header đang chưa ở
     trên cùng 1 dòng"). Wrapping used to be the escape valve for an
     overflowing row, but a two-line header on a phone eats a whole
     board-row's worth of height and reads as two separate bars. The
     fix is the one §4 of the UI rules prescribes for a cramped
     toolbar: give width back instead of adding a line — the copy
     button goes icon-only below (it already did at ≤ 400 px), and the
     wordmark is the single shrinkable item, ellipsised as a last
     resort so the right-hand cluster never wraps or spills. */
  .game-header {
    flex-wrap: nowrap;
    padding-left: 10px;
    padding-right: 10px;
  }
  .game-header-left { gap: 8px; min-width: 0; flex-wrap: nowrap; flex: 0 1 auto; }
  .game-header-right { gap: 8px; min-width: 0; flex-wrap: nowrap; flex: 0 0 auto; }
  .logo-small { min-width: 0; }
  .logo-small span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Icon-only copy button from 600 px down, not just 400 px: "📋 Sao
     chép link" is ~70 px of the row and the 📋 alone already says it.
     The label stays in the DOM (font-size:0, not display:none) so a
     screen reader still announces it. */
  .btn-copy {
    font-size: 0;
    padding: 4px 7px;
    min-width: 0;
  }
  .btn-copy::before {
    content: '📋';
    font-size: 0.95rem;
    line-height: 1;
  }
  .game-title-badge { white-space: nowrap; }
  /* Captured-pieces row in panel: previous 30vw was too generous at
     320 px (90 px slot + total-clock pill overflowed the panel).
     Cap at 18 vw so the secondary row always fits. */
  .captured-pieces { max-width: 18vw; }
  /* Waiting overlay: long invite URL + "Sao chép" button could
     overflow at 320 px because .link-box was nowrap. Let them wrap. */
  .link-box {
    flex-wrap: wrap;
    row-gap: 6px;
  }
  .link-box > span { min-width: 0; }
  /* Toast was at bottom-center; on narrow viewports it collided
     with the bottom-right .footer-floating pill. Lift it above. */
  .toast { bottom: max(72px, calc(env(safe-area-inset-bottom, 0px) + 64px)); }
}

/* ── ≤ 400 px (small phones) ──────────────────────────────────── */
@media (max-width: 400px) {
  /* Header: drop low-priority chrome to free horizontal space.
     (.btn-copy already went icon-only at ≤ 600 px.) */
  .room-label { display: none; }
  .btn-copy { padding: 4px 6px; }
  /* Promotion dialog: 4 pieces × 72 px + gap = 324 px overflowed
     the 264 px content area on 320 px viewports. Shrink. */
  .promotion-box { padding: 18px 14px; }
  .promotion-pieces { gap: 8px; }
  .promo-piece { width: 54px; height: 54px; }
  .waiting-footer { grid-template-columns: 1fr; gap: 6px; align-items: stretch; }
  /* Hide the total-clock pill on the OPPONENT panel (top) only — keep
     it on .panel-bottom (me) since the player still wants to see
     their own remaining time. The opponent's total is implied by
     the now-in-panel move-timer on mobile. Frees up the secondary
     row for captured pieces. */
  .panel-top .total-clock { display: none; }
  /* Captured pieces cap even tighter at this width. */
  .captured-pieces { max-width: 14vw; }
}

/* ══════════════════════════════════════════════════════════════════
   PHÒNG TẬP (training room) — solo-play page + homepage entry
══════════════════════════════════════════════════════════════════ */

/* Footer entry button on the homepage. Sits in .footer-controls's
   first grid cell, on the same horizontal baseline as the theme +
   language toggles. Sized to match their 32 px row height. Ghost
   styling so it doesn't compete with the primary "Tạo phòng" CTAs
   above the fold. */
/* Footer entry points — "Giải đấu" moved down here from the header
   (owner 2026-09-14) so the header keeps only session state: online
   count, notifications, account. Both entries share one shape. */
.footer-training-btn,
.footer-nav-btn {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  height: 32px;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.82rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.footer-training-btn:hover,
.footer-nav-btn:hover {
  color: var(--accent);
  border-color: var(--accent);
  background: rgba(124, 92, 248, 0.08);
}
.footer-training-btn svg,
.footer-nav-btn svg { flex: 0 0 auto; opacity: 0.9; }
/* On /tournaments.html the entry points at the page you are already on. */
.footer-nav-btn.is-current { color: var(--accent); border-color: var(--accent); }

/* Picker modal — fixed-position overlay that lets the user choose
   which game type to train in before jumping to /training.html. */
.training-picker {
  position: fixed; inset: 0;
  background: rgba(7, 11, 28, 0.72);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 16px;
}
.training-picker.hidden { display: none; }
.training-picker-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  /* Top padding bumped so the absolute-positioned X close button
     (top: 10px, 28 px tall) sits above the card row instead of
     overlapping the rightmost card's top-right corner. */
  padding: 48px 24px 24px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  max-width: 560px;
  width: 100%;
  position: relative;
}
.training-picker-box h3 {
  margin: 0 0 18px;
  font-size: 1.15rem;
  font-weight: 700;
  text-align: center;
  color: var(--text);
}
.training-picker-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}
.training-pick-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 18px 10px;
  background: var(--card);
  border: 1.5px solid var(--border);
  border-radius: 14px;
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
  transition: transform 0.15s ease, border-color 0.18s ease, background 0.18s ease;
}
.training-pick-card:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  background: rgba(124, 92, 248, 0.08);
}
.training-pick-icon {
  font-size: 2rem;
  line-height: 1;
}
.training-pick-label {
  font-size: 0.95rem;
  letter-spacing: 0.1px;
}
/* Top-right X close button — sized + styled to match .profile-close
   (the platform's reference popup X). Same 44×44 footprint, border,
   font-size, and corner placement so every modal's close X reads
   identical to a user navigating between profile / picker / etc. */
.training-picker-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;
  padding: 0;
}
.training-picker-close:hover { color: var(--text); background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); }
.training-picker-close:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
[data-theme="light"] .training-picker-close {
  background: rgba(15, 23, 42, 0.05);
  border-color: var(--border);
  color: var(--text-muted);
}
[data-theme="light"] .training-picker-close:hover {
  background: rgba(15, 23, 42, 0.10);
  color: var(--text);
  border-color: rgba(15, 23, 42, 0.18);
}
@media (max-width: 480px) {
  .training-picker-grid { grid-template-columns: 1fr; }
  .training-pick-card { flex-direction: row; padding: 14px 16px; justify-content: flex-start; gap: 14px; }
  .training-pick-icon { font-size: 1.6rem; }
}

/* ── Training page (/training.html) ──────────────────────────────
   Layout reuses .game-main / .game-layout / .board-wrapper /
   .player-panel from the multiplayer game page so the board sizing
   + container queries + mobile breakpoints carry over unchanged.
   Only training-SPECIFIC chrome lives here: the side-avatar glyph
   that stands in for the player image, the highlighted-panel
   accent for whose-turn-it-is, and the history drawer that slides
   in from the side without pushing the board around. */

/* Side avatar: solid disc with a centred piece glyph (♔ for chess,
   帥/將 for xiangqi, × / ○ for gomoku). Replaces the real <img> the
   multiplayer panels use. */
.training-side-avatar {
  background: var(--card);
  border: 2px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: default;
}
.training-side-glyph {
  font-size: 1.35rem;
  line-height: 1;
  color: var(--text);
  font-family: 'LXGW WenKai TC', 'Noto Serif SC', 'KaiTi', 'STKaiti', 'SimSun', serif;
}
/* Highlight the panel whose side is currently to move so the user
   can see at-a-glance whose turn it is without consulting the
   status-bar. Mirrors the accent ring the resign/draw treatment
   uses elsewhere. */
.training-turn-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 6px 18px rgba(124, 92, 248, 0.15);
}
.training-turn-active .training-side-avatar {
  border-color: var(--accent);
}

/* History drawer — sliding panel anchored to the right viewport
   edge (desktop) or the bottom (mobile). Opens via the header
   history toggle, dismisses via Escape or its own × button. */
.training-history-drawer {
  position: fixed;
  top: 60px;
  right: 12px;
  width: 280px;
  max-height: calc(100vh - 80px);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 90;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
}
.training-history-drawer.hidden { display: none; }
.training-history-header {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.training-history-header h3 {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text);
}
.training-history-close {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  width: 24px;
  height: 24px;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.7rem;
}
.training-history-close:hover { color: var(--text); border-color: var(--text-muted); }
.training-move-count {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-size: 0.78rem;
  color: var(--text-muted);
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  justify-self: end;
}
.training-history {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  max-height: 320px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.training-history-item {
  display: grid;
  grid-template-columns: 32px 1fr;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.82rem;
  transition: background 0.12s ease;
}
.training-history-item:hover { background: rgba(124, 92, 248, 0.1); }
.training-history-num {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-size: 0.75rem;
  color: var(--text-muted);
  text-align: right;
}
.training-history-text {
  color: var(--text);
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-size: 0.82rem;
}

/* Mobile: anchor the drawer to the BOTTOM edge so the rest of the
   game chrome stays visible behind it; cap its height. */
@media (max-width: 600px) {
  .training-history-drawer {
    top: auto;
    bottom: max(8px, env(safe-area-inset-bottom, 0px));
    right: 8px;
    left: 8px;
    width: auto;
    max-height: 50vh;
    border-radius: 14px;
  }
  .training-history { max-height: 36vh; }
  .training-side-glyph { font-size: 1.05rem; }
}

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

/* Bio mirrors the nickname layout — textarea + save button on a
   single row, with a foot row beneath that just carries the char
   counter. Save button anchors to the TOP of the row so it doesn't
   stretch to the textarea's min-height of 96 px. */
.profile-bio-row {
  display: flex;
  gap: 8px;
  align-items: flex-start;
}
.profile-bio-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  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: #78b39c;
  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: #d98c83;
  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 var(--on-accent);
  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, #35705d, #35705d) !important;
  color: white !important;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 18%, transparent);
  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 var(--chip-bg-strong);
  border-radius: 10px;
  background: var(--surface);
  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: var(--surface);
  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 var(--chip-bg-strong);
  border-radius: 10px;
  background: var(--surface);
  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: var(--surface);
  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, #a6332b);
}
.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, color-mix(in srgb, var(--accent) 35%, transparent));
}
.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: color-mix(in srgb, var(--accent) 4%, transparent);
  border: 1px dashed color-mix(in srgb, var(--accent) 18%, transparent);
}
.friends-empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin-bottom: 10px;
  border-radius: 50%;
  color: #a6332b;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.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: var(--chip-bg);
}
.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: #35705d;
  /* The ring punches the dot out of whatever surface it sits on, so it has
     to follow the theme rather than assume a dark card. */
  border: 2px solid var(--card);
  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: color-mix(in srgb, var(--accent) 18%, transparent); color: #a6332b; 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: #78b39c;
  cursor: default;
}
/* Outgoing pending: amber, non-clickable */
.friend-add-btn.is-pending {
  background: rgba(251, 191, 36, 0.14);
  color: #c9963e;
  cursor: default;
}
/* Incoming pending: stays clickable (shows ✓ Đồng ý) */
.friend-add-btn.is-incoming {
  background: rgba(110, 231, 183, 0.18);
  color: #78b39c;
}
.friend-accept { background: rgba(110, 231, 183, 0.18); color: #78b39c; }
.friend-decline { background: rgba(252, 165, 165, 0.14); color: #d98c83; }
.friend-remove {
  background: transparent;
  color: var(--text-muted);
  padding: 4px 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.friend-remove svg { display: block; }
.friend-remove:hover { color: #d98c83; 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 var(--chip-bg-strong);
  background: var(--hover);
  color: var(--text-muted);
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 0.82rem;
  cursor: pointer;
  font-weight: 500;
  /* Flex so the count badge sits nicely next to the label with a
     consistent inline gap. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.hist-filter:hover { background: var(--hover-strong); color: var(--text); }
.hist-filter.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  color: #a6332b;
}
/* Small numeric badge inside each filter pill — total games for that
   variant ("Tất cả 20", "Cờ vua 14", etc.). Tonal chip so it reads
   as a sub-piece of the pill rather than competing with the label. */
.hist-filter-count {
  font-size: 0.74rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--chip-bg-strong);
  color: var(--text-muted);
  line-height: 1.3;
}
.hist-filter.active .hist-filter-count {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: #d98c83;
}
/* Replay links on a tournament pairing — one ▶ per recorded game of the
   series, same circular affordance as .hist-replay but sized to sit
   inside the compact match row. */
.tour-match-replays {
  display: inline-flex;
  gap: 4px;
  align-items: center;
  flex-shrink: 0;
}
.tour-match-replay {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 24px;
  height: 24px;
  padding: 0 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  color: #a6332b;
  text-decoration: none;
  font-size: 0.7rem;
  font-weight: 700;
}
.tour-match-replay:hover {
  background: color-mix(in srgb, var(--accent) 25%, transparent);
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
}
.tour-match-replay svg { display: block; }

/* ── Move-quality analysis (replay page, chess) ──────────────────────
   Badge colors follow chess convention: teal-ish for brilliant, green
   for great/best, amber for inaccuracy, orange for mistake, red for
   blunder. Direction tokens --ok/--danger anchor the two extremes so
   both themes stay coherent; the middle grades use fixed hues that read
   on paper and stage alike. */
.replay-btn-analyze { white-space: nowrap; }
.rp-an-badge {
  margin-left: 6px;
  font-weight: 800;
  font-size: 0.78rem;
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
}
.rp-an-brilliant  { color: #1baaa6; }
.rp-an-great      { color: var(--ok); }
.rp-an-best       { color: var(--ok); opacity: 0.85; }
.rp-an-good       { color: var(--text-muted); }
.rp-an-inaccuracy { color: #c9963e; }
.rp-an-mistake    { color: #d0742c; }
.rp-an-blunder    { color: var(--danger); }

.replay-analysis-summary {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 10px auto 0;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  font-size: 0.85rem;
  max-width: 640px;
}
.rp-an-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.rp-an-player { font-weight: 700; margin-right: 4px; }
.rp-an-chip {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-size: 0.76rem;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
}
.rp-an-acpl {
  margin-left: auto;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
}

/* "🏆 <giải> · Vòng n" link in the replay header. */
.replay-tournament-link {
  color: var(--accent2);
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
}
.replay-tournament-link:hover { text-decoration: underline; }

/* Rating movement chip on a history row — "+12" / "−8". Sits on the top
   line next to the opponent name; direction colors follow --ok/--danger
   so both themes stay legible. Rows older than the Elo-snapshot columns
   simply don't render the chip. */
.hist-elo {
  margin-left: auto;
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  flex: 0 0 auto;
}
.hist-elo--up   { color: var(--ok); }
.hist-elo--down { color: var(--danger); }
.hist-elo--flat { color: var(--text-muted); }

/* "3/16" counter chip on the achievements accordion row — same tonal
   family as .hist-filter-count. */
.fbp-row-count {
  font-size: 0.74rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  padding: 1px 7px;
  margin-left: 8px;
  border-radius: 999px;
  background: var(--chip-bg-strong);
  color: var(--text-muted);
  line-height: 1.3;
}

/* Badge wall inside the achievements accordion. Auto-fill grid so 16
   badges wrap by themselves at any modal width; locked cells sit dim
   with a padlock where the unlock date would be. */
.ach-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  margin: 0;
  padding: 2px;
}
.ach-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
  padding: 10px 6px 8px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface);
  opacity: 0.45;
  filter: grayscale(0.8);
}
.ach-cell--unlocked {
  opacity: 1;
  filter: none;
  border-color: var(--border-strong);
  box-shadow: var(--shadow-sm);
}
.ach-icon { font-size: 1.5rem; line-height: 1.2; }
.ach-name {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.25;
}
.ach-when {
  font-size: 0.68rem;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  min-height: 1em;
}

.profile-history-more {
  /* Sits INSIDE .profile-history-body, just below the list. Full-width
     so it reads as the natural "footer" of the scrollable history box —
     user has to scroll down to see it, matching the requested UX. */
  display: block;
  width: 100%;
  margin: 0;
  border: none;
  border-top: 1px solid var(--hover);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: #a6332b;
  padding: 10px 16px;
  font-weight: 600;
  cursor: pointer;
  font-size: 0.86rem;
  text-align: center;
}
.profile-history-more:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.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: 28px; height: 28px; }
  .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 { --icon-size: 52px; }
  .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 { --icon-size: 42px; gap: 8px; padding: 11px 12px; }
  .game-card .btn-primary,
  .game-card .btn-secondary { padding: 8px 10px; font-size: 0.8rem; }
  .game-card .btn-icon { display: none; }
}

/* ── 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: var(--chip-bg); }
.notif-bell:focus-visible { outline: none; background: color-mix(in srgb, var(--accent) 12%, transparent); }
.notif-bell svg { display: block; }
.notif-dot {
  position: absolute;
  top: 6px; right: 6px;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: #bd4439;
  border: 2px solid var(--bg, var(--on-accent));
  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, #202a24);
  border: 1px solid var(--chip-bg-strong);
  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 var(--chip-bg);
  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: var(--chip-bg-strong);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
.notif-panel-list:hover::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  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: var(--chip-bg); }
.notif-item.notif-clickable { cursor: pointer; }
.notif-item.notif-clickable:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* 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, #a6332b);
  box-shadow: 0 0 0 2px var(--card, #202a24);
}

.notif-avatar, .notif-avatar-fallback {
  width: 56px;               /* FB-size avatar */
  height: 56px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--chip-bg);
  border: 1px solid var(--chip-bg);
}
.notif-avatar-fallback { background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 18%, transparent), color-mix(in srgb, var(--accent2) 18%, transparent)); }

.notif-icon {
  width: 56px; height: 56px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--chip-bg);
  font-size: 1.4rem;
}
.notif-icon-promo { background: rgba(251, 191, 36, 0.15); }
.notif-icon-admin { background: color-mix(in srgb, var(--accent) 15%, transparent); }
.notif-icon-game  { background: color-mix(in srgb, var(--accent2) 15%, transparent); }

.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: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  color: var(--accent, #a6332b);
  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: #651e19;
}
.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, #a6332b);
  color: var(--on-accent);
}
.notif-btn-primary:hover { filter: brightness(1.08); }
.notif-btn-secondary {
  background: transparent;
  color: var(--text-muted);
  border-color: var(--chip-bg-strong);
}
.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, #202a24);
  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: color-mix(in srgb, var(--accent2) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent2) 32%, transparent);
  color: #ecd49a;
  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: color-mix(in srgb, var(--accent2) 26%, transparent); border-color: color-mix(in srgb, var(--accent2) 50%, transparent); }
.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) {
  /* Fixed to the viewport, not to the bell. The bell is not the
     right-most header control (the avatar chip is), so a panel hung
     off the bell with `right:-8px` and a viewport-wide width ran
     past the LEFT edge of the screen — "Thông báo" rendered as
     "hông báo" (owner 2026-09-16: "ở mobile lỗi khi mở thông báo").
     top clears the tallest header variant (the lobby's, 65 px on a
     phone; the match header is 49 px) plus the notch inset. */
  .notif-panel {
    position: fixed;
    top: calc(70px + env(safe-area-inset-top, 0px));
    left: 8px;
    right: 8px;
    width: auto;
    max-width: none;
    max-height: calc(100vh - 86px - env(safe-area-inset-top, 0px));
  }
}

/* ── 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;
  }
  .home-lb-table-wrap,
  .auth-user,
  .btn-google-signin,
  .footer {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
}

/* ── Replay viewer (replay.html) ─────────────────────────────────────── */
/* Symmetric vertical padding — tightened so the whole viewer (header,
   players, board, controls, slider, footer) fits inside one viewport
   height on a standard 1080 p desktop without page scroll. */
.replay-page main {
  padding-top: clamp(8px, 1.2vw, 18px);
  padding-bottom: clamp(8px, 1.2vw, 18px);
}
/* Bumped from the default 1100 px so the side-by-side layout can fit
   two equal-width panels (board + movelist) + gap without crushing
   either column. Anything above 1320 reads as wasted air. */
.replay-page .container { max-width: 1320px; }
.replay-loading, .replay-error {
  padding: 80px 16px;
  text-align: center;
  color: var(--text-muted);
  font-size: 1.05rem;
}
.replay-error { color: #d98c83; }
.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 var(--chip-bg-strong);
  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;
  /* Holds a player name: a long unbroken one must wrap inside the pill
     rather than push it past a 320px screen. */
  max-width: 100%;
  overflow-wrap: anywhere;
  color: #aa7729;
  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: var(--surface);
  border: 1px solid var(--chip-bg);
  border-radius: 12px;
  padding: 14px 18px;
}
.replay-player { display: flex; align-items: center; gap: 12px; flex: 1 1 0; min-width: 0; }
.replay-player-right { justify-content: flex-end; }
.replay-player-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.replay-player-right .replay-player-meta { align-items: flex-end; text-align: right; }
.replay-player-color { font-size: 0.85rem; color: var(--text-muted); font-weight: 500; }
.replay-player-name {
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
  /* A name stays inside its half of the row and is shown IN FULL: it
     wraps (at spaces, or anywhere inside one long word) instead of being
     cut with "…" — a player's name is never truncated (frontend-
     implementation §6b). It used to be an inline-flex nowrap box, which
     neither wrapped nor shrank, so on the end-aligned right-hand side a
     long name grew leftwards over the other player (375px, two long
     names). A block box bounded by max-width fixes both. */
  display: block;
  max-width: 100%;
  overflow-wrap: anywhere;
}
/* Winner highlight — pure white + bold so the name pops against the
   default --text. Applied via JS to the .replay-player container so
   it's a single styling hook that works for all 3 game variants. */
.replay-player.replay-player-winner .replay-player-name {
  color: #ffffff;
  font-weight: 700;
}
[data-theme="light"] .replay-player.replay-player-winner .replay-player-name {
  /* Pure white disappears on the light theme — use the strongest
     dark text token instead so the contrast still reads. */
  color: #201b17;
}
.replay-player-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--card);
  border: 2px solid var(--chip-bg-strong);
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  font-weight: 700;
  color: var(--text);
  font-size: 1.1rem;
}
.replay-player-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.replay-player-initial {
  width: 100%; height: 100%;
  display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, var(--accent2), var(--accent));
  color: #fff;
}
.replay-vs { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 0 0 auto; }
@media (max-width: 540px) {
  /* Phones: avatar above the name, so the name gets the whole half of the
     row (~100px at 320) instead of what is left beside an avatar (~50px,
     which showed two letters and an ellipsis). */
  .replay-players { gap: 8px; padding: 12px 10px; align-items: flex-start; }
  .replay-player,
  .replay-player-right { flex-direction: column; align-items: center; justify-content: flex-start; gap: 6px; text-align: center; }
  .replay-player-right .replay-player-avatar { order: -1; }
  .replay-player-meta,
  .replay-player-right .replay-player-meta { align-items: center; text-align: center; width: 100%; }
  .replay-player-avatar { width: 36px; height: 36px; font-size: 0.95rem; }
  .replay-player-name { font-size: 0.95rem; }
  .replay-player-color { font-size: 0.78rem; }
  .replay-vs { align-self: center; }
}
.replay-vs-label { color: var(--text-muted); font-size: 0.78rem; letter-spacing: 0.5px; }
.replay-score-chip {
  font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.8px;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  white-space: nowrap;
}
/* Tint the chip by who won: gold for a result, neutral for draw. */
.replay-score-chip[data-winner="p1"],
.replay-score-chip[data-winner="p2"] {
  background: linear-gradient(135deg, rgba(245, 158, 11, 0.18), rgba(245, 158, 11, 0.08));
  border-color: rgba(245, 158, 11, 0.55);
  color: #fcd34d;
}
.replay-score-chip[data-winner="draw"] {
  background: rgba(148, 163, 184, 0.14);
  border-color: rgba(148, 163, 184, 0.35);
  color: var(--text);
}

/* Board stage: reuse the live-game .board-wrapper inside, which
   already carries the container-query sizing rules. In live game.html
   the wrapper gets its height from a constrained .game-layout, so the
   cell formulas' `100cqh` arm resolves correctly. In replay there's
   no such height constraint — the wrapper would otherwise size to
   content, creating a circular dependency (cell size ↘ wrapper height
   ↘ cqh ↘ cell size) that collapsed the board to tiny cells.
   Fix: pin a per-game aspect-ratio on the wrapper so cqh is defined
   purely from the wrapper's width. Also override `flex: 1` (which the
   live layout sets) so the wrapper takes its declared width instead
   of stretching across the row. */
.replay-board-stage {
  display: flex;
  justify-content: center;
  padding: 12px 0;
}
.replay-board-stage .board-wrapper {
  flex: none;
  /* Disable interaction — the live modules' render attaches click
     handlers, but rebuildAtIndex sets gameOver=true so they bail.
     Adding pointer-events: none is belt-and-suspenders in case any
     module gains a side-effect-free hover handler later. */
  pointer-events: none;
}
/* Per-game aspect-ratios only. Width comes from the flex parent
   (.replay-main-row -> .replay-board-stage with flex:1 1 0 +
   per-game max-width). 70% of the previous board caps so the
   board column is "balanced" with the movelist column beside it. */
.replay-content[data-game="chess"] .replay-board-stage .board-wrapper {
  width: 100%;
  aspect-ratio: 8 / 9;
}
.replay-content[data-game="xiangqi"] .replay-board-stage .board-wrapper {
  width: 100%;
  aspect-ratio: 9 / 12;
}
.replay-content[data-game="gomoku"] .replay-board-stage .board-wrapper {
  width: 100%;
  aspect-ratio: 12 / 21;
}

.replay-board-wrap {
  display: flex;
  justify-content: center;
  padding: 8px 0;
}
.replay-board { /* legacy text-glyph board — kept for compat with any
                   in-app links that still target the old DOM. */
  max-width: 100%;
}
.rp-grid {
  display: grid;
  background: var(--surface);
  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: #aa7729;
  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 var(--chip-bg-strong);
  background: var(--surface);
  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: color-mix(in srgb, var(--accent) 16%, transparent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.replay-btn-play { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
.replay-progress {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text-muted);
  min-width: 80px;
  text-align: center;
}
.replay-speed {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--chip-bg-strong);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 0.9rem;
  cursor: pointer;
}
.replay-slider {
  width: 100%;
  appearance: none;
  height: 6px;
  border-radius: 999px;
  background: var(--chip-bg-strong);
  outline: none;
  cursor: pointer;
}
.replay-slider::-webkit-slider-thumb {
  appearance: none;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #a6332b;
  border: 2px solid #fff;
  cursor: pointer;
}
.replay-slider::-moz-range-thumb {
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #a6332b;
  border: 2px solid #fff;
  cursor: pointer;
}

.replay-movelist-wrap {
  background: var(--surface);
  border: 1px solid var(--chip-bg);
  border-radius: 12px;
  padding: 14px 18px;
  /* Standalone fallback cap (rarely triggered now that .replay-main-row
     hosts the movelist side-by-side with the board). */
  max-height: 240px;
  overflow-y: auto;
}

/* Side-by-side row: board on the left (~60 %), movelist on the right
   (~40 %). Grid + aspect-ratio approach — turned out CSS Grid's
   `auto` row track uses each item's max-content size, so a long
   move list still pushed the row taller than the board even with
   min-height:0 + overflow:hidden. Pin movelist height geometrically
   via aspect-ratio (per-game block below), `box-sizing: border-box`
   so the 14 px vertical padding lives INSIDE the aspect box (not
   added on top), and `align-items: start` so the row track never
   stretches the items past their declared heights. */
.replay-main-row {
  display: grid;
  grid-template-columns: 6fr 4fr;
  gap: 18px;
  align-items: start;
  justify-content: center;
  /* Tight top/bottom margin so controls/slider sit close to the
     board (user wanted them moved up). Width inherits from
     .replay-content which is vh-clamped per game-type below. */
  padding-block: clamp(4px, 0.6vw, 10px);
}
.replay-main-row > .replay-board-stage,
.replay-main-row > .replay-movelist-wrap {
  min-width: 0;
}
.replay-main-row > .replay-board-stage {
  padding: 0;
}
/* Pin the board to the LEFT edge of its column instead of centering
   inside the wrapper — without this, the chess/xiangqi/gomoku grid
   sits 6-12 px right of the player-chip card above (which is full
   column width) and the misalignment is visible at the left margin.
   Applies only in replay (`.replay-main-row` scope) so the live
   game page's centered board layout is untouched. */
.replay-main-row > .replay-board-stage > .board-wrapper {
  align-items: flex-start;
}
.replay-main-row > .replay-movelist-wrap {
  box-sizing: border-box;
  min-height: 0;
  max-height: none;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.replay-main-row .replay-movelist-title {
  flex: none;
}
.replay-main-row .replay-movelist {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* 2 moves per row, vertical scroll for the rest. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* align-content: start packs the row-grid to the TOP of the
     scrollable area. Without this, short games (gomoku usually
     finishes in 15-30 moves) had the rows auto-stretched to fill
     the movelist's full height, so each cell looked loose — chess
     stayed tight only because it had enough moves to fill the box. */
  align-content: start;
  gap: 4px;
  /* Styled scrollbar so chess + xiangqi + gomoku all show the same
     slim accent track instead of falling back to OS default (which
     varied between browsers + game types). */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
.replay-main-row .replay-movelist::-webkit-scrollbar { width: 8px; }
.replay-main-row .replay-movelist::-webkit-scrollbar-track { background: transparent; }
.replay-main-row .replay-movelist::-webkit-scrollbar-thumb {
  background: var(--chip-bg-strong);
  border-radius: 4px;
}
.replay-main-row .replay-movelist::-webkit-scrollbar-thumb:hover {
  background: var(--border-strong);
}
.replay-main-row .rp-move {
  padding: 6px 10px;
  font-size: 0.86rem;
  gap: 6px;
  min-width: 0;
}
.replay-main-row .rp-move-ts {
  font-size: 0.76rem;
  flex-shrink: 0;
}

/* Movelist height is JS-driven (replay.js -> syncMovelistHeight)
   — copies the actually-rendered board element's offsetHeight to
   the movelist wrap so alignment is exact across game types and
   viewports. CSS aspect-ratio + container-query calc never landed
   pixel-perfect because the cell formulas reserve constants the
   wrapper aspect doesn't carry. Reset aspect-ratio so the inline
   style from JS wins. */
.replay-main-row > .replay-movelist-wrap {
  aspect-ratio: auto;
}

/* Per-game max-widths on .replay-content — set via plain height-based
   media queries instead of vh-in-calc (which some browsers silently
   ignore inside min()/max()). Empirical caps so the whole layout fits
   on a standard 1080p desktop, with stepped ladders for taller and
   shorter screens. .replay-content gets container-type:inline-size so
   the movelist's cqw math above queries this element's width. */
.replay-content {
  container-type: inline-size;
  margin-inline: auto;
}
.replay-content[data-game="chess"]   { max-width: 1080px; }
.replay-content[data-game="xiangqi"] { max-width: 960px; }
.replay-content[data-game="gomoku"]  { max-width: 720px; }

@media (min-height: 1200px) {
  .replay-content[data-game="chess"]   { max-width: 1300px; }
  .replay-content[data-game="xiangqi"] { max-width: 1100px; }
  .replay-content[data-game="gomoku"]  { max-width: 800px; }
}
@media (max-height: 900px) {
  .replay-content[data-game="chess"]   { max-width: 880px; }
  .replay-content[data-game="xiangqi"] { max-width: 780px; }
  .replay-content[data-game="gomoku"]  { max-width: 580px; }
}
@media (max-height: 720px) {
  .replay-content[data-game="chess"]   { max-width: 680px; }
  .replay-content[data-game="xiangqi"] { max-width: 600px; }
  .replay-content[data-game="gomoku"]  { max-width: 460px; }
}

/* Very-narrow phones: stack so each panel can use the full width
   instead of fighting over <200 px each. Grid switches from 6fr/4fr
   to a single column; the vh-clamp on .replay-content is dropped so
   the layout uses the full viewport width on mobile. */
@media (max-width: 540px) {
  .replay-main-row {
    grid-template-columns: 1fr;
  }
  .replay-content[data-game="chess"],
  .replay-content[data-game="xiangqi"],
  .replay-content[data-game="gomoku"] {
    max-width: none;
  }
}
.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: color-mix(in srgb, var(--accent) 12%, transparent); }
.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.). */
/* 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. */
/* 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: #651e19;
  color: #651e19;
}

/* Online badge: green pulse dot and matching green count and label */
[data-theme="light"] .header-online { color: #16a34a; }
[data-theme="light"] .header-online strong { color: #15803d; }
[data-theme="light"] .online-dot {
  background: #16a34a;
  box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.45);
}

/* 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; }
/* One token drives all three glyphs (see .chess-icon / .xiangqi-icon /
   .gomoku-icon): ink on paper in light, ivory on wood in dark. */
[data-theme="light"] .game-card { --piece-ink: #201b17; }

/* 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: #fbf6eb;
  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;
}

/* 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, #fbf6eb, #fff); }
[data-theme="light"] .fbp-stats-card { background: #fbf6eb; 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: #201b17;
  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: #a6332b;
  border-color: #87271f;
}

/* 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: var(--hover);
  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: var(--chip-bg-strong); 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; }
}

/* Wrapper used on the training page — the lang picker and theme
   toggle both sit inside one floating pill anchored to bottom-right.
   game.html used to render it too and no longer does.
   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; }
/* Lifted clear of the bottom player bar when the two would collide —
   the 768-900 px band, where a wide board leaves no corner margin.
   setupPanelTools() toggles the class and measures --panel-lift off
   the bar; the fallback only ever shows if that script never ran. */
.footer-floating.above-panel {
  bottom: var(--panel-lift, 84px);
}
@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 var(--chip-bg-strong);
  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,
    #a6332b 0%,
    #bd4439 18%,
    #aa7729 38%,
    #aa7729 58%,
    #a6332b 78%,
    #35705d 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;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   Giải đấu (tournaments)

   Everything below draws on the tokens already defined at the top of this
   file — paper for surfaces, ink for text, red for the accent, gold for
   the trophy, jade for a finished round. No literal colour appears here,
   so the whole section follows the theme without a second definition.
   ═══════════════════════════════════════════════════════════════════ */

.tour-main { padding: 32px 0 64px; }

.tour-page-title {
  font-size: clamp(1.6rem, 4vw, 2.2rem);
  margin: 0;
  letter-spacing: -0.02em;
}
.tour-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.tour-back { margin: 0 0 14px; font-size: 0.9rem; }
.tour-back a { color: var(--text-muted); text-decoration: none; }
.tour-back a:hover { color: var(--accent); }

/* ── The list ─────────────────────────────────────────────────────── */

.tour-list { display: grid; gap: 12px; }
.tour-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--card);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.18s ease, transform 0.18s ease;
}
.tour-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.tour-card-glyph {
  font-size: 1.7rem;
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border-radius: 12px;
  background: var(--chip-bg);
  color: var(--accent);
}
.tour-card-main { display: grid; gap: 3px; min-width: 0; }
.tour-card-name { font-weight: 700; font-size: 1.02rem; }
.tour-card-meta, .tour-card-owner { font-size: 0.82rem; color: var(--text-muted); }
.tour-card-side { display: grid; gap: 6px; justify-items: end; }
.tour-card-count {
  font-size: 0.82rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.tour-empty {
  padding: 28px;
  text-align: center;
  color: var(--text-muted);
  border: 1px dashed var(--border-strong);
  border-radius: 16px;
}

/* ── Status pills ─────────────────────────────────────────────────── */

.tour-status {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  border: 1px solid var(--border);
  background: var(--chip-bg);
  color: var(--text-muted);
  white-space: nowrap;
}
.tour-status-open { background: var(--jade-50); color: var(--jade-700); border-color: var(--jade-200); }
.tour-status-playing, .tour-status-running { background: var(--red-50); color: var(--red-700); border-color: var(--red-100); }
.tour-status-awaiting_round, .tour-status-pending { background: var(--gold-50); color: var(--gold-700); border-color: var(--gold-100); }
.tour-status-finished { background: var(--chip-bg); color: var(--ink-600); }
.tour-status-cancelled { background: var(--chip-bg); color: var(--ink-400); }

/* ── The create form ──────────────────────────────────────────────── */

.tour-create-box {
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--card);
  padding: 18px;
  margin-bottom: 20px;
}
.tour-create-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
}
.tour-field { display: grid; gap: 6px; font-size: 0.84rem; font-weight: 600; }
.tour-field input, .tour-field select {
  padding: 9px 11px;
  border-radius: 10px;
  border: 1px solid var(--border-strong);
  background: var(--thumb-bg);
  color: var(--text);
  font: inherit;
  font-weight: 500;
}
.tour-field input:focus-visible, .tour-field select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.tour-field input:disabled { opacity: 0.5; cursor: not-allowed; }
.tour-create-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; }

/* ── The detail head ──────────────────────────────────────────────── */

.tour-head {
  border: 1px solid var(--border);
  border-radius: 18px;
  background: var(--card);
  padding: 20px 22px;
  margin-bottom: 18px;
}
.tour-head-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.tour-title { margin: 0; font-size: clamp(1.3rem, 3.5vw, 1.9rem); letter-spacing: -0.02em; }
.tour-head-meta {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  margin: 10px 0 0;
  font-size: 0.86rem;
  color: var(--text-muted);
}
.tour-champion {
  margin: 12px 0 0;
  padding: 10px 14px;
  border-radius: 12px;
  background: var(--gold-50);
  border: 1px solid var(--gold-100);
  color: var(--gold-700);
  font-size: 0.94rem;
}
.tour-head-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.tour-head-actions:empty { margin-top: 0; }
.tour-pill {
  display: inline-flex; align-items: center;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--gold-50);
  border: 1px solid var(--gold-100);
  color: var(--gold-700);
  font-size: 0.85rem;
  font-weight: 600;
}
.tour-pill.is-muted { background: var(--chip-bg); border-color: var(--border); color: var(--text-muted); }

/* ── "Your match is ready" ────────────────────────────────────────── */

/* The one banner that gets the accent fill: it is the only thing on the
   page that asks the reader to do something right now. */
.tour-mymatch {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  padding: 16px 20px;
  border-radius: 16px;
  background: var(--accent);
  color: var(--on-red);
  margin-bottom: 18px;
}
.tour-mymatch-text { display: grid; gap: 3px; }
.tour-mymatch-text strong { font-size: 1.02rem; }
.tour-mymatch-text span { font-size: 0.86rem; opacity: 0.9; }
.tour-mymatch .btn-primary { background: var(--on-red); color: var(--accent); border-color: transparent; }
.tour-mymatch .btn-primary:hover { background: var(--thumb-bg); color: var(--accent-hover); }

/* ── Sections ─────────────────────────────────────────────────────── */

.tour-section-title {
  font-size: 1.05rem;
  margin: 24px 0 12px;
  letter-spacing: -0.01em;
}

.tour-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 14px; background: var(--card); }
.tour-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.tour-table th {
  text-align: left;
  padding: 11px 14px;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.tour-table td { padding: 10px 14px; border-bottom: 1px solid var(--border); }
.tour-table tr:last-child td { border-bottom: none; }
.tour-table tr.is-me td { background: var(--red-50); }
.tour-rank, .tour-points, .tour-wdl { font-variant-numeric: tabular-nums; }
.tour-points { font-weight: 700; }
.tour-player { display: flex; align-items: center; gap: 9px; }
/* A long unbroken name breaks inside its cell instead of widening the table
   past the screen (it pushed the standings 48px out at 320px). */
.tour-player > span:not(.tour-avatar) {
  min-width: 0;
  /* shown in full: wraps at spaces, or inside one very long word */
  overflow-wrap: anywhere;
}
/* The name column takes the room: the other columns are as narrow as their
   numbers, so at 320px a name gets ~150px instead of ~35px. */
.tour-table th:nth-child(2),
.tour-table td.tour-player { width: 100%; }
/* The short columns never break ("1-0-\n0" read as two numbers). */
.tour-table .tour-points,
.tour-table .tour-wdl,
.tour-table .tour-rank { white-space: nowrap; }

.tour-avatar {
  width: 26px; height: 26px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--chip-bg-strong);
}
.tour-avatar-fallback {
  display: grid; place-items: center;
  font-size: 0.75rem; font-weight: 700;
  color: var(--ink-700);
}

/* ── Join requests ────────────────────────────────────────────────── */

.tour-applicants { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.tour-applicant {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
}
.tour-applicant-name { flex: 1; font-weight: 600; min-width: 0; }

/* ── Rounds and pairings ──────────────────────────────────────────── */

.tour-round { margin-bottom: 18px; }
.tour-round-title {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 0.95rem;
  margin: 0 0 8px;
}
.tour-round-deadline { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
.tour-round-status { font-size: 0.7rem; }

.tour-matches { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.tour-match {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
}
.tour-match.is-mine { border-color: var(--accent); }
.tour-match.is-bye { grid-template-columns: 1fr auto; }
.tour-match-side { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 0.9rem; }
.tour-match-side span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The right-hand player mirrors: name first, then avatar, so the two
   portraits sit on the outside of the pairing and the score in the middle.
   Keyed on a class rather than :nth-of-type, because the score element is
   a span too and would win that match. */
.tour-match-side.is-p2 {
  /* row-reverse flips the main axis, so flex-START is what packs the pair
     against the right-hand edge of the row. flex-end would push it back
     towards the score, which is the wrong side. */
  flex-direction: row-reverse;
  justify-content: flex-start;
  text-align: right;
}
.tour-match-side.is-winner { font-weight: 700; color: var(--jade-700); }
.tour-match-score {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  padding: 2px 10px;
  border-radius: 8px;
  background: var(--chip-bg);
  white-space: nowrap;
}
.tour-match-enter {
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-red);
  font-size: 0.8rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
.tour-match-enter:hover { background: var(--accent-hover); }

/* ── Toast ────────────────────────────────────────────────────────── */

.tour-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 90;
  max-width: min(90vw, 460px);
  padding: 12px 18px;
  border-radius: 12px;
  font-size: 0.9rem;
  font-weight: 600;
  box-shadow: 0 8px 24px rgba(32, 27, 23, 0.18);
}
.tour-toast.is-error { background: var(--red-700); color: var(--on-red); }
.tour-toast.is-ok { background: var(--jade-700); color: var(--paper-0); }

@media (max-width: 620px) {
  .tour-card { grid-template-columns: auto 1fr; }
  .tour-card-side { grid-column: 1 / -1; justify-items: start; grid-auto-flow: column; gap: 10px; }
  .tour-match { grid-template-columns: 1fr auto 1fr; }
  .tour-match-enter { grid-column: 1 / -1; text-align: center; }
}

/* "Giải đấu của tôi" inside the profile panel. Deliberately quieter than
   the tournament page's own list — this is a summary someone scans on the
   way to the real screen, not the screen itself. */
.profile-tour-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.profile-tour-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px;
  border-radius: 10px;
  background: var(--row-bg);
}
.profile-tour-link { flex: 1; min-width: 0; display: grid; gap: 2px; text-decoration: none; color: inherit; }
.profile-tour-name { font-weight: 600; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-tour-meta { font-size: 0.76rem; color: var(--text-muted); }
.profile-tour-score {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 0.82rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.profile-tour-item:hover { background: var(--hover-strong); }

/* The tournament ribbon above the board. One line, quiet enough to sit
   over a game in progress without competing with the clock. */
.tour-banner {
  margin: 0 0 8px;
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--gold-50);
  border: 1px solid var(--gold-100);
  color: var(--gold-700);
  font-size: 0.8rem;
  font-weight: 700;
  text-align: center;
  letter-spacing: 0.01em;
}

.tour-round-extend {
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--text-muted);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.72rem;
  font-weight: 700;
  cursor: pointer;
}
.tour-round-extend:hover { border-color: var(--accent); color: var(--accent); }

/* The range under a typed field. Quiet — it answers "what may I type here?"
   without competing with the label above it. */
.tour-field-hint {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0.01em;
}

/* ═══════════════════════════════════════════════════════════════════
   LIGHT MODE — "nổi bật" pass (owner feedback 2026-09-13: light is
   pleasant but doesn't pop). Everything here is scoped to
   [data-theme="light"] and only turns existing elements up: the red
   word in the hero becomes a sealed brushstroke, each game card gets
   its own colour identity on hover, the CTA gains depth. Dark mode
   never sees any of it.
   ═══════════════════════════════════════════════════════════════════ */


/* The kicker's flanking rules live on .hero-eyebrow itself now (both
   themes); only their colour is per-theme, via --eyebrow-rule. */

/* Each card claims its game's colour: a soft wash of the hue behind the
   icon, and a hover that glows in the card's own colour instead of a
   generic grey lift. */
[data-theme="light"] #card-chess   { --card-glow: rgba(125, 155, 98, 0.30); }
[data-theme="light"] #card-xiangqi { --card-glow: rgba(166, 51, 43, 0.26); }
[data-theme="light"] #card-gomoku  { --card-glow: rgba(30, 95, 191, 0.24); }
[data-theme="light"] .game-card::after {
  content: '';
  position: absolute;
  top: -30%;
  left: -12%;
  width: 55%;
  height: 130%;
  background: radial-gradient(circle, var(--card-glow), transparent 68%);
  opacity: 0.35;
  pointer-events: none;
  transition: opacity 0.3s var(--ease-out);
}
[data-theme="light"] .game-card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-lg), 0 12px 42px var(--card-glow);
}
[data-theme="light"] .game-card:hover::after { opacity: 0.7; }

/* The CTA gains lacquer depth: a top highlight + a deeper red base so
   the pill reads pressed-from-wax instead of flat fill. */
[data-theme="light"] .btn-primary {
  background: linear-gradient(180deg, var(--red-600), var(--red-700));
  box-shadow: var(--shadow-red), inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
[data-theme="light"] .btn-primary:hover {
  background: linear-gradient(180deg, var(--red-600) 30%, var(--red-800));
  transform: translateY(-2px);
  box-shadow: 0 16px 38px rgba(166, 51, 43, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* Warmer room: the two corner washes breathe a little harder and a
   faint third seal-red wash anchors the lower right. */
[data-theme="light"] body {
  background:
    radial-gradient(circle at 78% 12%, rgba(201, 150, 62, 0.22), transparent 480px),
    radial-gradient(circle at 8% 88%, rgba(166, 51, 43, 0.09), transparent 540px),
    radial-gradient(circle at 92% 96%, rgba(166, 51, 43, 0.06), transparent 420px),
    linear-gradient(135deg, var(--body-base) 0%, var(--bg) 100%);
}

/* ═══════════════════════════════════════════════════════════════════
   RESPONSIVE OVERHAUL (owner 2026-09-13: "làm lại responsive cho mọi
   màn" + phone screenshot). Appended last so it wins the cascade over
   the per-component breakpoints above. Two structural fixes:

   1) Game cards: below the two-column grid the card kept its desktop
      shape — icon pinned far left, title/button centered in the wide
      leftover — which read as off-balance (the screenshot). Tablets
      left-align the text next to the icon; phones stack the whole card
      vertically, centered, like an app tile.

   2) Leaderboard: the swipe-right table hid Elo/Win Rate behind a
      scroll nobody discovered. Phones now show the five columns that
      matter (rank · player · tier · Elo · win rate) with no horizontal
      scroll; gender + join date are desktop-only garnish.
   ═══════════════════════════════════════════════════════════════════ */

/* Card sizing across widths is now a single number per breakpoint —
   --icon-size on .game-card — because every glyph is derived from it.

   Shape, though, has to flip once: three cards side by side leave each one
   only ~290px of usable width, and icon + name + CTA on one line needs
   ~340px, so the name broke into two lines ("Cờ / vua"). Above 560px the
   name and the CTA stack in the column right of the icon — left-aligned,
   not floated in the middle like the old grid. Below 560px each card owns
   the full width, so the single-row tile fits and keeps all three cards
   plus the leaderboard above the fold. */
@media (min-width: 901px) {
  .game-card {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    align-items: center;
    gap: 8px 18px;
  }
  .game-icon { grid-row: 1 / span 2; }
  .game-card h2 { align-self: end; }
  /* A third of the page is ~300px: the pair stacks and fills the column
     right of the icon, so neither button can run past the card's edge. */
  .game-card-actions { justify-self: stretch; align-self: start; }
}
@media (max-width: 900px) {
  .game-card { --icon-size: 56px; }
  .game-card-actions .btn-primary,
  .game-card-actions .btn-secondary { min-height: 44px; }
}
@media (max-width: 560px) {
  /* Four things share one row on a phone: icon · name · CTA · clock. Trim
     the two that can afford it so the name always fits on one line. */
  .game-card { --icon-size: 48px; gap: 10px; padding: 12px 14px; }
  .game-card .btn-primary,
  .game-card .btn-secondary { padding: 9px 14px; }
  /* Phone: icon · name on the first row, the pair side by side under them
     across the whole card — two equal 44px targets with room for the
     longest label, instead of squeezing three things into one row. */
  .game-card { flex-wrap: wrap; }
  .game-card-actions {
    flex: 1 0 100%;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
  }
}

/* Leaderboard on phones: five columns, zero horizontal scroll — and, this
   time, nothing cut off either. `overflow-x: hidden` alone did not fit the
   table, it only hid the overflow: .hlb-th-player carries min-width 240px
   in the base rules, so at 390px the Elo column was sliced in half at the
   right edge (owner screenshot 2026-09-14). table-layout: fixed plus
   percentage widths make the table resolve against the viewport instead,
   and the player name ellipsises into whatever is left. */
@media (max-width: 720px) {
  .home-lb-table-wrap { overflow-x: hidden; }
  .home-lb-table { table-layout: fixed; width: 100%; min-width: 0; }
  .home-lb-table thead th, .home-lb-table tbody td { padding: 9px 5px; }
  /* Two-word headers ("WIN RATE") wrap instead of forcing a wider column. */
  .home-lb-table thead th { white-space: normal; line-height: 1.25; font-size: 0.64rem; }
  .hlb-th-gender, .hlb-gender-cell,
  .hlb-th-joined, .hlb-joined { display: none; }
  .hlb-th-rank, .hlb-rank { width: 40px; }
  .hlb-th-player { min-width: 0; width: auto; }
  .hlb-player-cell { gap: 8px; overflow: hidden; }
  .hlb-name, .hlb-bio { max-width: 100%; }
  .hlb-th-tier, .hlb-tier-cell { width: 14%; }
  .hlb-tier-text { font-size: 0.72rem; }
  .hlb-th-metric, .hlb-metric-cell { width: 14%; font-size: 0.86rem; }
  .hlb-th-winrate, .hlb-winrate-cell { width: 16%; }
  .hlb-winrate { padding: 2px 6px; font-size: 0.72rem; }
}
@media (max-width: 420px) {
  .hlb-name { font-size: 0.8rem; }
  .hlb-bio { display: none; }
  .hlb-avatar { width: 26px; height: 26px; }
}

/* Sort/game pills: two segmented controls side by side overflow a
   phone — let them wrap into two calm rows and scroll never. */
@media (max-width: 720px) {
  .home-lb-filters { justify-content: center; }
  .home-lb-segmented { flex-wrap: wrap; justify-content: center; }
}

/* The profile modal's stat strip: five 1fr tiles squeeze unreadably on
   narrow phones — reflow into a 3-up grid (3 + 2 rows). */
@media (max-width: 420px) {
  .fbp-stats-card { grid-template-columns: repeat(3, 1fr); row-gap: 10px; }
}

/* Replay page: header meta + result stack on phones instead of
   fighting for one row; the analysis summary hugs the viewport. */
@media (max-width: 600px) {
  .replay-header { flex-direction: column; gap: 6px; align-items: flex-start; }
  .replay-analysis-summary { max-width: 100%; font-size: 0.8rem; }
  .rp-an-acpl { margin-left: 0; flex-basis: 100%; }
}

/* ══════════════════════════════════════════════════════════════════════
   WAITING-SCREEN CLOCK SETUP (owner 2026-09-14)

   The room opens in one click and the link is shareable immediately; this
   card sits under the waiting box for the player who wants a faster or
   slower game. It is a second card rather than more rows inside the first
   one so the invite link — the thing a waiting player is actually there to
   copy — keeps the top of the screen to itself.
   ══════════════════════════════════════════════════════════════════════ */
/* The setup lives INSIDE the waiting box: one card, divided by a hairline.
   It was briefly a second card stacked below and read as two unrelated
   panels (owner 2026-09-14: "mắc gì tách ra 2 tab riêng") — waiting for an
   opponent and picking the clock are the same moment. */
.waiting-setup {
  margin: 18px calc(var(--box-pad) * -1) 0;
  padding: 16px var(--box-pad) 0;
  border-top: 1px solid var(--border);
  text-align: left;
}
.waiting-setup[hidden] { display: none; }
.ws-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  color: var(--accent);
}
.ws-head h3 {
  flex: 1 1 auto;
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}
/* "Đã lưu" — the only feedback the card gives, since every change applies
   the moment it is pressed. Fades itself out from the script. */
.ws-saved {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ok);
  opacity: 1;
  transition: opacity 0.4s ease;
}
.ws-saved[hidden] { display: none; }
.ws-saved.is-fading { opacity: 0; }

.ws-field + .ws-field { margin-top: 12px; }
.ws-label {
  display: block;
  margin-bottom: 7px;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ws-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 7px;
}
.ws-option {
  padding: 9px 4px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: transform 0.14s var(--ease-out), border-color 0.16s ease,
              background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
}
.ws-option:hover:not(.active) {
  border-color: var(--border-strong);
  transform: translateY(-1px);
}
/* The chosen one is filled, not outlined: at a glance across two rows the
   eye should land on two solid chips, not hunt for two tinted borders. */
.ws-option.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-red);
}
.ws-option:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-glow); }
.ws-option:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

.ws-hint {
  margin: 12px 0 0;
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--text-muted);
}
.ws-hint.is-locked { color: var(--danger); }

@media (max-width: 560px) {
  /* 40px of inset on each side of a 360px phone leaves the chips no room. */
  .waiting-box { --box-pad: 22px; }
  .waiting-setup { margin-top: 14px; padding-top: 14px; }
  .ws-options { gap: 6px; }
  .ws-option { padding: 8px 2px; font-size: 0.8rem; }
}

/* The chosen clock, shown on the waiting screen so the friend who opens the
   invite link knows what they are walking into before they sit down. */
.waiting-clock {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 auto 10px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
}
.waiting-clock.hidden { display: none; }

/* Quick-match search card (game.html #searchOverlay). Built on the waiting
   card above; only the pieces that card does not have live here. */
.search-clock { gap: 6px; }
.search-elapsed { font-variant-numeric: tabular-nums; color: var(--text); }
.search-status { min-height: 1.5em; }
.search-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 8px;
}
.search-actions .btn-primary,
.search-actions .btn-secondary {
  min-height: 44px;
  justify-content: center;
}
/* The search card focuses its one action on open; the ring is the theme's,
   the same accent glow the other cards use, not the browser's default blue. */
.search-actions .btn-primary:focus-visible,
.search-actions .btn-secondary:focus-visible,
.game-card-actions .btn-primary:focus-visible,
.game-card-actions .btn-secondary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-glow);
}


/* ══ Support / affiliate bar ═══════════════════════════════════════════════
   A strip between the header and the hero, plus the editor behind it.

   Built from the site's own tokens, so both themes and any future palette
   change carry over. Two states are worth naming because they are easy to
   get wrong:
     • a link saved but hidden (`.is-off`) is drawn as a dashed draft — only
       an editor is ever sent one, and "live" vs "saved but hidden" has to be
       readable at a glance;
     • a row with unsaved edits (`.is-dirty`) grows an accent spine, and its
       Save button is the only one that is ever enabled.
   The bar element ships `hidden` and stays that way until there is
   something to show, so nothing on the page moves for a visitor. */

/* Header and bar stick as one unit. The wrapper carries the stickiness so
   the bar's `top` is whatever the header happens to be tall — a hard coded
   offset would drift the moment the header changes height, which it does
   between the desktop and mobile layouts. The header's own `position:
   sticky` is switched off inside the wrapper: two nested sticky boxes with
   the same top is a layout nobody should have to reason about. */
.site-top {
  position: sticky;
  top: 0;
  z-index: 40;
}
.site-top .header { position: static; }

.aff-bar {
  background: linear-gradient(to bottom, var(--surface), var(--bg));
  border-bottom: 1px solid var(--border);
}

.aff-row {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 44px;
  /* Vertical only. The shorthand `padding: 6px 0` also zeroed the 20px
     horizontal padding this row inherits from .container, so the bar sat
     20px further out than the header above it. */
  padding-top: 6px;
  padding-bottom: 6px;
}

/* ── the label at the left end ───────────────────────────────────────── */
/* The left end has one job: make a visitor understand, without reading the
   pills, that this is a shopping strip and that buying through it supports
   the site. A single uppercase word ("SUPPORT") did not say that. */
.aff-brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  flex: 0 0 auto;
  padding-right: 14px;
  border-right: 1px solid var(--border);
}
/* A solid accent tag with a cart in it. An outlined bag on a pale
   background read as "some icon and two lines of small print" — this reads
   as a shop label from across the room, which is the whole job of the left
   end of the bar. */
.aff-brand-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--on-red, #fff);
  box-shadow: 0 2px 8px var(--accent-glow);
}

/* The pills scroll sideways rather than wrapping: the bar stays one line
   tall on a phone, or it pushes the hero off the first screen. The mask
   fades the cut edge instead of ending mid-pill. */
.aff-scroller {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 20px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 20px), transparent 100%);
}
.aff-scroller::-webkit-scrollbar { display: none; }

.aff-pills {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 12px 3px 2px;
  width: max-content;
}

/* ── one link ────────────────────────────────────────────────────────── */
.aff-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 auto;
  height: 30px;
  padding: 0 12px 0 5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-size: 13px;
  font-weight: 650;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease, color .16s ease;
}
.aff-pill:hover {
  transform: translateY(-1px);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: 0 6px 16px var(--accent-glow);
}
.aff-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.aff-pill-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  background: #fff;
  border: 1px solid var(--border);
  overflow: hidden;
  font-size: 12px;
  line-height: 1;
}
/* A white tile behind the mark: shop logos are drawn for white backgrounds,
   and on the dark theme an orange Shopee bag on navy looks like a mistake. */
[data-theme="dark"] .aff-pill-icon { background: #fff; }

.aff-logo {
  display: block;
  width: 18px;
  height: 18px;
  object-fit: contain;
}
/* The emoji behind the logo. It is hidden by the sibling rule below while an
   <img> is present, and becomes the only child — so it appears — once the
   error handler in affiliate-bar.js removes a logo that would not load.
   It used to be pushed behind with z-index: -1, which put it behind the
   tile's own white background: a failed logo left an empty white square. */
.aff-logo-text {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 12px;
  line-height: 1;
}
.aff-logo + .aff-logo-text { display: none; }

/* The out-arrow is a hover affordance, not decoration: it says "this leaves
   the site", which for a paid link is the honest thing to show. */
.aff-pill-out {
  opacity: 0;
  transform: translate(-3px, 2px);
  transition: opacity .16s ease, transform .16s ease;
}
.aff-pill:hover .aff-pill-out { opacity: .85; transform: translate(0, 0); }

/* Saved but hidden — only an editor is ever sent one of these. */
.aff-pill.is-off {
  border-style: dashed;
  opacity: .55;
  box-shadow: none;
}
.aff-pill.is-preview { cursor: default; }

/* ── right end: disclosure + pencil ──────────────────────────────────── */
.aff-manage {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text-muted);
  cursor: pointer;
  transition: color .16s ease, border-color .16s ease, transform .16s ease;
}
.aff-manage:hover { color: var(--accent); border-color: var(--accent); transform: rotate(-8deg); }

.aff-add-first {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 14px;
  border-radius: 999px;
  border: 1px dashed var(--accent);
  background: transparent;
  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.aff-add-first:hover { background: var(--accent-glow); }

/* ══ editor dialog ═════════════════════════════════════════════════════ */
body.aff-lock { overflow: hidden; }

.aff-overlay {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: stretch;
  justify-content: flex-end;
  background: rgba(12, 8, 4, 0.45);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  /* The backdrop never scrolls: scrolling the page behind the panel is how
     its own header ends up above the top of the window. Everything that
     scrolls does so inside .aff-scroll. */
  overflow: hidden;
  animation: aff-fade .18s ease;
}
@keyframes aff-fade { from { opacity: 0; } to { opacity: 1; } }

/* A drawer down the right edge rather than a box in the middle: the page
   stays visible beside it, and its full height removes the two problems a
   centred modal had — the void under five short rows, and the resize every
   time a row was expanded. */
.aff-dialog {
  display: flex;
  flex-direction: column;
  width: min(460px, 100%);
  height: 100%;
  background: var(--card);
  border-left: 1px solid var(--border);
  border-radius: 20px 0 0 20px;
  box-shadow: -18px 0 50px rgba(0, 0, 0, 0.22);
  overflow: hidden;
  animation: aff-slide .26s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes aff-slide {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

/* Closing. `forwards` is safe here because the element is removed the moment
   the animation ends (or the timeout in affiliate-bar.js fires). */
.aff-overlay.is-closing { animation: aff-fade-out .22s ease forwards; }
.aff-overlay.is-closing .aff-dialog {
  animation: aff-slide-out .22s cubic-bezier(0.4, 0, 1, 1) forwards;
}
@keyframes aff-fade-out { to { opacity: 0; } }
@keyframes aff-slide-out { to { transform: translateX(100%); } }

.aff-dialog-head, .aff-preview-wrap, .aff-toast { flex: 0 0 auto; }

/* The one scrolling box in the dialog: the rows and the add form together,
   so "add" is always one scroll away from whatever you were looking at.
   overscroll-behavior stops a flick at the end of the list from scrolling
   the page underneath. */
.aff-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
@keyframes aff-pop {
  from { transform: translateY(14px) scale(0.975); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}

.aff-dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 20px 14px;
  border-bottom: 1px solid var(--border);
}
.aff-dialog-head h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 18px;
  /* 1.2 left the ink 21px tall inside a 22px line box, so the descender of
     the "g" sat on the very edge and read as clipped. */
  line-height: 1.45;
  color: var(--text);
}
.aff-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--hover);
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 700;
}
.aff-head-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Adding a link starts here rather than from a form parked under the list:
   at 520px the pinned form and the scrolling list were both trying to own
   the bottom of the dialog, and drew on top of each other. */
.aff-head-add {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 13px 0 10px;
  border-radius: 9px;
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--on-red, #fff);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.aff-head-add:hover { background: var(--accent-hover); }

/* ── add-link popup ──────────────────────────────────────────────────── */
.aff-add-overlay {
  position: fixed;
  inset: 0;
  z-index: 1300;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background: rgba(12, 8, 4, 0.5);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: aff-fade .16s ease;
}
.aff-add-overlay[hidden] { display: none; }
.aff-add-overlay.is-closing { animation: aff-fade-out .18s ease forwards; }
.aff-add-overlay.is-closing .aff-add-card {
  animation: aff-pop-out .18s cubic-bezier(0.4, 0, 1, 1) forwards;
}
@keyframes aff-pop-out {
  to { transform: translateY(8px) scale(0.97); opacity: 0; }
}

.aff-add-card {
  width: min(430px, 100%);
  max-height: 100%;
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: var(--shadow-lg);
  animation: aff-pop .2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.aff-add-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 16px 10px;
}
.aff-add-head h3 { margin: 0; font-size: 15px; color: var(--text); }
.aff-add-body { display: grid; gap: 12px; padding: 0 16px 4px; }
.aff-add-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 16px 16px;
}

.aff-close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.aff-close:hover { color: var(--text); background: var(--hover); }

/* ── live preview of the real bar ────────────────────────────────────── */
.aff-preview-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  background: linear-gradient(to bottom, var(--surface), var(--bg));
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}
.aff-preview-wrap::-webkit-scrollbar { display: none; }
.aff-preview-wrap {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
}
.aff-preview {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 30px;
}
.aff-preview-brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  background: var(--accent);
  color: var(--on-red, #fff);
  flex: 0 0 auto;
}
.aff-preview-empty { font-size: 12.5px; color: var(--text-muted); font-style: italic; }

.aff-dialog-body {
  padding: 16px 20px 4px;
}

/* ── one row ─────────────────────────────────────────────────────────── */
.aff-card {
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  margin-bottom: 8px;
  overflow: hidden;
  transition: border-color .18s ease, box-shadow .18s ease;
}
.aff-card:hover { border-color: var(--border-strong); }
.aff-card.is-dirty {
  border-left-color: var(--accent);
  box-shadow: 0 4px 18px var(--accent-glow);
}

.aff-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 6px;
  background: var(--card);
}
.aff-card.is-open .aff-card-head { border-bottom: 1px solid var(--border); }

/* The whole left side of the header is the expand control: with five links
   the dialog is a list you scan, not five forms you scroll past. */
.aff-card-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 4px 6px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.aff-card-toggle:hover { background: var(--hover); }
.aff-chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform .18s ease; }
.aff-card.is-open .aff-chevron { transform: rotate(180deg); }
.aff-card-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  background: #fff;
  border: 1px solid var(--border);
  overflow: hidden;
  font-size: 15px;
  line-height: 1;
  flex: 0 0 auto;
}
.aff-card-icon .aff-logo { width: 22px; height: 22px; }
.aff-card-icon .aff-logo-text { font-size: 13px; font-weight: 800; color: var(--text-muted); }
.aff-card-id { min-width: 0; flex: 1 1 auto; }
.aff-card-id strong {
  display: block;
  font-size: 13.5px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.aff-card-host {
  font-size: 11.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.aff-card-meta { display: flex; align-items: center; gap: 14px; flex: 0 0 auto; padding-right: 4px; }

.aff-stat {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* A real switch, not a checkbox with a word next to it: this is the control
   that decides whether a link is live on the home page. */
.aff-switch {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  user-select: none;
}
.aff-switch input { position: absolute; opacity: 0; pointer-events: none; }
.aff-switch-track {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background .18s ease;
  flex: 0 0 auto;
}
.aff-switch-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform .18s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.aff-switch input:checked + .aff-switch-track { background: var(--ok); }
.aff-switch input:checked + .aff-switch-track .aff-switch-knob { transform: translateX(14px); }
.aff-switch input:focus-visible + .aff-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }

.aff-card-body {
  display: none;
  grid-template-columns: 1fr 1fr;
  gap: 10px 12px;
  padding: 4px 12px 12px;
}

.aff-card.is-open .aff-card-body { display: grid; }
.aff-card-foot { display: none; }
.aff-card.is-open .aff-card-foot { display: flex; }

.aff-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.aff-field > label {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.aff-field input {
  width: 100%;
  padding: 8px 10px;
  border-radius: 9px;
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.aff-field input::placeholder { color: var(--text-muted); opacity: .65; }
.aff-field input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.aff-hint { margin: 0; font-size: 11px; color: var(--text-muted); }
.aff-field-error { margin: 0; font-size: 11.5px; color: var(--danger); min-height: 0; }
.aff-field.has-error input { border-color: var(--danger); }
.aff-field.has-error input:focus { box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18); }


.aff-card-foot {
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
  background: var(--card);
  flex-wrap: wrap;
}
.aff-order { display: inline-flex; gap: 4px; }
.aff-icon-btn {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
}
.aff-icon-btn:hover:not(:disabled) { color: var(--text); background: var(--hover); }
.aff-icon-btn:disabled { opacity: .35; cursor: default; }

.aff-dirty {
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.aff-card-actions { display: inline-flex; gap: 8px; margin-left: auto; }
.aff-confirm {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  font-size: 12.5px;
  color: var(--text);
}
/* A display rule beats the `hidden` attribute, so the delete prompt and the
   normal actions were on screen at the same time. Both of these are toggled
   by `hidden` in JS, so both have to opt back out of their own display. */
.aff-card-actions[hidden],
.aff-confirm[hidden] { display: none; }

/* ── buttons ─────────────────────────────────────────────────────────── */
.aff-btn {
  padding: 8px 14px;
  border-radius: 9px;
  border: 1px solid transparent;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease;
}
.aff-btn-primary { background: var(--accent); color: var(--on-red, #fff); }
.aff-btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
.aff-btn-primary:disabled { opacity: .4; cursor: default; }
.aff-btn-ghost { background: transparent; border-color: var(--border-strong); color: var(--text); }
.aff-btn-ghost:hover { background: var(--hover); }
.aff-btn-danger { color: var(--danger); border-color: transparent; }
.aff-btn-danger:hover { background: rgba(239, 68, 68, 0.1); }
.aff-btn-danger-solid { background: var(--danger); color: #fff; }

/* ── add form ────────────────────────────────────────────────────────── */


/* ── empty state + toast ─────────────────────────────────────────────── */
.aff-empty-state { text-align: center; padding: 26px 16px 22px; }
.aff-empty-icon { font-size: 26px; }
.aff-empty-title { margin: 8px 0 4px; font-size: 15px; font-weight: 700; color: var(--text); }
.aff-empty-sub { margin: 0; font-size: 13px; color: var(--text-muted); line-height: 1.6; }

.aff-toast {
  margin: 0;
  padding: 11px 20px;
  font-size: 13px;
  font-weight: 600;
  border-top: 1px solid var(--border);
}
.aff-toast.is-ok { color: var(--ok); background: rgba(53, 112, 93, 0.08); }
.aff-toast.is-error { color: var(--danger); background: rgba(239, 68, 68, 0.08); }
/* Saved, but the list on screen could not be refreshed: a heads-up, not a failure. */
.aff-toast.is-warn { color: var(--gold-700); background: var(--gold-50); }

@media (max-width: 680px) {
  .aff-brand { padding-right: 10px; }
  .aff-card-body { grid-template-columns: 1fr; }
  .aff-overlay { padding: 2vh 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .aff-overlay, .aff-dialog, .aff-add-card { animation: none; }
  .aff-pill, .aff-manage, .aff-switch-knob { transition: none; }
  .aff-pill:hover, .aff-manage:hover { transform: none; }
}

/* ══ Donate ════════════════════════════════════════════════════════════════
   A header button that opens a modal: QR, account number, a copy button —
   and, for an editor, the same modal turns into the place those are set up.
   Same tokens as everything else, so both themes follow. */

.dn-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 13px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.dn-trigger:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); }
.dn-trigger svg { color: var(--accent); }

body.dn-lock { overflow: hidden; }

.dn-overlay {
  position: fixed;
  inset: 0;
  z-index: 1250;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background: rgba(12, 8, 4, 0.5);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  overflow: hidden;
  animation: dn-fade .18s ease;
}
@keyframes dn-fade { from { opacity: 0; } to { opacity: 1; } }

.dn-modal {
  display: flex;
  flex-direction: column;
  width: min(480px, 100%);
  max-height: min(660px, 100%);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: dn-pop .22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes dn-pop {
  from { transform: translateY(12px) scale(0.97); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}
/* Closing: the element is removed as soon as this ends (or the timeout in
   donate.js fires), so `forwards` is safe here. */
.dn-overlay.is-closing { animation: dn-fade-out .2s ease forwards; }
.dn-overlay.is-closing .dn-modal { animation: dn-pop-out .2s cubic-bezier(0.4, 0, 1, 1) forwards; }
@keyframes dn-fade-out { to { opacity: 0; } }
@keyframes dn-pop-out { to { transform: translateY(8px) scale(0.97); opacity: 0; } }

.dn-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 18px 12px;
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
.dn-head h2 { margin: 0; font-size: 17px; line-height: 1.45; color: var(--text); }
.dn-head-actions { display: flex; align-items: center; gap: 8px; }

.dn-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.dn-icon-btn:hover:not(:disabled) { color: var(--text); background: var(--hover); }
.dn-icon-btn:disabled { opacity: .35; cursor: default; }

.dn-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 14px 18px 16px; }

/* ── what a visitor sees ─────────────────────────────────────────────── */
.dn-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.dn-tab {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.dn-tab.is-active { background: var(--accent); border-color: var(--accent); color: var(--on-red, #fff); }

.dn-method-name { margin: 0 0 12px; font-size: 15px; color: var(--text); }
.dn-method-body { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* As big as the modal allows: people scan this from across a desk, and a
   200px square with 8px of padding inside a 480px modal was mostly padding. */
.dn-qr {
  width: 100%;
  max-width: 300px;
  height: auto;
  aspect-ratio: 1 / 1.18;
  object-fit: contain;
  border-radius: 14px;
  background: #fff;
  border: 1px solid var(--border);
  padding: 6px;
}

.dn-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
}
.dn-account-label {
  display: block;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.dn-account-number {
  display: block;
  font-size: 17px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--text);
}
.dn-holder { display: block; font-size: 12px; color: var(--text-muted); }
.dn-copy {
  flex: 0 0 auto;
  padding: 8px 12px;
  border-radius: 9px;
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
}
.dn-copy:hover { background: var(--hover); }

.dn-note { margin: 0; font-size: 13px; line-height: 1.6; color: var(--text-muted); text-align: center; }
.dn-link-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* One footer, two jobs: the thank-you line for a visitor, the action bar for
   an editor. Either way it is pinned to the dialog — Save is not something
   you should have to scroll a QR out of the way to reach. */
.dn-foot {
  flex: 0 0 auto;
  border-top: 1px solid var(--border);
}
.dn-foot-thanks {
  padding: 12px 18px;
  font-size: 12.5px;
  color: var(--text-muted);
  text-align: center;
}
.dn-foot-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  background: var(--card);
}
.dn-foot-target {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dn-foot-buttons { display: flex; gap: 8px; flex: 0 0 auto; }
.dn-order { display: inline-flex; gap: 4px; flex: 0 0 auto; }

/* Which method the footer is about. Only drawn when there is more than one,
   so a single method never looks like it is in some special state. */
.dn-card.is-active .dn-card-head strong { color: var(--accent); }

.dn-empty { text-align: center; padding: 22px 8px; color: var(--text); }
.dn-empty p { margin: 0 0 6px; font-weight: 600; }
.dn-empty-sub { font-weight: 400 !important; font-size: 13px; color: var(--text-muted); }

/* ── what an editor sees ─────────────────────────────────────────────── */
/* The add form: one field per line, everything a method needs, including the
   QR. The first version crammed kind + label + account + button into one row,
   which fitted nothing and answered nothing. */
.dn-add {
  display: grid;
  gap: 10px;
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.dn-add-open { width: 100%; margin-bottom: 14px; }
.dn-add-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 2px; }

.dn-segs { display: flex; gap: 6px; flex-wrap: wrap; }
.dn-seg {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text-muted);
  font: inherit;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
}
.dn-seg.is-active { background: var(--accent); border-color: var(--accent); color: var(--on-red, #fff); }

/* ── bank picker ─────────────────────────────────────────────────────── */
.dn-combo { position: relative; }
.dn-combo-input { width: 100%; }
.dn-combo-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 5;
  margin: 4px 0 0;
  padding: 4px;
  list-style: none;
  max-height: 220px;
  overflow-y: auto;
  border-radius: 10px;
  border: 1px solid var(--border-strong);
  background: var(--card);
  box-shadow: var(--shadow-sm);
}
.dn-combo-list[hidden] { display: none; }
.dn-combo-list button {
  display: flex;
  flex-direction: column;
  gap: 1px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.dn-combo-list button:hover { background: var(--hover); }
.dn-combo-list strong { font-size: 13px; }
.dn-combo-list span { font-size: 11px; color: var(--text-muted); }
.dn-combo-none { padding: 8px 9px; font-size: 12.5px; color: var(--text-muted); }

.dn-qr-pick { display: flex; align-items: center; gap: 12px; }
.dn-hint { margin: 6px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--text-muted); }

.dn-add input, .dn-add select, .dn-field input, .dn-field select {
  padding: 8px 10px;
  border-radius: 9px;
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  min-width: 0;
}
.dn-field input:focus, .dn-field select:focus, .dn-add input:focus, .dn-add select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

/* The modal already draws a frame and already has padding. A bordered,
   padded card inside it was a second box around the same content — two
   borders and two paddings for one form. The methods are separated by a
   hairline instead, and the fields run the full width of the body. */
.dn-card {
  border-top: 1px solid var(--border);
  padding-top: 12px;
  margin-top: 12px;
}
.dn-card:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.dn-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 0 10px;
}
.dn-card-head strong { flex: 1 1 auto; font-size: 13.5px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dn-kind {
  flex: 0 0 auto;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 8px;
}
.dn-card-body { display: grid; gap: 10px; }
.dn-field { display: flex; flex-direction: column; gap: 5px; }
.dn-field > span {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.dn-qr-edit { display: flex; align-items: center; gap: 12px; }
.dn-qr-thumb {
  width: 64px; height: 64px;
  object-fit: contain;
  border-radius: 10px;
  background: #fff;
  border: 1px solid var(--border);
  padding: 4px;
}
.dn-qr-empty {
  display: grid;
  place-items: center;
  width: 64px; height: 64px;
  border-radius: 10px;
  border: 1px dashed var(--border-strong);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 800;
}
.dn-qr-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.dn-file { cursor: pointer; }



.dn-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 9px;
  border: 1px solid transparent;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.4;
  cursor: pointer;
  text-decoration: none;
}
.dn-btn-primary { background: var(--accent); color: var(--on-red, #fff); }
.dn-btn-primary:hover { background: var(--accent-hover); }
.dn-btn-ghost { background: transparent; border-color: var(--border-strong); color: var(--text); }
.dn-btn-ghost:hover { background: var(--hover); }
.dn-btn-danger { color: var(--danger); border-color: var(--border-strong); }

.dn-switch { display: inline-flex; align-items: center; cursor: pointer; flex: 0 0 auto; }
.dn-switch input { position: absolute; opacity: 0; pointer-events: none; }
.dn-switch-track {
  position: relative;
  width: 34px; height: 20px;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background .18s ease;
}
.dn-switch-knob {
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform .18s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.dn-switch input:checked + .dn-switch-track { background: var(--ok); }
.dn-switch input:checked + .dn-switch-track .dn-switch-knob { transform: translateX(14px); }
.dn-switch input:focus-visible + .dn-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }

.dn-toast {
  flex: 0 0 auto;
  margin: 0;
  padding: 10px 18px;
  font-size: 12.5px;
  font-weight: 600;
  border-top: 1px solid var(--border);
}
.dn-toast.is-ok { color: var(--ok); background: rgba(53, 112, 93, 0.08); }
.dn-toast.is-error { color: var(--danger); background: rgba(239, 68, 68, 0.08); }
/* Saved, but the list on screen could not be refreshed: a heads-up, not a failure. */
.dn-toast.is-warn { color: var(--gold-700); background: var(--gold-50); }

@media (max-width: 680px) {
  .dn-trigger span { display: none; }
  .dn-trigger { padding: 0 11px; }

  /* A sheet off the bottom edge rather than a box floating in the middle:
     more room for the QR, and the controls sit where a thumb already is. */
  .dn-overlay { align-items: flex-end; padding: 0; }
  .dn-modal {
    width: 100%;
    max-height: 92vh;
    border-radius: 18px 18px 0 0;
    border-bottom: 0;
    animation: dn-sheet .24s cubic-bezier(0.22, 0.61, 0.36, 1);
  }
  .dn-overlay.is-closing .dn-modal { animation: dn-sheet-out .2s cubic-bezier(0.4, 0, 1, 1) forwards; }
}
@keyframes dn-sheet { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes dn-sheet-out { to { transform: translateY(100%); } }

@media (prefers-reduced-motion: reduce) {
  .dn-overlay, .dn-modal { animation: none; }
  .dn-switch-knob, .dn-trigger { transition: none; }
}
