:root {
  /* Midnight Jade theme — app-wide reskin (issue #23). */
  --bg: #0d1822;
  --fg: #ecdfc6;
  --accent: #f3c552;
  --accent-gold: #eac558;
  --muted: #9fb6b0;
  --card-bg: #193040;
  --border: rgba(234, 197, 88, 0.4);
  --panel-2: #21404c;
  --success: #3fae6b;
  --on-accent: #16262f;
  --hair: rgba(180, 210, 200, 0.13);
  /* Semantic accents. --gold and --danger were referenced as
     var(--gold, #d9b04a) / var(--danger, #d66) at every callsite but never
     actually declared, so they silently fell through to the literal fallback;
     declaring them here makes the variable real with zero render change.
     --gold-rgb is the same gold (#d9b04a == rgb(217,176,74)) for rgba() glows. */
  --gold: #d9b04a;
  --gold-rgb: 217, 176, 74;
  --danger: #d66;
  --down: #c2685e;        /* negative score delta / furiten text */
  --pao: #e26a6a;         /* pao (liable-payer) marker */
  --pao-dot: #b03030;     /* pao indicator inset / dot */
  --scrim: rgba(20, 20, 20, 0.55);  /* translucent panel backdrop */
  --font-ui: 'Nunito', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-display: 'DM Serif Display', Georgia, 'Times New Roman', serif;
  /* Global multiplier for every in-game tile size (Iteration 12 item 1).
     One knob so the exact factor can be tuned live on -dev. */
  --tile-scale: 1.333;
  /* Independent knob for the perimeter + ceremony wall tile size, applied
     on top of --tile-scale (Iteration 12.3: wall tiles +1/3). */
  --wall-scale: 1.333;
  /* The maximized board square. Named so tile sizes can cap themselves to
     a fraction of the *actual* board (not just vmin) — that keeps the
     14-tile self hand inside the viewport at every window aspect ratio
     instead of spilling under the bottom edge (Iteration 12 resize fix).
     Reserves 96px for the fixed bottom action bar.

     A real mahjong table is SQUARE: you (bottom) + across (top) are
     horizontal rows; left/right players' hand/melds/discards run as
     vertical strips down their edge. --board is that square side and
     also drives the tile-size caps. (No bottom action bar any more, so
     only a small margin is reserved.) */
  --board: min(98vw, calc(98vh - 24px));
  /* Tile widths hoisted to named vars so the fixed seat-grid row
     reservations below can derive their heights from the exact same
     expression the tiles use. --ar is tile height/width (50/36). */
  --ar: 1.389;
  /* Cap progression: /26 (original 6-wide pond) → /20 (8-wide pond)
     → /18 (fixed-slot melds) → /16 (melds back to 1-column so the only
     remaining horizontal constraint is the 8-wide pond row). At /16,
     8 sm-tiles ≈ 0.5 board vs the self-zone pond half-column ~7*--lg-w
     +20 ≈ 0.467 board + 20 — tight (a handful of pixels of overflow on
     the rightmost discard at narrow viewports, harmless). Opponent
     pond/meld tiles use --top-w/--side-w via the
     .seat-zone--top/--side .lt-tile override (those caps don't change),
     so the bump only affects the self zone. */
  --sm-w: min(clamp(calc(15px * var(--tile-scale)),
    calc(2.4vmin * var(--tile-scale)), calc(38px * var(--tile-scale))),
    calc(var(--board) / 16));
  --md-w: min(clamp(calc(22px * var(--tile-scale)),
    calc(3.0vmin * var(--tile-scale)), calc(46px * var(--tile-scale))),
    calc(var(--board) / 20));
  --lg-w: min(clamp(calc(28px * var(--tile-scale)),
    calc(4.2vmin * var(--tile-scale)), calc(64px * var(--tile-scale))),
    calc(var(--board) / 15));
  /* --xl-w: prominent tiles for the claim popup so its live-3D tiles read as
     clearly as the (larger) board hand tiles. */
  --xl-w: min(clamp(calc(40px * var(--tile-scale)),
    calc(6.0vmin * var(--tile-scale)), calc(92px * var(--tile-scale))),
    calc(var(--board) / 10));
  --top-w: min(clamp(calc(12px * var(--tile-scale)),
    calc(2.0vmin * var(--tile-scale)), calc(30px * var(--tile-scale))),
    calc(var(--board) / 30));
  --side-w: min(clamp(calc(11px * var(--tile-scale)),
    calc(1.7vmin * var(--tile-scale)), calc(26px * var(--tile-scale))),
    calc(var(--board) / 44));
}

/* Per-ruleset concealed-hand width (in tiles), used to size the fixed
   .seat-zone box. 13-tile rulesets (HK/Riichi/MCR/American/Beijing) peak at
   14 (13 + just-drawn); Taiwanese is a 16-tile hand → 17 to win. Declared on
   <main> (the data-rule-code carrier) so it cascades down to every .seat-zone.
   The Taiwanese override also tightens the self-hand tile cap (/15 → /18) so a
   17-tile row fits --board at narrow viewports; opponent face-down hands use
   --top-w/--side-w and render concealedCount dynamically, so they're untouched. */
main.table-page { --max-hand-tiles: 14; }
main.table-page[data-rule-code="taiwanese"] {
  --max-hand-tiles: 17;
  --lg-w: min(clamp(calc(24px * var(--tile-scale)),
    calc(3.6vmin * var(--tile-scale)), calc(56px * var(--tile-scale))),
    calc(var(--board) / 18));
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--fg);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Midnight-Jade reskin: serif display face for headings (issue #23). */
h1, h2, h3 { font-family: var(--font-display); font-weight: 400; }

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  border-bottom: 1px solid var(--border);
}

header .brand {
  color: var(--accent);
  font-weight: 700;
  font-size: 1.7rem;
  text-decoration: none;
}

header nav {
  display: flex;
  align-items: center;
}

/* Mobile: the brand + account controls (sign-up / Jade / clubs / user menu) can't fit on one
   row on a phone, which pushed the user menu off-screen (it was unreachable). Let the header
   wrap so the nav drops to its own right-aligned row, keep the menu reachable, and hide the
   scrolling challenge ticker (awkward on a narrow header). Scoped to the shared lobby header —
   the /worlds globe uses a separate <header class="worlds-topbar"> with its own responsive rules. */
@media (max-width: 600px) {
  header:not(.worlds-topbar) { flex-wrap: wrap; padding: 0.6rem 1rem; row-gap: 0.45rem; }
  header:not(.worlds-topbar) .brand { font-size: 1.45rem; }
  header:not(.worlds-topbar) nav { margin-left: auto; flex-wrap: wrap; justify-content: flex-end; gap: 0.4rem; }
  header:not(.worlds-topbar) .signup-chip { max-width: 52vw; }
  header:not(.worlds-topbar) #challenge-ticker { display: none; }
}

header nav a {
  color: var(--muted);
  text-decoration: none;
  margin-left: 1rem;
}

/* Display-language selector — a dropdown menu item inside the user menu. Sits
 * as a full-width row between Appearance and Exit; no visible label. */
.header-menu-lang {
  display: block;
  width: 100%;
  margin: 0.15rem 0;
  padding: 0.4rem 0.9rem;
  background: var(--card-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 5px;
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
}
.header-menu-lang:hover { border-color: var(--accent); }

footer {
  /* One compact row on every page: "LotusTable lobby server" left, About / Cookie Policy right.
     Same padding as the immersive globe footer so both pages match in height. */
  padding: 0.55rem 1.5rem;
  border-top: 1px solid var(--border);
  color: var(--muted);
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 16px;
}

main {
  padding: 2rem;
  max-width: 1100px;
  margin: 0 auto;
  width: 100%;
}

main.login form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-width: 320px;
}

main.login input, main.login button {
  padding: 0.5rem 0.75rem;
  font-size: 1rem;
  background: var(--card-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 4px;
}

main.login button {
  background: var(--accent);
  color: var(--bg);
  cursor: pointer;
  font-weight: 600;
}

/* Themed page-hero banner (transparent emblem + CSS wordmark) atop the worlds
   landing, per-world lobby + sign-in pages. Centered column; transparent so it
   sits on the page background with no colored band. */
.page-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  margin: 0 auto 1.75rem;
}
.page-banner-emblem { display: block; height: 132px; width: auto; }
.page-banner-word { font-size: 2.6rem; line-height: 1; }
@media (max-width: 560px) {
  .page-banner-emblem { height: 100px; }
  .page-banner-word { font-size: 2rem; }
}

/* Exit menu item rendered as a logout <form><button> (logout must be POST):
   make the button look identical to the sibling <a role="menuitem"> links. */
.header-menu-logout { margin: 0; }
.header-menu-logout button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.9rem;
  margin: 0;
  background: none;
  border: 0;
  font: inherit;
  color: var(--fg);
  cursor: pointer;
}
.header-menu-logout button:hover { background: var(--bg); color: var(--accent); }

.world-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  margin-top: 1.5rem;
}

.worlds h1 { color: var(--accent); }

.world-card {
  display: block;
  padding: 1.5rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--fg);
  text-decoration: none;
}

.world-card:hover {
  border-color: var(--accent);
}

.world-card h2 {
  margin: 0 0 0.5rem 0;
  color: var(--accent);
}

.world-card .rule-code {
  display: inline-block;
  margin-top: 0.75rem;
  font-family: ui-monospace, monospace;
  font-size: 0.875rem;
  color: var(--muted);
}

/* ── World picker: map view + view toggle (issue #23) ───────────────────── */
.worlds-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.worlds-head h1 { color: var(--accent); }

/* Segmented Map | Cards toggle. */
.world-view-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  flex: none;
  margin-top: 0.35rem;
}
.world-view-toggle .vt-btn {
  padding: 0.25rem 0.7rem;
  font-size: 0.78rem;
  color: var(--fg);
  background: var(--card-bg);
  text-decoration: none;
  border: none;
  cursor: pointer;
}
.world-view-toggle .vt-btn + .vt-btn { border-left: 1px solid var(--border); }
.world-view-toggle .vt-btn:hover { color: var(--accent); }
.world-view-toggle .vt-btn.is-active {
  background: var(--accent);
  color: #20180c;
  font-weight: 600;
}

.world-view-toggle .vt-btn.is-active { color: var(--on-accent); }

/* ── Immersive worlds globe (issue #23, deep-space design) ────────────────
   The globe fills the whole page; only a transparent top bar (brand + account
   controls) and the footer overlay it. There is NO Globe|Cards toggle — the
   card grid is the no-WebGL fallback, revealed by world-picker-globe.js (which
   also drops the `worlds-immersive` body class, restoring normal flow). */

/* Card grid hidden while the globe is the active view. */
.worlds[data-view="globe"] .world-grid { display: none; }
.worlds[data-view="cards"] .world-globe { display: none; }

/* Full-bleed: strip the page's centered/padded main; the scene fills the
   viewport behind the fixed bar + footer, and page scroll is locked. */
body.worlds-immersive { overflow: hidden; }
body.worlds-immersive main.worlds { max-width: none; margin: 0; padding: 0; width: 100%; }

/* Visually-hidden (screen-reader only) — the localized page heading. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Overlay top bar (a <header>, so reset the base header chrome here). */
.worlds-topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1rem 2rem;
}
body.worlds-immersive .worlds-topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  padding: 14px 22px; pointer-events: none; border-bottom: none;
  background: linear-gradient(to bottom, rgba(2, 4, 10, 0.72), rgba(2, 4, 10, 0));
}
body.worlds-immersive .worlds-topbar > * { pointer-events: auto; }
/* Account controls (Jade chip + user menu) stay on ONE line in the overlay bar
   — the header nav flex styling only targets `header nav`, so restate it here. */
.worlds-topbar nav { display: inline-flex; align-items: center; gap: 0.9rem; flex-wrap: nowrap; white-space: nowrap; min-width: 0; }
/* Fallback (cards) — the bar reads as a normal header. */
body:not(.worlds-immersive) .worlds-topbar { border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; }

/* The brand lockup never shrinks (wordmark stays whole); when the row is cramped the
   long sign-up CTA absorbs the squeeze (see .signup-chip below) so the right-side
   controls (Jade chip + user menu) are never pushed off the right edge. */
.worlds-brand { display: inline-flex; align-items: center; gap: 0; text-decoration: none; flex: 0 0 auto; }
.worlds-logo3d { position: relative; width: 116px; height: 116px; flex: 0 0 auto; }
/* Pull the wordmark into the badge's transparent right padding so it reads as
   one lockup (the emblem art doesn't reach the 116px box edge) — but leave a
   small visible gap. */
.worlds-wordmark { font-family: var(--font-display); font-size: 2.1rem; line-height: 1; white-space: nowrap; margin-left: 0; }
.worlds-wordmark .bw-lotus { color: var(--fg); }
.worlds-wordmark .bw-table { color: var(--accent); }
/* Mid widths (tablet / small laptop): shrink the brand lockup so the right-side
   controls fit without the wordmark having to ellipsize. */
@media (max-width: 880px) {
  .worlds-logo3d { width: 96px; height: 96px; }
  .worlds-wordmark { font-size: 1.75rem; }
  body.worlds-immersive .worlds-topbar { padding: 12px 16px; }
}
@media (max-width: 640px) {
  .worlds-logo3d { width: 89px; height: 89px; }
  .worlds-wordmark { font-size: 1.55rem; }
  body.worlds-immersive .worlds-topbar { padding: 10px 14px; }
}
/* Phone: even shrunk, the brand + Leaderboard + sign-up + user menu can't share one line, so
   the user menu was clipped off the right edge (unreachable). Let the immersive topbar wrap so
   the account controls drop to their own right-aligned row (the nav's default nowrap is undone
   here); the long sign-up CTA still ellipsizes so the menu never falls off. */
@media (max-width: 600px) {
  body.worlds-immersive .worlds-topbar { flex-wrap: wrap; row-gap: 0.3rem; padding: 8px 12px; }
  .worlds-topbar nav { margin-left: auto; flex-wrap: wrap; justify-content: flex-end; gap: 0.45rem; white-space: normal; }
  .worlds-topbar .signup-chip { max-width: 44vw; }
}

/* Footer pinned to the bottom as an overlay in globe view — keep the separator
   line + the left-aligned "LotusTable lobby server" text (as on every page). */
body.worlds-immersive footer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  pointer-events: none;
  background: linear-gradient(to top, rgba(7, 13, 21, 0.9), rgba(7, 13, 21, 0.55));
}

/* ── The globe stage ─────────────────────────────────────────────────── */
.world-globe {
  position: relative; width: 100%; height: min(68vh, 600px);
  min-height: 360px; overflow: visible; touch-action: none; user-select: none;
}
/* Immersive: the stage IS the page. */
body.worlds-immersive .world-globe {
  position: fixed; inset: 0; width: 100vw; height: 100vh; min-height: 0;
  margin: 0; z-index: 0;
  background:
    radial-gradient(60% 60% at 50% 42%, rgba(22, 52, 74, 0.5) 0%, rgba(5, 8, 15, 0) 70%),
    #05080f;
}
.globe-canvas-host { position: absolute; inset: 0; overflow: hidden; cursor: grab; }
.globe-canvas-host:active { cursor: grabbing; }
.globe-pins { position: absolute; inset: 0; pointer-events: none; }
.globe-hint {
  position: absolute; left: 0; right: 0; bottom: 10px; margin: 0; text-align: center;
  font-size: 0.8rem; color: var(--muted); pointer-events: none;
}
/* Lift the hint above the overlaid footer bar in immersive mode. */
body.worlds-immersive .globe-hint { bottom: 64px; z-index: 15; }

/* Pin: a button anchored by its teardrop tip at the projected point. Name pill
   above the teardrop is always visible (design). */
/* The clickable box is EXACTLY the teardrop (40×50). The name pill is
   absolutely positioned ABOVE it and pointer-events:none, so it neither
   inflates the hit box nor steals clicks from a neighbouring pin in the
   dense East-Asia cluster. */
.globe-pin {
  position: absolute; left: 0; top: 0;
  width: 40px; height: 50px;
  transform: translate(-50%, -100%); transform-origin: bottom center;
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  pointer-events: auto; font-family: inherit; line-height: 0;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.28)); z-index: 12;
  transition: opacity 0.25s ease;
}
.globe-pin:hover, .globe-pin:focus-visible, .globe-pin.is-selected { z-index: 31; }
.globe-pin:focus-visible { outline: none; }
.globe-pin-mark { position: absolute; inset: 0; display: block;
  animation: mjbob 1.9s ease-in-out infinite; }
.globe-pin-mark svg { display: block; width: 100%; height: 100%;
  transform-origin: bottom center; transition: transform 0.18s ease; }
.globe-pin:hover .globe-pin-mark svg, .globe-pin:focus-visible .globe-pin-mark svg { transform: scale(1.14); }
.globe-pin-label {
  position: absolute; left: 50%; bottom: calc(100% + 5px); transform: translateX(-50%);
  background: var(--panel-2); color: var(--fg); border: 1.5px solid var(--border);
  padding: 3px 10px; border-radius: 999px; font-size: 13px; font-weight: 800;
  white-space: nowrap; line-height: 1.2; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.18);
  pointer-events: none; transition: transform 0.18s ease;
}
.globe-pin-drop { fill: var(--accent); stroke: var(--accent-gold); stroke-width: 1.6; }
.globe-pin-disc { fill: var(--on-accent); }
.globe-pin-glyph { fill: var(--accent); font: 800 13px "Noto Sans SC", "PingFang SC", sans-serif; }
.globe-pin.soon { filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.28)) grayscale(0.55); opacity: 0.78; }
.globe-pin.soon .globe-pin-drop { fill: #8a9a96; stroke: #5c6c68; }
.globe-pin.soon .globe-pin-glyph { fill: #2a3a36; }

/* Popover detail card. */
.globe-pop {
  position: absolute; width: 256px; z-index: 40;
  background: var(--card-bg); color: var(--fg);
  border: 1.5px solid var(--border); border-radius: 16px; padding: 18px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
  font-family: var(--font-ui, inherit);
}
.globe-pop-close {
  position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; border-radius: 12px;
  border: none; background: var(--border); color: var(--muted); cursor: pointer;
  font-size: 15px; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.globe-pop-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.globe-pop-glyph {
  width: 34px; height: 34px; border-radius: 10px; background: var(--accent); color: var(--on-accent);
  display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 800;
  font-family: "Noto Sans SC", sans-serif; flex: 0 0 auto;
}
.globe-pop-name { font-family: var(--font-display, inherit); font-size: 22px; line-height: 1; }
.globe-pop-region { font-size: 11.5px; color: var(--muted); margin-top: 3px; letter-spacing: 0.3px; text-transform: uppercase; font-weight: 700; }
.globe-pop-blurb { margin: 10px 0 12px; font-size: 13px; line-height: 1.5; color: var(--muted); }
.globe-pop-tags { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
.globe-pop-tags span { font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 6px; background: var(--hair, rgba(180,210,200,.13)); color: var(--muted); }
.globe-pop-meta { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.globe-pop-meta .globe-pop-k { font-size: 10.5px; font-weight: 700; color: var(--muted); letter-spacing: 0.4px; text-transform: uppercase; display: block; margin-bottom: 5px; }
.globe-pop-online { text-align: right; }
.globe-pop-online span:last-child { display: inline-flex; align-items: center; gap: 5px; font-size: 14px; font-weight: 800; }
.globe-pop-online .dot { width: 7px; height: 7px; border-radius: 7px; background: var(--success); }
.globe-pips { display: inline-flex; gap: 4px; }
.globe-pips .pip { width: 9px; height: 9px; border-radius: 9px; background: var(--border); }
.globe-pips .pip.on { background: var(--accent); }
.globe-pop-actions { display: flex; flex-direction: column; gap: 8px; }
.globe-pop-variants { display: flex; flex-direction: column; gap: 6px; }
/* Primary: open the lobby to play with people (join a table or start one). */
.globe-pop-play {
  display: block; width: 100%; text-align: center; padding: 11px 0; border-radius: 11px;
  background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 800;
  letter-spacing: 0.3px; text-decoration: none; box-shadow: 0 4px 0 var(--accent-gold);
}
.globe-pop-play:hover { filter: brightness(1.05); }
/* Secondary: a quiet "play now vs bots" link, with a nudge when few players are online. */
.globe-pop-bots { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: 2px; }
.globe-pop-bots-lead { font-size: 11.5px; color: var(--muted); }
.globe-pop-quickplay {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  background: none; border: 0; padding: 4px 6px; cursor: pointer; font-family: inherit;
  color: var(--accent); font-size: 13px; font-weight: 700;
}
.globe-pop-quickplay:hover { color: var(--accent-gold); text-decoration: underline; }
.globe-pop-quickplay:disabled, .globe-pop-quickplay.is-loading { opacity: 0.6; cursor: default; text-decoration: none; }
.globe-pop-soon {
  display: block; text-align: center; padding: 10px 0; border-radius: 11px;
  background: var(--border); color: var(--muted); font-weight: 800; font-size: 13px;
  text-transform: uppercase; letter-spacing: 0.5px;
}
.globe-pop-arrow {
  position: absolute; left: 50%; width: 18px; height: 18px; background: var(--card-bg);
  bottom: -9px; transform: translateX(-50%) rotate(45deg);
  border-right: 1.5px solid var(--border); border-bottom: 1.5px solid var(--border);
}
.globe-pop.below .globe-pop-arrow {
  bottom: auto; top: -9px;
  border: none; border-left: 1.5px solid var(--border); border-top: 1.5px solid var(--border);
}
/* #58 — beside placement (near-top pins): the card sits to the side of the pin, so the
   up/down caret doesn't apply; the highlighted pin conveys the association. */
.globe-pop.beside .globe-pop-arrow { display: none; }

/* Night-sky decor (stars + moon) is now rendered in-scene by globe3d.js
   (deep-space design), so the old CSS-overlay star/moon rules were removed. */
@keyframes mjbob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) {
  .globe-pin-mark { animation: none; }
}

/* Lobby page */
.lobby-header {
    margin-bottom: 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}
/* Title block (name + rule code) sits left; the room people-count sits to its RIGHT. */
.lobby-header .lobby-titles { display: flex; flex-direction: row; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
.lobby-header .lobby-title-block { display: flex; flex-direction: column; gap: 0.15rem; }
.lobby-header h1 { margin: 0; color: var(--accent); }
.lobby-header .rule-code { margin: 0; color: var(--muted); font-family: ui-monospace, monospace; }
/* Room people-count (issue #38): a compact playing / forming / roaming row beside the title.
   The left border + padding visually separate it from the title block. */
.room-occupancy {
    margin: 0;
    padding-left: 1.1rem;
    border-left: 1px solid var(--border, #2a3a33);
    display: flex;
    flex-wrap: wrap;
    gap: 0.85rem;
    font-size: 0.85rem;
    color: var(--muted);
}
.room-occupancy .ro-item {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-variant-numeric: tabular-nums;
}
.room-occupancy .ro-icon { opacity: 0.85; font-size: 0.95em; line-height: 1; }
.room-occupancy .ro-playing { color: var(--accent, #2e8b6f); }
.lobby-toolbar { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; }
/* Table filters (issue #39). A wrap-friendly row of small labelled controls. */
.lobby-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1rem;
    margin: -0.5rem 0 1.25rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--border, #2a3a33);
    border-radius: 10px;
    background: var(--card-bg, #16242b);
    font-size: 0.85rem;
}
.lobby-filters label { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); }
.lobby-filters label.filter-check { gap: 0.4rem; cursor: pointer; }
.lobby-filters select {
    background: var(--bg, #0d1822);
    color: var(--fg);
    border: 1px solid var(--border, #2a3a33);
    border-radius: 6px;
    padding: 0.2rem 0.4rem;
    font-size: 0.85rem;
}
.lobby-filters .filter-clear {
    margin-left: auto;
    background: none;
    border: 1px solid var(--border, #2a3a33);
    color: var(--muted);
    border-radius: 999px;
    padding: 0.2rem 0.7rem;
    cursor: pointer;
}
.lobby-filters .filter-clear:hover { color: var(--accent); border-color: var(--accent); }
.filter-shown { color: var(--muted); font-weight: 400; font-size: 0.9rem; }
/* A row hidden by the active filters (orthogonal to the Datastar $lobbyView container show/hide). */
.filtered-out { display: none !important; }

/* Mobile: on a phone the list-view table row is a fixed horizontal flex that squeezed the rule
   summary into a sliver (it broke mid-phrase — "1", "round", "min 1", "han" each on its own line)
   and crammed the Join/View/Close buttons. Stack the row (meta on top, actions below) so each has
   room; and lay the filter toolbar out as a tidy 2-column grid instead of a ragged wrap. */
@media (max-width: 600px) {
  .table-row { flex-direction: column; align-items: stretch; gap: 0.55rem; }
  .table-row .table-meta { flex-wrap: wrap; gap: 0.3rem 0.7rem; }
  .table-row .row-actions { justify-content: flex-end; }

  .lobby-filters label { flex: 1 1 42%; justify-content: space-between; }
  .lobby-filters label.filter-check { flex-basis: 100%; justify-content: flex-start; }
  .lobby-filters select { min-width: 0; }
  .lobby-filters .filter-clear { margin-left: auto; }
}
.lobby-toolbar button {
  padding: 0.5rem 1rem;
  background: var(--card-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
}
.lobby-toolbar button.primary { background: var(--accent); color: var(--bg); }
.lobby-toolbar .play-now-form { margin: 0; display: inline-flex; }
/* "Play now vs bots" CTA — ▶ glyph + label as one lockup. */
.play-now { display: inline-flex; align-items: center; gap: 0.4em; font-weight: 700; }
.tables-pane h2 { margin: 0 0 0.75rem 0; }
.tables-pane .empty { color: var(--muted); }
/* Empty-lobby onboarding: instead of a passive "no tables" line, offer a one-click game.
   #tables-empty is the SSE-owned lead ("No tables yet."); .empty-cta is the separate CTA. */
.tables-pane .empty#tables-empty { text-align: center; }
.empty-cta { display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
  text-align: center; padding: 0.25rem 1rem 1.5rem; }
.empty-cta .play-now-form { margin: 0; }
/* Primary: "Start a table for others to join" — encourage playing with people. */
.empty-cta .empty-create { padding: 0.7rem 1.4rem; font-size: 1.05rem; border-radius: 6px;
  background: var(--accent); color: var(--bg); border: 1px solid var(--accent); cursor: pointer;
  font-weight: 700; }
.empty-cta .empty-create:hover { filter: brightness(1.08); }
/* Secondary: the quiet "play vs bots" fallback, with a nudge shown when few players are around. */
.empty-bots { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
.empty-bots-lead { color: var(--muted); font-size: 0.85rem; }
/* A submit button that reads as a plain text link (the play-vs-bots form). */
.linklike { background: none; border: 0; padding: 0; margin: 0; cursor: pointer; font: inherit;
  color: var(--accent); font-size: 0.9rem; }
.linklike:hover { color: var(--accent-gold); text-decoration: underline; }
.tables-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.table-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.75rem 1rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 4px;
}
.table-row .table-meta { display: flex; gap: 1rem; align-items: baseline; }
.table-row .rule-summary,
.table-card .rule-summary,
.table-mini .rule-summary {
    font-size: 0.875rem;
    color: var(--fg);
    cursor: help;
    border-bottom: 1px dotted var(--muted);
}
.table-card .rule-summary { font-size: 0.9rem; font-weight: 500; }
.table-mini .rule-summary { font-size: 0.8125rem; color: var(--muted); }

/*
 * Rule-summary hover tooltip. Mirrors the table-page rules panel layout
 * (dl with dt/dd pairs) so users see the same rule view in both places.
 * Hidden by default; shown on chip hover OR keyboard focus (via tabindex
 * on .rule-summary). Positioned absolutely below the chip with a small
 * offset; min-width keeps the values legible at any chip size.
 */
.rule-summary-wrap { position: relative; display: inline-block; }
/* Rich rule popover: a titled header (rule set + scroll icon) over a two-column key→value grid,
   with a caret pointing at the chip — instead of a plain rectangle. Hidden until hover/focus. */
.rule-tooltip {
    position: absolute;
    top: calc(100% + 0.55rem);
    left: 0;
    z-index: 20;
    display: none;
    min-width: 236px;
    width: max-content;
    max-width: 340px;
    margin: 0;
    padding: 0;
    border-radius: 12px;
    background: linear-gradient(180deg, var(--panel-2, #21404c), var(--card-bg, #16242b));
    border: 1px solid var(--border);
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
.rule-summary-wrap:hover .rule-tooltip,
.rule-summary-wrap:focus-within .rule-tooltip { display: block; }
/* Caret pointing up at the chip (a rotated square with the two upper borders). */
.rule-tooltip::before {
    content: "";
    position: absolute;
    top: -6px;
    left: 1.1rem;
    width: 11px;
    height: 11px;
    background: var(--panel-2, #21404c);
    border-left: 1px solid var(--border);
    border-top: 1px solid var(--border);
    transform: rotate(45deg);
}
.rule-tooltip-head {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.55rem 0.9rem;
    border-radius: 11px 11px 0 0;
    border-bottom: 1px solid var(--border);
    background: rgba(255, 255, 255, 0.04);
    color: var(--accent, #2e8b6f);
    font-family: var(--font-display, "DM Serif Display", serif);
    font-size: 1rem;
    line-height: 1.1;
}
.rule-tooltip-icon { width: 15px; height: 15px; flex: none; opacity: 0.9; }
.rule-tooltip-body {
    margin: 0;
    padding: 0.6rem 0.9rem 0.7rem;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.4rem 1.25rem;
    align-items: baseline;
}
.rule-tooltip-body dt {
    margin: 0;
    color: var(--muted);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.rule-tooltip-body dd {
    margin: 0;
    color: var(--fg);
    font-size: 0.85rem;
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
/* Styled replacement for the browser's plain native title popups (tooltip.js). One floating
   element, positioned with a caret pointing at the hovered/focused control. */
.app-tooltip {
    position: absolute;
    z-index: 60;
    max-width: 260px;
    padding: 0.4rem 0.65rem;
    border-radius: 8px;
    background: linear-gradient(180deg, var(--panel-2, #21404c), var(--card-bg, #16242b));
    border: 1px solid var(--border);
    color: var(--fg);
    font-size: 0.8rem;
    line-height: 1.3;
    box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
    pointer-events: none;
}
.app-tooltip[hidden] { display: none; }
.app-tooltip::before {
    content: "";
    position: absolute;
    top: -5px;
    left: var(--caret-x, 50%);
    width: 9px;
    height: 9px;
    margin-left: -4.5px;
    background: var(--panel-2, #21404c);
    border-left: 1px solid var(--border);
    border-top: 1px solid var(--border);
    transform: rotate(45deg);
}
.app-tooltip.is-above::before { top: auto; bottom: -5px; transform: rotate(225deg); }
.table-row .seats { color: var(--accent); font-weight: 600; }
.table-row button { padding: 0.25rem 0.75rem; background: var(--accent); color: var(--bg); border: 0; border-radius: 3px; cursor: pointer; }
.row-actions, .card-actions, .mini-actions { display: flex; gap: 0.5rem; align-items: center; }
.row-actions form, .card-actions form, .mini-actions form { margin: 0; }
/* Base .action-btn style — bare .action-btn buttons (gift panel, pass) had none and rendered
 * as raw browser buttons. Lobby join/view buttons keep their look via the higher-specificity
 * context selectors (.table-row button etc.), which override this. */
.action-btn { padding: 0.4rem 0.9rem; background: var(--accent); color: var(--bg); border: 0; border-radius: 4px; cursor: pointer; font-weight: 600; }
.action-btn:hover { filter: brightness(1.05); }
/* Disabled join/leave buttons: greyed + un-clickable. Applies to lobby
 * join (full table) and the table-page header Leave during ready phase. */
.action-btn:disabled, .leave-table:disabled { opacity: 0.45; cursor: not-allowed; }
.lock-icon { font-size: 0.95rem; line-height: 1; margin-right: 0.25rem; }
.lock-icon-public { opacity: 0.45; }
.tables-pane .empty.hidden { display: none; }

/* View toggle: three icon buttons in the lobby header; aria-pressed drives
 * the active highlight. Icons are inline SVG with fill: currentColor so the
 * active flip reads as both a background swap and an icon-color swap. */
.view-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.view-toggle button {
  width: 2.25rem;
  height: 2.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--card-bg);
  color: var(--muted);
  border: 0;
  border-right: 1px solid var(--border);
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.view-toggle button:hover { color: var(--fg); }
.view-toggle button:last-child { border-right: 0; }
.view-toggle button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--bg);
  box-shadow: inset 0 0 0 2px var(--accent-gold);
}
.view-toggle .view-icon { width: 1.25rem; height: 1.25rem; display: block; }

/* Tiles view: cards laid out in a responsive grid. */
.tables-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75rem; }
.table-card {
  display: flex; flex-direction: column; gap: 0.5rem;
  padding: 1rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.table-card .card-header { display: flex; justify-content: space-between; align-items: baseline; }
.table-card .card-rule { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.table-card .card-seats { color: var(--accent); }
.table-card button { padding: 0.4rem 0.75rem; background: var(--accent); color: var(--bg); border: 0; border-radius: 3px; cursor: pointer; align-self: flex-start; }

/* Graphical view: SVG mini-tables in a responsive grid. */
.tables-graphical { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; }
.table-mini {
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  padding: 0.75rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.table-mini .mini-svg { display: block; }
.table-mini .mini-table { fill: var(--bg); stroke: var(--accent); stroke-width: 1.5; }
.table-mini .seat-occupied .seat-dot { fill: var(--accent); stroke: var(--accent); stroke-width: 1; }
.table-mini .seat-occupied .seat-initial { fill: var(--bg); font-size: 11px; font-weight: 700; font-family: ui-sans-serif, sans-serif; }
.table-mini .seat-empty .seat-dot { fill: transparent; stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 2; }
.table-mini .seat-empty .seat-initial { fill: transparent; }
.table-mini .mini-dealer-pip { fill: var(--accent); font-size: 9px; font-weight: 700; font-family: ui-sans-serif, sans-serif; }
.table-mini .mini-top {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.75rem;
    width: 100%;
    justify-content: center;
}
.table-mini .mini-bottom {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.75rem;
}
.table-mini .mini-seats { color: var(--accent); font-weight: 600; }
.table-mini button { padding: 0.3rem 0.75rem; background: var(--accent); color: var(--bg); border: 0; border-radius: 3px; cursor: pointer; }

/* Modal */
.modal { border: 1px solid var(--border); background: var(--card-bg); color: var(--fg); padding: 1.5rem; border-radius: 8px; }
.modal form { display: flex; flex-direction: column; gap: 0.75rem; min-width: 320px; }
.modal h3 { margin: 0 0 0.5rem 0; color: var(--accent); }
.modal label { display: flex; flex-direction: column; gap: 0.25rem; }
.modal label.checkbox { flex-direction: row; align-items: center; gap: 0.5rem; }
.modal input, .modal select { padding: 0.5rem; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px; }
.modal-countdown { margin: 0 0 0.75rem 0; display: flex; justify-content: center; }
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.5rem; }
.modal-actions button { padding: 0.5rem 1rem; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; }
.modal-actions button.primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }

/* Connection-status pill. Shown when an SSE channel (lobby or game) is
 * disconnected and trying to reconnect. Fixed top-right under the header so
 * it's visible without stealing focus; pulsing dot signals "live retry".
 * Hidden via the `hidden` attribute when all registered streams are
 * connected — set by connection-pill.js. */
.connection-pill {
  position: fixed;
  top: 0.5rem;
  right: 0.75rem;
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.75rem;
  background: var(--card-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.8125rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  animation: connection-pill-in 180ms ease-out;
}
.connection-pill[hidden] { display: none; }
.connection-pill .pill-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent-gold);
  animation: connection-pill-pulse 1.1s ease-in-out infinite;
}
.connection-pill.is-offline .pill-dot {
  background: #c44;
  animation: none;
}
@keyframes connection-pill-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}
@keyframes connection-pill-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Disconnect overlay — table page only. Owned by table-disconnect-overlay.js.
 * Appears after ~1.5s of `reconnecting` so brief blips (<1.5s) don't flash
 * a disruptive overlay. The card is fixed-positioned (not anchored to the
 * board) so it survives DOM re-renders, and pointer-events:all on the
 * overlay itself eats clicks before they reach the underlying tiles/buttons
 * (no need to mutate the game DOM during disconnect). The header's Leave
 * Table form sits outside this overlay and remains usable — that path goes
 * through the lobby server, not the gateway, so it works even when the
 * gateway is unreachable.
 *
 * Two states:
 *  - .is-reconnecting  → soft dim + "Reconnecting…" with pulsing dot
 *  - .is-offline       → harder dim + "Lost connection" + Reload button
 */
.disconnect-overlay {
  position: fixed;
  inset: 0;
  z-index: 980;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(8, 12, 16, 0);
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms ease, background-color 200ms ease;
}
.disconnect-overlay[hidden] { display: none; }
.disconnect-overlay.is-visible {
  opacity: 1;
  pointer-events: all;
  background: rgba(8, 12, 16, 0.45);
}
.disconnect-overlay.is-offline {
  background: rgba(8, 12, 16, 0.62);
}
.disconnect-overlay .disconnect-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  padding: 1.25rem 1.75rem;
  background: var(--card-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
  min-width: 240px;
  text-align: center;
}
.disconnect-overlay .disconnect-title {
  font-size: 1rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.disconnect-overlay .disconnect-spinner {
  width: 0.9rem;
  height: 0.9rem;
  border-radius: 50%;
  border: 2px solid var(--accent-gold);
  border-top-color: transparent;
  animation: disconnect-spin 0.9s linear infinite;
}
.disconnect-overlay.is-offline .disconnect-spinner { display: none; }
.disconnect-overlay .disconnect-detail {
  font-size: 0.8125rem;
  color: var(--fg-muted, #b9b9b9);
  margin: 0;
}
.disconnect-overlay .disconnect-reload {
  padding: 0.45rem 1.1rem;
  background: var(--accent);
  color: var(--bg);
  border: 0;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  font: inherit;
}
.disconnect-overlay .disconnect-reload:hover { filter: brightness(1.1); }
.disconnect-overlay:not(.is-offline) .disconnect-reload { display: none; }
@keyframes disconnect-spin {
  to { transform: rotate(360deg); }
}

/* Move-rejected toast — Iter 33.x. Banner inside #game-board, centered
 * near the top, slides down briefly when the engine refuses a player's
 * action (Tsumo without yaku, Riichi when locked, illegal kan, …). Sits
 * above the seats but below the connection pill / modals. Auto-dismissed
 * by the renderer after ~4s. */
.move-rejected-toast {
  position: absolute;
  top: 56px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 950;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  background: rgba(120, 28, 28, 0.96);
  color: #ffe7e0;
  border: 1px solid #b35a4a;
  border-radius: 6px;
  font-size: 0.9rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
  animation: move-rejected-in 200ms ease-out;
}
.move-rejected-toast .rejection-action { font-weight: 700; }
.move-rejected-toast .rejection-sep    { opacity: 0.6; }
.move-rejected-toast .rejection-reason { opacity: 0.95; }
@keyframes move-rejected-in {
  from { opacity: 0; transform: translate(-50%, -8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* In-game hint (#37, Jade sink). Toggle sits bottom-left of the board (clear of
   the bottom-right game-id badge + the centred action bar); the drawer opens
   above it. Chrome — shown in both 2D and 3D. */
.hint-toggle {
  position: absolute;
  bottom: 0.6rem;
  left: 0.75rem;
  z-index: 940;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.7rem;
  background: var(--card-bg, #15302a);
  color: var(--fg, #eaf5ef);
  border: 1px solid var(--accent, #2f6f5e);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
}
.hint-toggle:hover { border-color: var(--accent-gold, #d9b25a); }
.hint-toggle.is-open { border-color: var(--accent-gold, #d9b25a); color: var(--accent-gold, #d9b25a); }
.hint-toggle-icon { font-size: 1rem; line-height: 1; }
.hint-toggle-cost {
  margin-left: 0.15rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 700;
}
.hint-toggle-cost.is-free { background: rgba(47, 111, 94, 0.4); color: #cdeede; }
.hint-toggle-cost.is-paid { background: rgba(217, 178, 90, 0.22); color: var(--accent-gold, #d9b25a); }
.hint-toggle.is-pending { opacity: 0.7; cursor: progress; }
.hint-toggle.is-error { border-color: #b35a4a; }
.hint-toggle-error { margin-left: 0.3rem; font-size: 0.72rem; color: #ffb9ab; font-weight: 600; }

.hint-drawer {
  position: absolute;
  bottom: 3.2rem;
  left: 0.75rem;
  z-index: 945;
  width: min(440px, 92vw);
  max-height: min(60vh, 520px);
  display: flex;
  flex-direction: column;
  /* Half-transparent so the board shows through; blur keeps the text legible. */
  background: color-mix(in srgb, var(--card-bg, #12241f) 62%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: 1px solid var(--accent, #2f6f5e);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  /* NO entrance animation: render() is full-replace and re-creates this drawer on
     every notification, so an entrance keyframe would replay (flicker) each render —
     and move-rejected-in's translate(-50%) is wrong for a left-anchored panel. */
  overflow: hidden;            /* clip to the rounded box; body scrolls */
}
.hint-drawer-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.hint-drawer-title { font-weight: 700; color: var(--accent-gold, #d9b25a); margin-right: auto; }
.hint-drawer-cost {
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
}
.hint-drawer-cost.is-free { background: rgba(47, 111, 94, 0.4); color: #cdeede; }
.hint-drawer-cost.is-paid { background: rgba(217, 178, 90, 0.22); color: var(--accent-gold, #d9b25a); }
.hint-drawer-close {
  background: none;
  border: none;
  color: var(--fg, #eaf5ef);
  font-size: 1rem;
  cursor: pointer;
  opacity: 0.7;
  line-height: 1;
}
.hint-drawer-close:hover { opacity: 1; }
.hint-drawer-body {
  flex: 1 1 auto;
  min-height: 0;               /* let the flex child actually scroll */
  padding: 0.5rem 0.7rem;
  overflow-y: auto;
  overflow-x: hidden;
}
.hint-loading, .hint-empty { opacity: 0.75; font-size: 0.85rem; padding: 0.4rem 0; }
.hint-completion {
  padding: 0.5rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.hint-completion:last-child { border-bottom: none; }
.hint-completion.is-short { opacity: 0.72; }
/* The grouped winning hand can be wide (5 sets) — wrap to fit the drawer rather
   than overflow, and keep the tiles compact. */
.hint-completion .result-hand-grouped { flex-wrap: wrap; gap: 0.3rem 0.45rem; max-width: 100%; }
.hint-completion .result-hand-grouped .lt-tile,
.hint-completion .result-hand-grouped .tile3d-slot { max-width: 100%; }
.hint-completion-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.25rem; }
.hint-completion-faan { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; font-size: 0.8rem; margin-bottom: 0.3rem; }
.hint-faan { opacity: 0.85; }
.hint-faan.is-win { opacity: 1; font-weight: 700; }
.hint-faan--ron.is-win { color: var(--accent-gold, #d9b25a); }
.hint-faan--tsumo.is-win { color: #7fd6b0; }
.hint-faan.is-short { opacity: 0.6; }
.hint-selfdraw-badge {
  display: inline-block;
  margin-bottom: 0.3rem;
  padding: 0.05rem 0.4rem;
  border-radius: 4px;
  font-size: 0.7rem;
  background: rgba(127, 214, 176, 0.18);
  color: #7fd6b0;
}

/* 混兒 (hùnr) wildcard hint (Beijing). A persistent top-CENTRE pill naming
   this hand's wild rank. Rendered as a direct child of #game-board so it
   survives both board modes (board-3d.css hides only .board-grid, and
   #game-board sits above the 3D canvas) — z-index 950 keeps it above the
   canvas. Sits near the top edge so it clears the centred move-rejected toast
   (top:56px) below it. Gold accent matches the .wait winning-tile treatment. */
.beijing-wild-hint {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 950;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.55rem 0.35rem 0.6rem;
  background: rgba(26, 22, 15, 0.92);
  border: 1px solid var(--gold);
  border-radius: 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5),
              0 0 10px rgba(var(--gold-rgb), 0.35);
  pointer-events: none;
}
.beijing-wild-hint .wild-hint-label {
  color: var(--gold);
  font-weight: 700;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.beijing-wild-hint .lt-tile {
  filter: drop-shadow(0 0 4px rgba(var(--gold-rgb), 0.8));
}

/* Table page */
/* Header is a flex row: titles on the left (rule name + table id stacked),
 * action buttons (Ready + Leave) on the right. Shares max-width with the
 * compass grid below so the right edge of Leave lines up with the East seat. */
.table-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.table-header-titles { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.table-header h1 { margin: 0; color: var(--accent); }
.table-header .header-actions { margin: 0; display: flex; gap: 0.5rem; align-items: center; }
.table-header .header-actions button { padding: 0.5rem 1rem; background: var(--accent); color: var(--bg); border: 0; border-radius: 4px; cursor: pointer; font-weight: 600; }
.table-header .header-actions .leave-table { background: transparent; color: var(--accent); border: 1px solid var(--accent); }
/* "Play with bots" (#42): a gold primary action; bot-fill.js hides it once the table fills or
 * the game starts, and flips .is-error briefly on an out-of-Jade / sign-up nudge. */
.table-header .header-actions .fill-bots { background: var(--accent); color: var(--bg); }
.table-header .header-actions .fill-bots:hover { filter: brightness(1.08); }
.table-header .header-actions .fill-bots[hidden] { display: none; }
.table-header .header-actions .fill-bots:disabled { opacity: 0.6; cursor: progress; }
.table-header .header-actions .fill-bots.is-error { background: var(--danger); color: #fff; cursor: default; }
/* Ready button is gated on .ready-phase (toggled by JS from seat count +
 * game-started state). Hidden by default so the button only appears during
 * the ConfirmingReadiness window. */
.table-header #ready-button { display: none; }
.table-page.ready-phase .table-header #ready-button { display: inline-block; }
.table-page.game-started .table-header #ready-button { display: none; }
.game-started .table-header #ready-button { display: none; }

/* GitLab #8 — kill the 2D forming-page flash on a MID-GAME refresh. The
 * forming compass grid + rules live in .table-area; in-game, game-render.js
 * hides it (ensureMounted: area.hidden=true) only AFTER the SSE snapshot lands
 * — so a hard refresh into a live game paints the 2D forming page for the
 * round-trip. The server already emits body.game-started for an IN_GAME table,
 * so hide .table-area from byte 0. This does NOT interfere with the
 * forming→game seat-shuffle ceremony (which plays inside .table-area): that
 * runs while body.game-started is still ABSENT — the engine paces gameStarted
 * to land only after the shuffle, where the flip is synchronous
 * (table-stream.js gameStarted handler). Reconnect suppresses shuffle replay,
 * so there's nothing to keep visible on a refresh. */
body.game-started .table-area { display: none; }
/*
 * Compass seat layout: 3x3 grid with the table-center in the middle and the
 * four seats arranged North/East/South/West. DOM order is
 * North, West, center, East, South — screen readers walk the seats in a
 * predictable order while CSS grid coordinates drop each one into its
 * compass cell. The center cell renders an actual table shape (rounded rect
 * with the accent border) mirroring the lobby's Graphical mini view, not a
 * labelled box.
 */
.table-grid {
    list-style: none;
    padding: 0;
    /* Auto margins horizontally center the grid in main.table-page; the wider
     * main element still uses its own auto-centering up the tree. */
    margin: 0 auto 1.5rem auto;
    display: grid;
    grid-template-columns: 1fr 1.5fr 1fr;
    grid-template-rows: 1fr 1.5fr 1fr;
    gap: 0.75rem;
    max-width: 540px;
    aspect-ratio: 1;
}
/* Mahjong convention (viewed from above, counter-clockwise turn order
 * E → S → W → N): South top, East right, North bottom, West left. */
.table-grid .seat-south  { grid-column: 2; grid-row: 1; }
.table-grid .seat-east   { grid-column: 3; grid-row: 2; }
.table-grid .seat-north  { grid-column: 2; grid-row: 3; }
.table-grid .seat-west   { grid-column: 1; grid-row: 2; }
/* Player-cards lift above both the table-center cell (z-index: 1) and any
 * sibling seat backgrounds during the shuffle animation. We deliberately do
 * NOT put z-index on `.table-grid .seat`: that turns each seat into a
 * stacking context, trapping its child .player-card inside the seat's
 * layer. With all four seats tied at the same z-index, DOM order resolves
 * ties — so a card animating from a DOM-earlier origin (e.g. North) toward
 * a DOM-later destination (West/East/South) gets painted behind the
 * destination cell as it crosses. Lifting the card itself (a non-stacking-
 * context'd seat lets the card escape) puts it unambiguously above sibling
 * seat backgrounds regardless of DOM order. */
.table-grid .player-card { position: relative; z-index: 5; }
.table-grid .table-center {
    position: relative;
    z-index: 1;
    grid-column: 2;
    grid-row: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    border: 2px solid var(--accent);
    border-radius: 12px;
    box-shadow: inset 0 0 0 6px rgba(0, 0, 0, 0.25);
    color: var(--accent);
    text-transform: uppercase;
    font-size: 0.75rem;
    letter-spacing: 0.15em;
    font-weight: 600;
}
.table-grid .seat { position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 0.75rem 1rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; gap: 0.25rem; text-align: center; min-width: 0; transition: border-color 0.4s ease, box-shadow 0.4s ease; }
.table-grid .seat .position { color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; }
.table-grid .seat .player-card { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; font-family: ui-monospace, monospace; max-width: 100%; color: var(--accent); font-weight: 600; }
.table-grid .seat .player-card .player-name-text { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.table-grid .seat-empty .player-card { color: var(--muted); }
.table-grid .seat.seat-east  { writing-mode: horizontal-tb; }
.table-grid .seat.seat-west  { writing-mode: horizontal-tb; }
.seat { display: flex; flex-direction: column; justify-content: center; padding: 0.75rem 1rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 4px; gap: 0.25rem; }
.seat .position { color: var(--muted); font-size: 0.875rem; }

/* Shuffle animation —
 *
 * Choreography: when seatsShuffled arrives, JS animates each .player-card
 * through a three-beat sequence (gather → spin → split) by setting inline
 * `transition` + `transform` styles per beat. All four cards converge on the
 * table-center in a tight inner-compass fan, rotate 720° in place, then split
 * back out to their assigned compass cells. After the visual lands, JS
 * reparents each card into its destination cell atomically with a transform
 * clear, then runs the settle bounce + dealer reveal.
 *
 * The settle keyframe + the dealer-reveal styles below are the only shuffle-
 * related rules in CSS; everything else is driven by inline JS transitions.
 */

/* "✓ Ready" badge — appears on a player-card once their playerReady
 * notification lands (or immediately for the current player after they
 * confirm in the modal). Lives inside the card so it travels with the
 * card during the seat shuffle. Small spring-in on first appearance. */
.player-card .ready-badge {
    display: block;
    width: fit-content;
    margin: 0.55rem auto 0;
    padding: 0.2rem 0.6rem;
    border-radius: 999px;
    background: #1f5a3a;
    color: #b7f0c8;
    border: 1px solid #2d8b56;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    font-family: system-ui, sans-serif;
    animation: ready-badge-in 0.3s cubic-bezier(.18, .7, .3, 1.25);
}
@keyframes ready-badge-in {
    0%   { opacity: 0; transform: scale(0.4); }
    100% { opacity: 1; transform: scale(1); }
}
.player-card.shuffle-settle { animation: shuffle-settle 0.2s ease-out; }
@keyframes shuffle-settle {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.05); }
    100% { transform: scale(1); }
}

/* Center-cell "Shuffling…" overlay — fades in for the anticipation beat,
 * fades out during the slide. Sits on top of the table-center pip area. */
.shuffle-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--accent);
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    pointer-events: none;
    opacity: 1;
    transition: opacity 0.4s ease;
}
.shuffle-overlay.is-hidden { opacity: 0; }

/* Dealer marker — gold halo on the seat plus a 莊 corner pip.
 *
 * `.seat.is-dealer` toggles on the cell that holds the dealer after shuffle
 * (set by JS from seatsShuffled.dealerSeatIndex + positions mapping). The
 * border + shadow transitions follow the same 0.4s ease curve as the base
 * seat transition above. The pip animates in once with a spring scale and
 * stays visible.
 */
.table-grid .seat.is-dealer {
    border-color: var(--accent-gold);
    box-shadow: 0 0 0 1px var(--accent-gold), 0 0 22px rgba(212, 168, 71, 0.45);
}
.dealer-pip {
    position: absolute;
    top: -10px;
    right: -10px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: linear-gradient(135deg, #f0c969 0%, #b0832a 100%);
    border: 1.5px solid #f4d488;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #2a1d05;
    font-size: 12px;
    font-weight: 900;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
    transform-origin: center;
    transform: scale(1);
    opacity: 1;
}
.dealer-pip.is-hidden { display: none; }
.dealer-pip.is-arriving { animation: dealer-pip-in 0.4s cubic-bezier(.18, .7, .3, 1.25); }
@keyframes dealer-pip-in {
    0%   { opacity: 0; transform: scale(0.4) rotate(-180deg); }
    100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
/* The table page balances horizontally around the compass grid; the header,
 * viewer list, and event log all align under the grid's width. */
.table-page .table-header { max-width: 540px; margin: 0 auto 1rem auto; }
/*
 * Two-column grid wrapping the compass table:
 *   [ table-grid ] [ rules panel ]
 *
 * The viewer drawer floats over the page's left edge (position: fixed) so
 * its open/closed state doesn't shift the table or rules panel. Rules
 * stretches to the table-grid height via grid align-items: stretch.
 */
.table-area {
    display: grid;
    grid-template-columns: minmax(0, 540px) minmax(0, 220px);
    gap: 1.5rem;
    margin: 0 auto 1.5rem auto;
    align-items: stretch;
    justify-content: center;
    max-width: 800px;
}
.table-area > .seats { display: contents; }
.table-area .table-grid { grid-column: 1; margin: 0; }
.rules-panel { grid-column: 2; align-self: stretch; width: 100%; }

/*
 * Viewer drawer: floats fixed to the left edge of the viewport so toggling
 * open/closed doesn't push the table or rules panel. Top is aligned with
 * the top of the table-area (compass + rules) via the `--drawer-top` CSS
 * variable set from JS on load / resize; falls back to 11rem before JS
 * runs to keep the drawer near where it'll end up.
 *
 * Handle is a small fixed-size vertical tab pinned at the top-left of
 * the drawer's bounding box. The drawer's bounding box is taller than
 * the handle (to give the open body fixed height with scroll), so the
 * outer is `pointer-events: none` and only the handle + body intercept
 * clicks — the otherwise-invisible region below the closed handle stays
 * click-transparent.
 *
 * When open: body extends to the right of the handle with fixed height
 * (matches the table-grid / rules-panel max of 540px, capped at 75vh)
 * and overflow-y: auto so a long viewer list scrolls inside.
 *
 * Surface is transparent so the table-area shows through; only the
 * handle and the floating list items inside the body have backgrounds.
 */
.viewer-drawer {
    position: fixed;
    left: 0;
    top: var(--drawer-top, 11rem);
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    height: min(540px, 75vh);
    background: transparent;
    border: 0;
    pointer-events: none;
    transition: width 0.18s ease;
    width: 232px;
    z-index: 5;
}
.viewer-drawer[data-open="false"] { width: 32px; }
.viewer-drawer .drawer-handle {
    pointer-events: auto;
    flex: 0 0 32px;
    width: 32px;
    height: 96px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-left: 0;
    border-radius: 0 6px 6px 0;
    cursor: pointer;
    padding: 0.5rem 0;
    font-weight: 600;
    font-size: 0.85rem;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    gap: 0.35rem;
}
.viewer-drawer .handle-icon { writing-mode: horizontal-tb; transform: rotate(180deg); }
.viewer-drawer .drawer-body {
    pointer-events: auto;
    flex: 1;
    min-width: 0;
    height: 100%;
    padding: 0.75rem;
    overflow-y: auto;
}
.viewer-drawer[data-open="false"] .drawer-body { display: none; }

.viewer-drawer ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.viewer-drawer li { padding: 0.25rem 0.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; font-size: 0.875rem; font-family: ui-monospace, monospace; }

/* Rules drawer — right-edge mirror of the viewer drawer, ONLY shown on the
 * playing page (body.game-started). The waiting page already has the
 * static .rules-panel inside .table-area, so a second copy would be
 * redundant. Half-transparent body so the board shows through while play
 * continues; sits above seat zones (z-index higher than .seat-zone's 1,
 * lower than .game-hub's 2). Anchored to right: 0 with a handle that hugs
 * the right edge. */
body:not(.game-started) .rules-drawer { display: none; }
.rules-drawer {
    position: fixed;
    right: 0;
    top: var(--drawer-top, 11rem);
    display: flex;
    flex-direction: row-reverse;
    align-items: flex-start;
    height: min(540px, 75vh);
    background: transparent;
    border: 0;
    pointer-events: none;
    transition: width 0.18s ease;
    width: 260px;
    /* Above the .game-board overlay (z-index 900) so the handle stays
     * tappable during play; below the result-card overlay (950) and
     * modal dialogs (1000) so it never blocks the win/scoring screens. */
    z-index: 905;
}
.rules-drawer[data-open="false"] { width: 32px; }
.rules-drawer .drawer-handle {
    pointer-events: auto;
    flex: 0 0 32px;
    width: 32px;
    height: 96px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-right: 0;
    border-radius: 6px 0 0 6px;
    cursor: pointer;
    padding: 0.5rem 0;
    font-weight: 600;
    font-size: 0.85rem;
    writing-mode: vertical-rl;
    gap: 0.35rem;
}
.rules-drawer .handle-icon { writing-mode: horizontal-tb; }
.rules-drawer .drawer-body {
    pointer-events: auto;
    flex: 1;
    min-width: 0;
    height: 100%;
    padding: 0.75rem;
    overflow-y: auto;
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--border);
    border-right: 0;
    border-radius: 6px 0 0 6px;
    color: var(--fg);
}
.rules-drawer[data-open="false"] .drawer-body { display: none; }
.rules-drawer dl { margin: 0; display: grid; grid-template-columns: 1fr; gap: 0.5rem; }
.rules-drawer dt { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.125rem; }
.rules-drawer dd { margin: 0; font-size: 0.85rem; }

/* ⚙ settings drawer — mirrors the rules drawer, sits just below it. Present
 * only on the table page (this markup lives in table.html), shown in BOTH 2D
 * and 3D: sound + language always; in-game decor (.settings-decor-3d) is
 * CSS-hidden in 2D. forming-3d.css overrides the top offset during forming. */
.decor-drawer {
    display: flex;
    position: fixed;
    right: 0;
    top: calc(var(--drawer-top, 11rem) + 116px);
    flex-direction: row-reverse;
    align-items: flex-start;
    height: min(440px, 62vh);
    background: transparent;
    border: 0;
    pointer-events: none;
    transition: width 0.18s ease;
    width: 248px;
    z-index: 905;
}
.decor-drawer[data-open="false"] { width: 32px; }
/* In-game decor sections are 3D-only (no tablecloth/scene on the flat 2D board). */
.settings-decor-3d { display: none; }
body.board-3d .settings-decor-3d,
body.forming-3d .settings-decor-3d { display: block; }
/* Sound + language pref rows: full-width label + state, single column. */
.settings-pref-grid { grid-template-columns: 1fr; }
.settings-pref-cell { flex-direction: row; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.4rem 0.55rem; }
.settings-pref-label { font-size: 0.78rem; color: var(--fg); }
.settings-pref-state { font-size: 0.72rem; color: var(--muted); white-space: nowrap; }
.settings-pref-cell.is-current .settings-pref-state { color: var(--accent-gold); }
/* In-drawer language dropdown (mirrors the header selector). */
.settings-lang-select {
    width: 100%;
    background: var(--card-bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 0.35rem 0.5rem;
    font-size: 0.8rem;
    margin-bottom: 0.6rem;
    cursor: pointer;
}
.settings-lang-select:hover { border-color: var(--accent); }
.decor-drawer .drawer-handle {
    pointer-events: auto;
    flex: 0 0 32px;
    width: 32px;
    height: 96px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-right: 0;
    border-radius: 6px 0 0 6px;
    cursor: pointer;
    padding: 0.5rem 0;
    font-weight: 600;
    font-size: 0.85rem;
    writing-mode: vertical-rl;
    gap: 0.35rem;
}
.decor-drawer .handle-icon { writing-mode: horizontal-tb; }
.decor-drawer .drawer-body {
    pointer-events: auto;
    flex: 1;
    min-width: 0;
    height: 100%;
    padding: 0.75rem;
    overflow-y: auto;
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--border);
    border-right: 0;
    border-radius: 6px 0 0 6px;
    color: var(--fg);
}
.decor-drawer[data-open="false"] .drawer-body { display: none; }
.decor-drawer-h { margin: 0.25rem 0 0.4rem; color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
.decor-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.4rem; margin-bottom: 0.6rem; }
.decor-cell { display: flex; flex-direction: column; gap: 0.25rem; align-items: stretch; padding: 0.25rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 5px; cursor: pointer; color: var(--fg); }
.decor-cell:hover { border-color: var(--accent); }
.decor-cell.is-current { border-color: var(--accent-gold); box-shadow: 0 0 0 1px var(--accent-gold) inset; }
.decor-cell .decor-swatch { width: 100%; height: auto; aspect-ratio: 11 / 7; border-radius: 3px; border: 0; }
.decor-cell .decor-cap { font-size: 0.66rem; color: var(--muted); text-align: center; line-height: 1.15; }
.decor-cell.is-current .decor-cap { color: var(--fg); }

/* Scoring-rules drawer (#21) — the SECOND reference drawer, the PRIMARY left
 * panel during play: its handle sits at the top of the play area (just below
 * the header / Leave button) and the body spans down to just above the player's
 * own nameplate, so it's a big, easy-to-read reference. It floats above the
 * board (z-index 905); the half-transparent body lets the board show through.
 * The bottom is kept clear of the player hand + centre claim/discard controls.
 * The viewer drawer drops below this handle (+116px) while a game is running. */
.scoring-drawer { display: none; }
body.game-started .scoring-drawer {
    display: flex;
    position: fixed;
    left: 0;
    top: var(--drawer-top, 11rem);
    /* Explicit height (not top+bottom) so the body's height:100% resolves and
     * the panel fills from just below the header down to ~11rem above the
     * viewport bottom — raised ~1 tile vs the first pass so it never overlaps
     * the player's concealed hand. */
    height: calc(100vh - var(--drawer-top, 11rem) - 11rem);
    flex-direction: row;
    align-items: flex-start;
    background: transparent;
    border: 0;
    pointer-events: none;
    transition: width 0.18s ease;
    /* Wide enough that the longest single meld (nine-gates' 13-tile run) fits on
     * one line, so the body never scrolls horizontally. */
    width: 520px;
    z-index: 905;
}
/* While playing, the spectator drawer drops below the scoring handle so the two
 * left-edge handles don't collide (scoring takes the top slot now). */
body.game-started .viewer-drawer { top: calc(var(--drawer-top, 11rem) + 116px); }
.scoring-drawer[data-open="false"] { width: 32px; }
.scoring-drawer .drawer-handle {
    pointer-events: auto;
    flex: 0 0 32px;
    width: 32px;
    height: 96px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-left: 0;
    border-radius: 0 6px 6px 0;
    cursor: pointer;
    padding: 0.5rem 0;
    font-weight: 600;
    font-size: 0.85rem;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    gap: 0.35rem;
}
.scoring-drawer .handle-icon { writing-mode: horizontal-tb; transform: rotate(180deg); display: flex; }
.scoring-drawer .scoring-handle-icon { width: 18px; height: 18px; }
.scoring-drawer .drawer-body {
    pointer-events: auto;
    flex: 1;
    min-width: 0;
    height: 100%;
    padding: 0.75rem;
    overflow-y: auto;
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--border);
    border-left: 0;
    border-radius: 0 6px 6px 0;
    color: var(--fg);
}
.scoring-drawer[data-open="false"] .drawer-body { display: none; }

/* 🎯 Challenges reminder drawer — TOP-MIDDLE, ONLY shown on the playing page
 * (mirrors .rules-drawer/.scoring-drawer: a mid-game reminder, not needed on
 * the forming/waiting page). Anchored to top-center instead of an edge: the
 * handle is a small tab centered just below the viewport top; the body drops
 * straight down when open (translucent dark panel so it reads over the 3D
 * board/canvas). Same z-tier as the side drawers (905) — above the board
 * (900), below the result overlay (950) and modal dialogs (1000). Reuses the
 * .challenge-item/.challenge-row/.challenge-hand styles defined above (the
 * settings Challenges tab) verbatim. */
body:not(.game-started) .challenge-drawer { display: none; }
.challenge-drawer {
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    width: min(760px, 94vw);
    background: transparent;
    border: 0;
    pointer-events: none;
    z-index: 905;
}
.challenge-drawer .drawer-handle {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.9rem;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 8px 8px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.85rem;
}
.challenge-drawer .drawer-body {
    pointer-events: auto;
    width: 100%;
    max-height: min(60vh, 420px);
    overflow-y: auto;
    margin-top: 0.3rem;
    padding: 0.7rem 0.85rem;
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 10px 10px;
    color: var(--fg);
    box-sizing: border-box;
}
.challenge-drawer[data-open="false"] .drawer-body { display: none; }
.challenge-drawer .challenge-list { margin: 0; }
/* In-game the world is a non-clickable <span> (no navigate mid-game), so drop the
   settings <a>'s dotted underline that would read as a broken link. */
.challenge-drawer .challenge-row-world { border-bottom: 0; cursor: default; }
/* "You can win this right now" highlight — the challenge's ruleset matches
 * this table's. Bolder + gold vs the default accent-colored world label. */
.challenge-row-world.is-here {
    color: var(--gold);
    font-weight: 700;
    border-bottom-style: solid;
}

/* 💬 Chat (#41) — a fixed BOTTOM-RIGHT corner widget, shared by the table page and
 * the lobby. A stable, predictable spot on every surface (no edge-slot collisions).
 * Collapsed = a small pill; open = a panel that rises from the corner. Open/closed
 * persists via the `chatDrawer` cookie; the JS modules wire the toggle + close. */
.chat-corner {
    position: fixed;
    right: 1rem;
    /* Sit above the footer separator line — --footer-h is the footer's measured height (about-modal.js),
     * with a fallback for before the script runs / pages without a footer. */
    bottom: calc(var(--footer-h, 2.6rem) + 0.55rem);
    z-index: 906;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
    pointer-events: none;
}
/* On the table page, lift clear of the bottom-right game-id badge (bottom .6rem). */
.table-page .chat-corner { bottom: 2.9rem; }
.chat-corner > * { pointer-events: auto; }
.chat-corner-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.85rem;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.85rem;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
}
.chat-corner-toggle:hover { border-color: var(--accent); }
.chat-corner-online { font-size: 0.7rem; color: var(--muted); white-space: nowrap; }
/* Open state: hide the pill, show the panel. */
.chat-corner[data-open="true"] .chat-corner-toggle { display: none; }
.chat-corner-panel { display: none; }
.chat-corner[data-open="true"] .chat-corner-panel {
    display: flex;
    flex-direction: column;
    width: min(330px, 92vw);
    height: min(440px, 70vh);
    padding: 0.6rem;
    background: var(--scrim);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
    color: var(--fg);
}
.chat-corner-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.45rem;
    flex: 0 0 auto;
}
.chat-corner-title { font-weight: 700; color: var(--accent); font-size: 0.85rem; }
.chat-corner-close {
    background: none;
    border: 0;
    color: var(--muted);
    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
    padding: 0 0.2rem;
}
.chat-corner-close:hover { color: var(--fg); }
.chat-messages {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    /* Themed scrollbar — see the shared .scoring-doc-scroll group below (chat joins it). */
}
/* A modern messaging-app row: hued avatar + speech bubble. Others align left,
 * own messages right (--self). Consecutive same-sender rows collapse the avatar +
 * name (--cont). A short fade/slide-up gives each arriving line some life. */
.chat-msg {
    display: flex;
    align-items: flex-end;
    gap: 0.4rem;
    max-width: 88%;
    align-self: flex-start;
    font-size: 0.8rem;
    animation: chatMsgIn 0.18s ease both;
}
.chat-msg--self { flex-direction: row-reverse; align-self: flex-end; }
.chat-msg--cont { margin-top: -0.18rem; }
@keyframes chatMsgIn {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}
.chat-avatar {
    width: 26px;
    height: 26px;
    flex: 0 0 auto;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.62rem;
    font-weight: 700;
    color: #f5ecd8;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
    user-select: none;
}
.chat-msg--cont .chat-avatar { visibility: hidden; }
.chat-bubble {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.05rem;
    min-width: 0;
    padding: 0.32rem 0.55rem;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 12px;
    border-bottom-left-radius: 4px;
    word-break: break-word;
    overflow-wrap: anywhere;
}
/* Bubble tail (popover feel) — a small triangle near the avatar side. */
.chat-bubble::after {
    content: "";
    position: absolute;
    bottom: 6px;
    left: -5px;
    width: 0;
    height: 0;
    border: 5px solid transparent;
    border-right-color: var(--border);
    border-left: 0;
}
.chat-msg--cont .chat-bubble::after { display: none; }
.chat-msg--self .chat-bubble {
    background: linear-gradient(180deg, rgba(var(--gold-rgb), 0.22), rgba(var(--gold-rgb), 0.12));
    border-color: var(--accent);
    border-radius: 12px;
    border-bottom-right-radius: 4px;
}
.chat-msg--self .chat-bubble::after {
    left: auto;
    right: -5px;
    border-right: 0;
    border-left: 5px solid var(--accent);
}
.chat-msg-name {
    font-weight: 700;
    font-size: 0.7rem;
    color: var(--accent-gold, #d9b25a);
}
.chat-msg--self .chat-msg-name,
.chat-msg--cont .chat-msg-name { display: none; }
.chat-msg-text { color: var(--fg); line-height: 1.3; }
.chat-msg-time {
    align-self: flex-end;
    font-size: 0.6rem;
    color: var(--muted);
    margin-top: 0.05rem;
}
.chat-empty { margin: auto 0; text-align: center; color: var(--muted); font-size: 0.78rem; }
.chat-input-form { display: flex; gap: 0.3rem; flex: 0 0 auto; }
.chat-input-form input {
    flex: 1;
    min-width: 0;
    padding: 0.4rem 0.5rem;
    background: var(--card-bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 5px;
    font-size: 0.8rem;
}
.chat-input-form input:focus { outline: none; border-color: var(--accent); }
.chat-input-form button {
    flex: 0 0 auto;
    padding: 0.4rem 0.7rem;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    border-radius: 5px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.78rem;
}
.chat-input-form button:hover { filter: brightness(1.08); }
.chat-status { margin: 0.35rem 0 0; font-size: 0.72rem; color: #ffb9ab; text-align: center; }

/* Club chat (#41 Phase 3) reuses the shared corner drawer above. On the /clubs page the
 * drawer stays hidden until a club is opened; the attribute selector beats the base
 * .chat-corner display:flex so toggling the `hidden` attribute actually hides it. */
.chat-corner[hidden] { display: none; }

/* ===================================================================
 * In-app support drawer (feature/support-channel). Mirrors the chat
 * corner drawer above (same gold-accent + blurred-scrim look), but it's
 * a GLOBAL widget (rendered in the header fragment) with three views:
 * the thread list, one thread's bubbles (reusing .chat-messages /
 * .chat-bubble), and the new-request form. Sits left of the chat pill so
 * the two don't overlap where both render (the per-world lobby). ===== */
.support-corner {
    position: fixed;
    right: 1rem;
    bottom: calc(var(--footer-h, 2.6rem) + 0.55rem); /* above the footer separator — see .chat-corner */
    z-index: 905;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
    pointer-events: none;
}
.support-corner > * { pointer-events: auto; }
/* On a page that also has the chat drawer (the per-world lobby / clubs), stack the
 * support pill just above the chat pill so they don't overlap at bottom-right. The
 * two aren't DOM siblings (support lives in the header, chat in <main>), so
 * support-chat.js adds `.has-chat` when it finds #chat-corner rather than a sibling
 * selector. */
/* When the chat drawer is also present, sit the support pill BESIDE the chat pill (same line, just to
 * its left) rather than stacked above it — keeps the bottom-right corner low. Offset ≈ the chat pill's
 * width; support-chat.js measures the real width into --chat-pill-w for an exact fit, this is the
 * fallback. */
.support-corner.has-chat { bottom: calc(var(--footer-h, 2.6rem) + 0.55rem); right: calc(1rem + var(--chat-pill-w, 8.5rem) + 0.5rem); }
.support-corner-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.85rem;
    background: var(--card-bg);
    color: var(--accent);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.85rem;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
}
.support-corner-toggle:hover { border-color: var(--accent); }
/* Unread agent-reply pill on the corner toggle. */
.support-corner-badge {
    min-width: 1.1rem;
    padding: 0 0.32rem;
    border-radius: 999px;
    background: var(--danger, #c0392b);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1.3rem;
    text-align: center;
}
.support-corner-badge[hidden] { display: none; }
.support-corner[data-open="true"] .support-corner-toggle { display: none; }
.support-corner-panel { display: none; }
.support-corner[data-open="true"] .support-corner-panel {
    display: flex;
    flex-direction: column;
    width: min(340px, 92vw);
    height: min(460px, 72vh);
    padding: 0.6rem;
    background: var(--scrim);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
    color: var(--fg);
}
.support-corner-head {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.45rem;
    flex: 0 0 auto;
}
.support-corner-title {
    flex: 1;
    font-weight: 700;
    color: var(--accent);
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.support-back, .support-corner-close {
    background: none;
    border: 0;
    color: var(--muted);
    line-height: 1;
    cursor: pointer;
    padding: 0 0.2rem;
}
.support-back { font-size: 1.05rem; }
.support-corner-close { font-size: 1.25rem; }
.support-back:hover, .support-corner-close:hover { color: var(--fg); }
.support-back[hidden] { display: none; }
/* Each view fills the panel body as a flex column so its list/messages scroll. */
.support-view {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.support-view[hidden] { display: none; }

/* --- View 1: the thread list --- */
.support-thread-list {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}
.support-thread-btn {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    width: 100%;
    text-align: left;
    padding: 0.45rem 0.55rem;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--fg);
    cursor: pointer;
    font: inherit;
}
.support-thread-btn:hover { border-color: var(--accent); }
.support-thread-top {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.support-thread-subject {
    flex: 1;
    min-width: 0;
    font-size: 0.82rem;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.support-thread-dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--danger, #c0392b);
}
.support-thread-meta {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.68rem;
    color: var(--muted);
}
.support-cat-chip {
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    font-size: 0.64rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--accent-gold, #d9b25a);
}
.support-cat-bug { color: #ffb9ab; }
.support-cat-improvement { color: #9ad0a1; }
.support-thread-time { margin-left: auto; }
.support-empty { margin: auto 0; text-align: center; color: var(--muted); font-size: 0.78rem; }
.support-new-btn {
    flex: 0 0 auto;
    align-self: stretch;
    padding: 0.45rem 0.7rem;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    border-radius: 6px;
    cursor: pointer;
    font-weight: 700;
    font-size: 0.8rem;
}
.support-new-btn:hover { filter: brightness(1.08); }

/* --- View 2: thread messages (bubbles reuse .chat-messages / .chat-bubble) + composer --- */
.support-messages {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
.support-input-form { display: flex; gap: 0.3rem; flex: 0 0 auto; align-items: flex-end; }
.support-input-form textarea {
    flex: 1;
    min-width: 0;
    resize: none;
    max-height: 5rem;
    padding: 0.4rem 0.5rem;
    background: var(--card-bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 5px;
    font: inherit;
    font-size: 0.8rem;
    line-height: 1.3;
}
.support-input-form textarea:focus { outline: none; border-color: var(--accent); }
.support-input-form button {
    flex: 0 0 auto;
    padding: 0.4rem 0.7rem;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    border-radius: 5px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.78rem;
}
.support-input-form button:hover { filter: brightness(1.08); }

/* --- View 3: new-request form --- */
.support-new-form {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}
.support-field { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.78rem; color: var(--muted); }
.support-field select,
.support-field textarea {
    padding: 0.4rem 0.5rem;
    background: var(--card-bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 5px;
    font: inherit;
    font-size: 0.82rem;
}
.support-field textarea { resize: vertical; min-height: 4.5rem; line-height: 1.35; }
.support-field select:focus,
.support-field textarea:focus { outline: none; border-color: var(--accent); }
.support-submit {
    flex: 0 0 auto;
    align-self: flex-start;
    padding: 0.45rem 0.9rem;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    border-radius: 6px;
    cursor: pointer;
    font-weight: 700;
    font-size: 0.8rem;
}
.support-submit:hover { filter: brightness(1.08); }
.support-status { margin: 0.35rem 0 0; font-size: 0.72rem; color: #ffb9ab; text-align: center; }
.support-status[hidden] { display: none; }

.support-corner[hidden] { display: none; }

/* Scoring doc content — shared by the in-game drawer body and the lobby
 * popup (#scoring-modal). Vertical scroll; each hand = name + value badge +
 * grouped example tiles + short description. */
/* Popup: a flex column capped to the viewport so ONLY the body scrolls (no
 * nested/double scrollbar), responsive width down to small screens. Scoped to
 * [open] — a bare `.modal.scoring-doc { display:flex }` would override the UA
 * `dialog:not([open]){display:none}` and leave the dialog permanently visible /
 * un-closable. */
.modal.scoring-doc[open],
.modal.scoreboard-doc[open] {
    display: flex;
    flex-direction: column;
    width: min(560px, 94vw);
    max-width: 94vw;
    max-height: 88vh;
    overflow: hidden;
    /* The scroll body now provides the content↔scrollbar gap, so pull the
     * scrollbar close to the dialog edge (small right padding) rather than the
     * default 1.5rem. */
    padding-right: 0.5rem;
}
.scoring-modal-head { flex: 0 0 auto; display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.scoring-modal-title { margin: 0 0 0.5rem; color: var(--accent); display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.scoring-modal-ruleset { color: var(--muted); font-size: 0.85rem; font-weight: 500; }
.scoring-close { background: none; border: 0; color: var(--muted); font-size: 1.1rem; cursor: pointer; line-height: 1; padding: 0.25rem; }
.scoring-close:hover { color: var(--fg); }
/* padding-right keeps the hand list clear of the scrollbar (the bar sits in the
 * gutter outside the padding box). The drawer gets this from its .drawer-body
 * padding; the popup's scroll element had none, so the badges sat against the
 * bar. */
.modal.scoring-doc .scoring-doc-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; min-width: 0; padding-right: 0.6rem; }
/* Side breathing room for the hand list in BOTH the popup and the drawer
 * (on top of the container's own padding). */
.scoring-doc-body { padding-inline: 0.6rem; }
.scoring-section { margin-bottom: 1rem; }
.scoring-section-title { margin: 0.25rem 0 0.5rem; color: var(--accent-gold); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
.scoring-hand { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.scoring-hand:last-child { border-bottom: 0; }
.scoring-hand-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.scoring-hand-name { font-weight: 600; font-size: 0.9rem; }
/* #44: value/closed badges stay right; the "×N" play-count sits next to the name. */
.scoring-hand-badges { display: flex; gap: 0.35rem; align-items: baseline; flex-shrink: 0; margin-left: auto; }
.scoring-hand-value { color: var(--accent); font-size: 0.8rem; font-weight: 600; white-space: nowrap; }
.scoring-hand-closed { color: var(--muted); font-size: 0.66rem; border: 1px solid var(--border); border-radius: 3px; padding: 0 0.3rem; white-space: nowrap; }
/* #44: per-player "×N" play-count badge on a scoring hand card. */
.scoring-hand-count { color: var(--gold, #d4af37); font-size: 0.72rem; font-weight: 700; background: color-mix(in srgb, var(--gold, #d4af37) 15%, transparent); border-radius: 3px; padding: 0 0.3rem; white-space: nowrap; }
/* Example hand: tiles abut WITHIN a meld/set (.scoring-hand-group), groups are
 * spaced apart so the set structure reads at a glance. */
.scoring-hand-tiles { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 9px; margin: 0.45rem 0 0.3rem; }
.scoring-hand-group { display: flex; gap: 1px; }
.scoring-hand-desc { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.35; }
.scoring-empty { color: var(--muted); font-size: 0.85rem; }

/* Global themed scrollbar so EVERY scroll region (page body, settings, drawers,
 * lists, dialogs) matches the Midnight Jade theme instead of falling back to the
 * default OS bar, which clashes on the dark background. The scoped rules further
 * below refine specific regions. Firefox: scrollbar-* (inherited); WebKit/Blink:
 * unprefixed ::-webkit-scrollbar-* applies to all scrollable elements. */
html {
    scrollbar-width: thin;
    scrollbar-color: var(--accent) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: var(--accent);
    border-radius: 6px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--accent-gold); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* Themed scrollbar for the scroll regions (scoring popup body + the in-game
 * scoring drawer + the ⚙ settings drawer) so it reads as part of the game
 * chrome rather than a default OS bar: thin, transparent track, gold-accent
 * thumb. Firefox uses scrollbar-* ; WebKit/Blink use ::-webkit-scrollbar-*. */
.modal.scoring-doc .scoring-doc-scroll,
.scoring-drawer .drawer-body,
.decor-drawer .drawer-body,
.chat-messages {
    scrollbar-width: thin;
    scrollbar-color: var(--accent) transparent;
}
.modal.scoring-doc .scoring-doc-scroll::-webkit-scrollbar,
.scoring-drawer .drawer-body::-webkit-scrollbar,
.decor-drawer .drawer-body::-webkit-scrollbar,
.chat-messages::-webkit-scrollbar { width: 9px; }
.modal.scoring-doc .scoring-doc-scroll::-webkit-scrollbar-track,
.scoring-drawer .drawer-body::-webkit-scrollbar-track,
.decor-drawer .drawer-body::-webkit-scrollbar-track,
.chat-messages::-webkit-scrollbar-track { background: transparent; }
.modal.scoring-doc .scoring-doc-scroll::-webkit-scrollbar-thumb,
.scoring-drawer .drawer-body::-webkit-scrollbar-thumb,
.decor-drawer .drawer-body::-webkit-scrollbar-thumb,
.chat-messages::-webkit-scrollbar-thumb {
    background: var(--accent);
    border-radius: 6px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.modal.scoring-doc .scoring-doc-scroll::-webkit-scrollbar-thumb:hover,
.scoring-drawer .drawer-body::-webkit-scrollbar-thumb:hover,
.decor-drawer .drawer-body::-webkit-scrollbar-thumb:hover {
    background: var(--accent-gold);
    background-clip: padding-box;
}

/* ── Scoring playground (#28) — tabbed scoring popup ────────────────────────
 * The popup carries two tabs: "Hands from tiles" (reference) and "Try a hand"
 * (playground). One panel shows at a time, each scrolling independently. */
.modal.scoring-doc.has-tabs[open],
.modal.scoreboard-doc.has-tabs[open] { width: min(920px, 96vw); }
.scoring-tabs { flex: 0 0 auto; display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border); margin-bottom: 0.6rem; }
.scoring-tab { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted); font: inherit; font-size: 0.85rem; padding: 0.4rem 0.75rem; cursor: pointer; margin-bottom: -1px; }
.scoring-tab:hover { color: var(--fg); }
.scoring-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.scoring-tabpanels { flex: 1 1 auto; min-height: 0; display: flex; }
.scoring-tabpanel { flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; }
.scoring-tabpanel[hidden] { display: none; }
.scoring-playground-scroll { padding-right: 0.6rem; scrollbar-width: thin; scrollbar-color: var(--accent) transparent; }
.scoring-playground-scroll::-webkit-scrollbar { width: 9px; }
.scoring-playground-scroll::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; }

/* ── Leaderboard popup (#50) — global + per-ruleset skill-point boards ──────
 * Reuses `.modal.scoring-doc[open]`/`.has-tabs[open]` sizing (extended
 * selector lists above) and `.scoring-tabs`/`.scoring-tab`/`.scoring-tabpanels`/
 * `.scoring-tabpanel` verbatim for the tab switch. The issue asks for a
 * "half-transparent window" — override the base `.modal`'s opaque
 * `--card-bg` (line ~784) with the same translucent/frosted `--scrim` + blur
 * look used by `.scoring-drawer .drawer-body`; `.modal.scoreboard-doc[open]`
 * outranks the single-class `.modal` on specificity alone, so this wins
 * regardless of source order. */
.modal.scoreboard-doc[open] {
    background: var(--scrim);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.scoreboard-modal-head { flex: 0 0 auto; display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.scoreboard-modal-title { margin: 0 0 0.25rem; color: var(--accent); display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
#scoreboard-modal-scope { color: var(--muted); font-size: 0.85rem; font-weight: 500; }
.scoreboard-note { color: var(--muted); font-size: 0.8rem; line-height: 1.35; margin: 0 0 0.6rem; }

/* Rank · player (avatar+name[+You tag]) · points · wins · matches. The player
 * column takes the remaining space; the numeric columns are fixed-ish so the
 * grid stays legible narrow (a @media rule below tightens it further). */
.scoreboard-list { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.scoreboard-row {
    display: grid;
    grid-template-columns: 2rem minmax(0, 1fr) 4rem 3.5rem 4rem;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.5rem;
    border-radius: 4px;
    font-size: 0.85rem;
    min-width: 0;
}
.scoreboard-row.scoreboard-head {
    color: var(--muted);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-bottom: 0.3rem;
    margin-bottom: 0.15rem;
    border-bottom: 1px solid var(--border);
}
.scoreboard-cell-rank { text-align: center; color: var(--muted); font-weight: 600; }
.scoreboard-row.scoreboard-head .scoreboard-cell-rank { text-align: center; }
.scoreboard-cell-player { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.scoreboard-cell-points, .scoreboard-cell-wins, .scoreboard-cell-matches {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.scoreboard-row:not(.scoreboard-head) .scoreboard-cell-points { color: var(--accent-gold, #eac558); font-weight: 600; }
.scoreboard-avatar {
    flex: 0 0 auto;
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 50%;
    background: var(--accent);
    color: var(--bg);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
}
.scoreboard-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.scoreboard-you-tag {
    flex: 0 0 auto;
    color: var(--accent);
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    border: 1px solid var(--accent);
    border-radius: 3px;
    padding: 0 0.3rem;
}
.scoreboard-row.is-you {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
.scoreboard-empty { color: var(--muted); font-size: 0.85rem; padding: 0.75rem 0.5rem; }

@media (max-width: 480px) {
    .scoreboard-row {
        grid-template-columns: 1.6rem minmax(0, 1fr) 3.2rem 2.7rem 3.2rem;
        gap: 0.3rem;
        font-size: 0.78rem;
        padding: 0.35rem 0.35rem;
    }
    .scoreboard-avatar { width: 1.6rem; height: 1.6rem; font-size: 0.6rem; }
}

/* Opener button (Task 5 wiring) — one on the per-ruleset lobby header, one on the
 * immersive worlds globe topbar. A pill in the same visual family as `.jade-chip`
 * so it reads as a header control on both pages/themes; the icon dims/brightens
 * on hover like `.ruleset-title-icon`. */
.scoreboard-open {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.3rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: rgba(217, 176, 74, 0.10);
    color: var(--fg);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.scoreboard-open:hover { color: var(--accent); border-color: var(--accent); }
.scoreboard-open-icon { width: 1rem; height: 1rem; flex-shrink: 0; opacity: 0.75; transition: opacity 0.15s ease; }
.scoreboard-open:hover .scoreboard-open-icon { opacity: 1; }
/* Immersive globe overlay: frosted-dark treatment matching the challenge-ticker
 * so the pill reads against the starfield instead of assuming a lobby card bg. */
body.worlds-immersive .worlds-topbar .scoreboard-open {
    background: rgba(5, 10, 18, 0.55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

.pp-root { display: flex; flex-direction: column; gap: 0.7rem; }
.pp-title { margin: 0; color: var(--accent); font-size: 1rem; }
.pp-intro, .pp-hint { margin: 0; color: var(--muted); font-size: 0.78rem; line-height: 1.35; }
.pp-loading, .pp-error { color: var(--muted); font-size: 0.85rem; }
.pp-error { color: var(--danger); }
.pp-field-label, .pp-subfield-label { color: var(--accent-gold); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.3rem; }

/* Palette: the full wall multiset laid out in sequence. One row per family
 * (man / pin / sou / honours / flowers / jokers); within a row, one column per
 * kind in rank order, with that kind's copies stacked as lines (4 for numbered
 * + honours; the 5-column's 4th line is the red five). A placed copy greys out. */
.pp-palette { display: flex; flex-direction: column; gap: 0.6rem; }
/* Each row pairs two family-groups (man|pin, then sou|honours+flowers) with a
 * wide gap between the groups. */
.pp-pal-row { display: flex; flex-wrap: wrap; gap: 1.6rem; align-items: flex-start; }
.pp-pal-fam { display: flex; flex-wrap: wrap; gap: 4px; }
.pp-pal-col { display: flex; flex-direction: column; gap: 2px; }
.pp-pal-tile { cursor: grab; border-radius: 3px; padding: 1px; line-height: 0; }
.pp-pal-tile:hover { outline: 1px solid var(--accent); }
.pp-pal-tile:active { cursor: grabbing; }
.pp-pal-tile.pp-consumed { opacity: 0.2; filter: grayscale(1); pointer-events: none; cursor: default; }
.pp-handcount { float: right; font-size: 0.72rem; font-weight: 700; color: var(--muted); text-transform: none; letter-spacing: 0; }
.pp-handcount-full { color: var(--accent-gold); }

/* Ponds + drop zones. */
.pp-ponds { display: flex; flex-direction: column; gap: 0.55rem; }
.pp-pond { border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem; }
.pp-pond.pp-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.pp-pond-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.3rem; }
.pp-pond-label { color: var(--muted); font-size: 0.74rem; }
.pp-drop { display: flex; flex-wrap: wrap; gap: 4px; min-height: 42px; align-items: center; border-radius: 4px; padding: 2px; }
.pp-drop:empty::before { content: attr(data-empty); color: var(--muted); font-size: 0.72rem; opacity: 0.7; padding: 0 0.3rem; }
.pp-drop-over { background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* A placed tile: face + corner remove; winner gets a gold ring. */
.pp-tile { position: relative; line-height: 0; border-radius: 4px; }
.pp-tile-face { cursor: pointer; display: inline-block; line-height: 0; }
.pp-tile-winner { outline: 2px solid var(--accent-gold); outline-offset: 1px; border-radius: 4px; }
.pp-tile-x { position: absolute; top: -6px; right: -6px; width: 15px; height: 15px; border-radius: 50%; border: 0; background: var(--card-bg); color: var(--muted); font-size: 9px; line-height: 1; cursor: pointer; padding: 0; opacity: 0; transition: opacity 0.12s; }
.pp-tile:hover .pp-tile-x { opacity: 1; }
.pp-tile-x:hover { color: var(--danger); }
.pp-tile-fallback { font-size: 0.7rem; color: var(--muted); padding: 0 2px; }

/* Melds: a fixed 2×2 grid of up to four exposed sets. */
.pp-melds-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; }
.pp-meld { border: 1px dashed var(--border); border-radius: 5px; padding: 0.3rem; }
.pp-meld.pp-active { border-color: var(--accent); border-style: solid; }
.pp-meld-head { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; margin-bottom: 0.25rem; min-height: 1.3rem; }
/* Inferred meld type (no dropdown) + the concealed/revealed kong choice that
 * appears only once four tiles are placed. */
.pp-meld-typelabel { font-size: 0.75rem; color: var(--accent-gold); font-weight: 600; }
.pp-kong-toggle { display: flex; gap: 2px; }
.pp-kong-opt { background: var(--card-bg); color: var(--muted); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font: inherit; font-size: 0.68rem; padding: 0.1rem 0.4rem; }
.pp-kong-opt:hover { color: var(--fg); border-color: var(--accent); }
.pp-kong-opt.is-active { background: var(--accent); color: var(--bg); border-color: var(--accent); }

/* Context controls. */
.pp-context { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.6rem; }
.pp-radios, .pp-flags { display: flex; flex-wrap: wrap; gap: 0.5rem 0.9rem; }
.pp-radio, .pp-flag { display: flex; align-items: center; gap: 0.3rem; font-size: 0.8rem; cursor: pointer; }
.pp-winds { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.pp-wind { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.75rem; }
.pp-wind-label { color: var(--muted); }
.pp-wind select { padding: 0.25rem; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px; }
.pp-subfield-label { margin-top: 0.2rem; }

/* Actions + result. */
.pp-actions { display: flex; gap: 0.5rem; }
.pp-btn { padding: 0.4rem 0.9rem; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
.pp-btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.pp-btn:hover { border-color: var(--accent); }
.pp-result { border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.6rem; min-height: 2.4rem; }
.pp-result-empty, .pp-result-pending { color: var(--muted); font-size: 0.8rem; margin: 0; }
.pp-result-invalid { color: var(--danger); font-size: 0.84rem; margin: 0; }
/* Below-minimum: the hand scores but doesn't clear the win threshold — a
 * warning callout (amber tint, left accent bar, ⚠ icon) above the still-shown
 * score breakdown. */
.pp-result-belowmin {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    color: var(--accent-gold);
    background: color-mix(in srgb, var(--accent-gold) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent-gold) 45%, transparent);
    border-left-width: 3px;
    border-radius: 5px;
    padding: 0.4rem 0.6rem;
    margin: 0 0 0.45rem;
    font-size: 0.82rem;
    font-weight: 600;
}
.pp-result-belowmin::before {
    content: "⚠";
    font-size: 0.95rem;
    line-height: 1;
    flex-shrink: 0;
}
/* Graphical result, aligned with the in-game result board: total header, the
 * grouped winning hand (winning set highlighted via the shared .result-hand-set
 * .wait / .winning-tile rules), open melds, then a per-yaku/fan breakdown with
 * each item's anchor tiles. */
.pp-result-card { display: flex; flex-direction: column; gap: 0.5rem; }
.pp-score-head { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.pp-score-value { color: var(--accent); font-size: 1.05rem; font-weight: 700; }
.pp-score-fu { color: var(--muted); font-size: 0.8rem; }
.pp-score-cap { color: var(--accent-gold); font-size: 0.82rem; font-weight: 700; }
.pp-hand-grouped { margin: 0; gap: 10px; }
.pp-result-melds { display: flex; gap: 3px; flex-wrap: wrap; }
.pp-result-breakdown { display: flex; flex-direction: column; gap: 1px; margin-top: 0.2rem; }
.pp-breakdown-row { padding: 0.15rem 0; border-top: 1px solid var(--border); }

/* Lobby ruleset title doubles as the scoring-doc opener (#21). Inline SVG icon
 * (matches the page's view-toggle icons) sized to the title and tinted via
 * currentColor; dims slightly until hover. */
.ruleset-title-open { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 0.5rem; }
.ruleset-title-open:hover { color: var(--accent); }
.ruleset-title-icon { width: 1.15rem; height: 1.15rem; flex-shrink: 0; opacity: 0.6; transition: opacity 0.15s ease; }
.ruleset-title-open:hover .ruleset-title-icon { opacity: 1; }

/* Rules panel: static card, no fold. Fills full panel height (stretched by
 * the grid) so its bottom edge aligns with the table-grid + drawer. */
.rules-panel {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.75rem 1rem;
    overflow-y: auto;
}
.rules-panel h2 { margin: 0 0 0.5rem 0; color: var(--accent); font-size: 0.95rem; font-weight: 600; }
.rules-panel .panel-icon { margin-right: 0.25rem; }
.rules-panel dl { margin: 0; display: grid; grid-template-columns: 1fr; gap: 0.5rem; }
.rules-panel dt { color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.125rem; }
.rules-panel dd { margin: 0; color: var(--fg); font-size: 0.875rem; }

@media (max-width: 760px) {
    /* Stack table + rules vertically on narrow viewports. Drawer stays
     * fixed to the left edge and floats over content. */
    .table-area {
        grid-template-columns: minmax(0, 540px);
        justify-content: center;
    }
    .table-area .table-grid { grid-column: 1; }
    .rules-panel  { grid-column: 1; align-self: stretch; max-width: 540px; }
}

.viewer-count { color: var(--muted); font-size: 0.875rem; margin-left: 0.25rem; }
/* Foldable Events: <details>/<summary> with the accent summary styled like
 * the other section headings. Defaults open (see template); user toggles. */
.event-log { margin: 1.5rem auto 0 auto; max-width: 540px; }
.event-log summary { cursor: pointer; color: var(--accent); font-size: 1.25rem; font-weight: 600; padding: 0.25rem 0; user-select: none; list-style: revert; }
.event-log summary::-webkit-details-marker { color: var(--accent); }
.event-log ol { list-style: none; padding: 0; margin: 0.5rem 0 0 0; font-family: ui-monospace, monospace; font-size: 0.875rem; max-height: 240px; overflow-y: auto; }
.event-log li { padding: 0.25rem 0; border-bottom: 1px solid var(--border); }

/* Header current player */
.player-name { color: var(--accent); }

/* Header user menu — native <details>/<summary> disclosure.
 *
 * The summary is the click target: avatar + player name + a caret that
 * rotates when open. We hide the default disclosure triangle (varies per
 * browser; both forms are needed for full coverage). The dropdown panel is
 * absolutely positioned under the trigger, right-aligned to the trigger so
 * a long player name doesn't push the panel off the right edge.
 *
 * `header nav a` sets a left margin on every link; we override that inside
 * the menu list since items are flow-laid in a column.
 */
.header-menu { position: relative; }
.header-menu summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.6rem;
    border-radius: 4px;
    user-select: none;
}
.header-menu summary::-webkit-details-marker { display: none; }
.header-menu summary::marker { display: none; content: ""; }
.header-menu summary:hover,
.header-menu[open] summary { background: var(--card-bg); }
.header-menu .avatar { border-color: var(--accent); }
.header-menu .header-menu-caret {
    width: 10px;
    height: 7px;
    color: var(--muted);
    transition: transform 0.15s ease;
    flex: 0 0 auto;
}
.header-menu[open] .header-menu-caret { transform: rotate(180deg); }
.header-menu-list {
    position: absolute;
    top: calc(100% + 0.35rem);
    right: 0;
    list-style: none;
    padding: 0.35rem 0;
    margin: 0;
    min-width: 180px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
    z-index: 50;
}
.header-menu-list a {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.5rem 0.9rem;
    margin: 0;
    color: var(--fg);
    text-decoration: none;
}
.header-menu-list a:hover { background: var(--bg); color: var(--accent); }

/* Brand: layered lotus-platform mark + "LotusTable" wordmark, vertically
 * centered. The mark sits at roughly 2× the wordmark cap height so the
 * texture is legible at viewing distance. */
header .brand { display: inline-flex; align-items: center; gap: 0.6rem; }
/* Themed brand banner = transparent emblem + CSS wordmark. The emblem viewBox is
   wider than tall (~240×210); size by height + keep aspect so it isn't squished. */
header .brand .brand-icon {
  height: 74px; width: auto; display: block; flex: 0 0 auto; object-fit: contain;
}
header .brand .brand-text { line-height: 1; display: inline-flex; align-items: baseline; }
/* "Lotus" (warm) + accent "Table" (gold) wordmark, in the display serif. */
.bw-lotus { font-family: var(--font-display); color: var(--fg); }
.bw-table { font-family: var(--font-display); color: var(--accent); }
@media (max-width: 560px) {
  header .brand { font-size: 1.35rem; }
  header .brand .brand-icon { height: 54px; }
}

/* Avatars — same SVG markup, three CSS-driven sizes. Round, with a subtle
 * border to lift them off the page background. The inner <svg> is sized to
 * 100% so the wrapper controls dimensions. */
.avatar { display: inline-block; line-height: 0; border-radius: 50%; overflow: hidden; border: 1px solid var(--border); background: var(--card-bg); flex: 0 0 auto; vertical-align: middle; }
.avatar .avatar-svg { width: 100%; height: 100%; display: block; }
.avatar-sm { width: 20px; height: 20px; }
.avatar-md { width: 28px; height: 28px; }
.avatar-lg { width: 48px; height: 48px; }

/* Viewer list row: avatar + name aligned horizontally. */
.viewer-drawer li { display: flex; align-items: center; gap: 0.5rem; }
.viewer-drawer li .viewer-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Avatar picker (/me/avatar) — a focused single-purpose form, kept narrow. */
main.avatar-picker { max-width: 640px; }
/* The unified settings hub (/me/settings) carries the appearance grids, stat cards,
   play report + the challenge rows — it uses the app's standard responsive width
   (the base main: max-width 1100px, centered, fluid below that) rather than the
   narrow avatar cap, so it fills a desktop monitor instead of a 640px column. */
main.settings.avatar-picker { max-width: 1100px; }
main.avatar-picker h1 { color: var(--accent); margin: 0 0 0.5rem 0; }
.appearance-head { display: flex; align-items: baseline;
    justify-content: space-between; gap: 1rem; }
.appearance-head h1 { margin-bottom: 0; }
.appearance-back { color: var(--muted); text-decoration: none;
    font-size: 0.9rem; white-space: nowrap; }
.appearance-back:hover { color: var(--accent); }
main.avatar-picker .muted { color: var(--muted); }
main.avatar-picker .muted.small { font-size: 0.85rem; }
.avatar-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.75rem; margin: 1.25rem 0; }
.avatar-tile { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 0.9rem 0.5rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); cursor: pointer; transition: border-color 0.15s ease, transform 0.1s ease; }
.avatar-tile:hover { border-color: var(--accent); }
.avatar-tile:active { transform: translateY(1px); }
.avatar-tile.is-current { border-color: var(--accent-gold); box-shadow: 0 0 0 1px var(--accent-gold) inset; }
.avatar-tile .tile-label { font-size: 0.85rem; color: var(--muted); }
.avatar-tile.is-current .tile-label { color: var(--fg); }
/* Tablecloth (#12) / Scene (#13) picker swatches: the velvet canvas swatch
   and the scene-thumbnail <img> share one fixed, rounded preview box. */
.decor-swatch { display: block; width: 100%; max-width: 9rem; aspect-ratio: 11 / 7; border-radius: 5px; border: 1px solid var(--border); object-fit: cover; }
.decor-swatch.decor-plain { background: #1a3024; }
/* Logo-theme preview on the Appearance "Logo" cards — the direction's emblem,
   aspect-preserved (emblem viewBox is ~8:7). */
.logo-card-emblem { display: block; width: 100%; max-width: 9rem; aspect-ratio: 8 / 7; object-fit: contain; }
/* Appearance page — tile-set and display-toggle sections. */
main.avatar-picker h2 { color: var(--fg); font-size: 1.05rem;
    margin: 1.75rem 0 0.15rem 0; }
/* Tabbed nav between Avatar / Tile set / Display / Sound effect. */
.appearance-tabs { display: flex; gap: 0; margin: 1.25rem 0 0 0;
    border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.appearance-tabs button { background: transparent; color: var(--muted);
    border: 0; border-bottom: 2px solid transparent; padding: 0.6rem 1rem;
    margin-bottom: -1px; cursor: pointer; font-size: 0.95rem;
    font-family: inherit; transition: color 0.15s ease,
    border-color 0.15s ease; }
.appearance-tabs button:hover { color: var(--fg); }
.appearance-tabs button[aria-selected="true"] { color: var(--accent-gold);
    border-bottom-color: var(--accent-gold); }
/* Second-level tabs inside the settings Appearance tab — same treatment as the
   top-level tabs but slightly smaller/inset so the nesting reads. */
.appearance-subtabs { display: flex; gap: 0; margin: 0.85rem 0 0 0;
    border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.appearance-subtabs button { background: transparent; color: var(--muted);
    border: 0; border-bottom: 2px solid transparent; padding: 0.45rem 0.85rem;
    margin-bottom: -1px; cursor: pointer; font-size: 0.85rem;
    font-family: inherit; transition: color 0.15s ease, border-color 0.15s ease; }
.appearance-subtabs button:hover { color: var(--fg); }
.appearance-subtabs button[aria-selected="true"] { color: var(--accent-gold);
    border-bottom-color: var(--accent-gold); }
.tab-panel { padding-top: 0.5rem; }
.tab-panel > .muted.small:first-child { margin-top: 0.75rem; }
.tileset-list { display: flex; flex-direction: column; gap: 0.75rem;
    margin: 1.25rem 0; }
.tileset-card { display: flex; flex-direction: column; gap: 0.5rem;
    padding: 0.9rem; background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 6px; color: var(--fg); cursor: pointer; text-align: left;
    transition: border-color 0.15s ease; }
.tileset-card:hover { border-color: var(--accent); }
.tileset-card.is-current { border-color: var(--accent-gold);
    box-shadow: 0 0 0 1px var(--accent-gold) inset; }
.tileset-name { font-weight: 700; color: var(--muted); }
.tileset-card.is-current .tileset-name { color: var(--accent-gold); }
.tileset-preview { display: flex; flex-direction: column; gap: 4px; }
.tileset-line { display: flex; gap: 4px; }
.tileset-line .lt-tile { width: 46px; height: auto; }
.pref-state { font-size: 1.35rem; font-weight: 700;
    color: var(--accent-gold); line-height: 1.2; }
.avatar-tile:not(.is-current) .pref-state { color: var(--muted); }

/* ============================================================ *
 *  In-game diamond board (game-render.js)                       *
 * ============================================================ */

/* Tiles scale with the maximized board (vmin) but stay bounded so they
   never get microscopic on a phone or absurd on a 4K panel. */
/* Each width is the scaled clamp, but hard-capped to a fraction of the
   real board so a full hand row can never be wider/taller than the board
   (which is itself <= viewport). On a small/short window the cap wins and
   tiles shrink to fit — the self hand stays fully on-screen and clickable
   regardless of resize. The cap divisors leave room for ~14 hand tiles
   (lg) / opponent rows (sm) plus gaps. */
.lt-tile { display: block; aspect-ratio: 36 / 50; height: auto;
  width: min(clamp(calc(22px * var(--tile-scale)),
    calc(3.4vmin * var(--tile-scale)), calc(52px * var(--tile-scale))),
    calc(var(--board) / 17)); }
.lt-tile--sm { width: var(--sm-w); }
.lt-tile--md { width: var(--md-w); }
.lt-tile--lg { width: var(--lg-w); }
.lt-tile--xl { width: var(--xl-w); }
.lt-tile--rot90 { transform: rotate(90deg); }

/* Live-3D overlay tile placeholders (issue #29). When the 3D board is active,
   the claim window + result card emit these empty boxes where an SVG tile
   would go; tile-overlay-3d.js pins a live 3D tile viewport over each box (a
   single shared WebGL canvas at z-index 906, scissor-clipped per box). The
   box reserves the same layout footprint as the SVG tile so the surrounding
   chrome is unchanged. The box itself is transparent — the canvas paints the
   tile. A rotated (called) tile swaps to a landscape box so the live tile,
   framed to fill the box, renders at full size and reserves the horizontal
   room its upright neighbours expect. */
.tile3d-slot { display: inline-block; vertical-align: middle;
  aspect-ratio: 36 / 50; background: transparent; }
.tile3d-slot.tile3d-sm { width: var(--sm-w); }
.tile3d-slot.tile3d-md { width: var(--md-w); }
.tile3d-slot.tile3d-lg { width: var(--lg-w); }
.tile3d-slot[data-rot="90"] { aspect-ratio: 50 / 36; }
.tile3d-slot.tile3d-sm[data-rot="90"] { width: calc(var(--sm-w) * 50 / 36); }
.tile3d-slot.tile3d-md[data-rot="90"] { width: calc(var(--md-w) * 50 / 36); }
.tile3d-slot.tile3d-lg[data-rot="90"] { width: calc(var(--lg-w) * 50 / 36); }
/* Contiguous tile ROW (claim window): cells abut with NO gap so the live 3D
   overlay paints one seamless meld over the whole row. Each cell reserves the
   same footprint as the corresponding tile (rotated cells go landscape). */
.tile3d-row { display: inline-flex; align-items: center; gap: 0; }
.tile3d-cell { display: inline-block; aspect-ratio: 36 / 50; background: transparent; }
.tile3d-cell.c-sm { width: var(--sm-w); }
.tile3d-cell.c-md { width: var(--md-w); }
.tile3d-cell.c-lg { width: var(--lg-w); }
.tile3d-cell.c-xl { width: var(--xl-w); }
.tile3d-cell[data-rot="90"] { aspect-ratio: 50 / 36; }
.tile3d-cell.c-sm[data-rot="90"] { width: calc(var(--sm-w) * 50 / 36); }
.tile3d-cell.c-md[data-rot="90"] { width: calc(var(--md-w) * 50 / 36); }
.tile3d-cell.c-lg[data-rot="90"] { width: calc(var(--lg-w) * 50 / 36); }
.tile3d-cell.c-xl[data-rot="90"] { width: calc(var(--xl-w) * 50 / 36); }

/* Full-viewport immersive overlay: covers the global + table-page header
   (connection pill at z-1000 stays on top; in-overlay .game-exit handles
   leaving). Mounted by game-render.js ensureMounted() over the hidden
   .table-area. */
.game-board {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: radial-gradient(ellipse at center, #1c2b22, var(--bg) 72%);
  padding: 12px;
  overflow: hidden;
  /* Tap/double-tap to discard a tile must never select or copy text — a
     fast double-click on a tile otherwise highlights nearby content. The
     game-id badge re-enables selection on itself (user-select: text). */
  user-select: none;
  -webkit-user-select: none;
}
/* The HTML `hidden` attribute is a low-specificity UA rule; the `display`
   declarations above (and on .game-actionbar) override it, so without these
   the empty pre-game overlay blankets the seat table. Same guard as
   .connection-pill[hidden]. */
.game-board[hidden], .game-actionbar[hidden] { display: none; }
/* Maximized square: as large as the viewport allows, reserving room for
   the fixed bottom action bar so a short-landscape window can't clip it. */
.board-grid {
  width: var(--board);
  height: var(--board);
  display: grid;
  /* Side columns wide enough for the vertical hand|pond|melds strips.
     minmax(0,…) (same as the rows) is essential: without it the wide
     self-hand row forces the center column past its share and the whole
     grid overflows to the right instead of staying centered. Tiles are
     already capped to fractions of --board, so the content still fits. */
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 2fr) minmax(0, 1.4fr);
  /* Middle row holds the tall vertical side strips (~13 tiles) plus the
     center hub; self (bottom) row stacks plate+melds+pond+lg hand so it
     gets the next-largest share. min-height:0 lets cells constrain
     children instead of growing the grid past the viewport. */
  grid-template-rows: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1.6fr);
  gap: 4px;
}
/* Seat block: ONE structure for every player, rotated as a unit.
   Each seat is a single block -- melds (left) + discards (right) share
   the top level, the concealed hand spans the row below, the name tag
   the row below that (the player / outer-edge side). The whole block is
   then rotated to face its player: bottom 0, top 180, left +90, right
   -90. The hand is never re-flowed; the block just turns. transform
   does not change the grid cell, so each block stays centered in its
   cell and rotates in place. */
.seat-zone {
  /* --row-w: tile width used for melds/pond in this zone; --hand-w: the
     concealed-hand tile width. Defaults are the self (bottom) seat
     (sm pond/melds, lg hand); opponent zones override below. */
  --row-w: var(--sm-w);
  --hand-w: var(--lg-w);
  display: grid;
  /* FIXED width too: the concealed hand spans both columns and is the
     widest row, so with auto columns its width drove the whole grid's
     width — every draw/discard changed it and place-content recentred
     the block, drifting melds/pond. Reserve the max hand width (14
     tiles + gaps/just-drawn margin) split 1fr/1fr; a shorter hand just
     centres inside this constant box instead of resizing it.
     --max-hand-tiles is 14 for 13-tile rulesets, 17 for Taiwanese (its
     16-tile hand peaks at 17 to win) — see the main.table-page rules. */
  width: calc(var(--max-hand-tiles, 14) * var(--hand-w) + 40px);
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "melds pond"
    "hand  hand"
    "tag   tag";
  /* FIXED row heights — the melds/pond band reserves exactly 3 tile
     lines (the discard-pond max) so a full 3-line pond's bottom line
     sits right above the hand, while a 1-line pond still starts at the
     top and grows down. The hand band is one tile. width AND rows fixed
     => the block is constant-size in both axes, so centring it in the
     fixed cell keeps melds/discards from drifting through the hand.
     (A rare 4th meld line overflows ~1 line into the hand row, but a
     4-meld hand has only 1 concealed tile so they don't collide.) */
  grid-template-rows:
    calc(3 * var(--row-w) * var(--ar) + 8px)
    calc(var(--hand-w) * var(--ar))
    auto;
  place-content: center;
  justify-self: center;
  align-self: center;
  justify-items: center;
  align-items: center;
  /* column-gap 0 so the 1fr|1fr boundary IS the block's centre line —
     pond.justify-self:start puts the pond's left edge exactly on centre. */
  gap: 6px 0;
  overflow: visible;
  min-height: 0;
}
/* melds + pond are pinned to the TOP of their reserved band and fill
   downward — the first meld/discard line is always the top line and
   placed tiles never move. The band height (grid row 1) is sized so a
   full 3-line pond's bottom line ends up right above the concealed
   hand; a 1-line pond simply leaves empty space below it (intended:
   discards start at the top and grow toward the hand). */
/* justify-self:start pins the box to the LEFT edge of the half-column,
   then margin-left pushes it back rightward by (--meld-shift) hand-tile
   widths so it sits part-way between the seat-zone outer-left and the
   centre line. Uses --hand-w (per-zone concealed-hand tile width: lg
   self, top top zone, side left/right) so the margin scales with the
   zone's own size. --meld-shift defaults to 2.8 (matches the self-zone
   position the user dialled in); opponent zones override it lower to
   put more breathing room between their melds and the centre-line
   pond — opponent melds inherit the parent box width (4.44 * top/side
   for a kan) which is a larger fraction of their narrower half-column
   than for self, so they look "closer to pond" at the same shift factor. */
.seat-zone { --meld-shift: 2.8; }
.seat-melds { grid-area: melds; align-self: start; justify-self: start;
  margin-left: calc(var(--hand-w) * var(--meld-shift)); }
/* Discard pond starts AT the centre line (left edge on the 1fr|1fr
   boundary) and grows rightward, instead of being centred in the right
   half. justify-self overrides the container's justify-items:center. */
.seat-pond  { grid-area: pond;  align-self: start; justify-self: start; }
.seat-hand  { grid-area: hand; }
.seat-plate { grid-area: tag; }
/* HKMA Classical exposed flowers — share the `tag` grid-area with the
   plate, sitting beside it (or stacking below on tight zones). Grid
   auto-placement puts multiple children in the same named area in
   document order; pairing with display:flex on the area's container
   isn't possible here (each child is its own grid item), so we use
   justify-self / align-self to nudge the row beside the plate. */
.seat-flowers { grid-area: tag; justify-self: end; align-self: center;
  display: flex; flex-wrap: wrap; gap: 2px; max-width: 50%; }
/* Hand→pond reveal — fired at doraRevealed for deal-time flowers and on
   each tileDrawn/seatDrew with flowersExposed for mid-game draws. The
   tile starts pulled toward the seat's hand row (translateY positive
   within the zone — opponent zones are rotated so it points to their
   hand) and lands at the pond. Duration matches game-render.js
   FLOWER_REVEAL_MS + game-render-3d.js FLOWER_FLY_MS. */
@keyframes flower-reveal {
  0%   { transform: translateY(48px) scale(1.25); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: translateY(0) scale(1);       opacity: 1; }
}
.seat-flowers .flower-reveal {
  animation: flower-reveal 700ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .seat-flowers .flower-reveal { animation: none; }
}
/* Flower tiles use the same sm width as the discard pond. Opponent zones
   inherit --row-w via the `.seat-zone--{top,left,right} .lt-tile` rules
   below; for the self zone we leave the default lt-tile--sm width. */
.seat-zone--bottom { grid-column: 2; grid-row: 3; }
.seat-zone--top    { grid-column: 2; grid-row: 1; transform: rotate(180deg);
  --row-w: var(--top-w); --hand-w: var(--top-w); --meld-shift: 1.4; }
.seat-zone--left   { grid-column: 1; grid-row: 2; transform: rotate(90deg);
  --row-w: var(--side-w); --hand-w: var(--side-w); --meld-shift: 1.4; }
.seat-zone--right  { grid-column: 3; grid-row: 2; transform: rotate(-90deg);
  --row-w: var(--side-w); --hand-w: var(--side-w); --meld-shift: 1.4; }
/* Opponent tiles smaller so a rotated side/top block fits its cell. */
.seat-zone--top .lt-tile { width: var(--top-w); }
.seat-zone--left .lt-tile, .seat-zone--right .lt-tile { width: var(--side-w); }

.seat-plate { display: flex; align-items: center; gap: 9px;
  font: 600 16px ui-sans-serif; color: var(--accent);
  background: #0f0d0bcc; border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 14px; }
.seat-plate.is-turn { border-color: var(--accent-gold);
  animation: turn-pulse 1.4s ease-in-out infinite; }
@keyframes turn-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(212,168,71,0.35); }
  50%      { box-shadow: 0 0 16px 4px rgba(212,168,71,0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .seat-plate.is-turn { animation: none;
    box-shadow: 0 0 14px 3px rgba(212,168,71,0.5); }
}
.seat-wind { display: grid; place-items: center; width: 27px; height: 27px;
  border-radius: 50%; background: var(--border); color: var(--accent-gold);
  font: 700 15px serif; position: relative; }
.seat-plate.is-turn .seat-wind { background: var(--accent-gold); color: var(--bg); }
.seat-plate.is-dealer .seat-wind::after { content: "莊"; position: absolute;
  left: 50%; top: -13px; transform: translateX(-50%);
  font-size: 12px; color: var(--accent-gold); }
.seat-name { color: var(--fg); font-family: ui-monospace, monospace; }
.seat-score { color: var(--fg); font-weight: 700; }
.seat-riichi { color: var(--accent-gold); font-weight: 700; letter-spacing: .06em; }
.seat-furiten { color: var(--down); }
/* Pao badge — flags a seat that fed a pao-triggering meld (HK 4th-meld /
   big-dragons, Riichi yakuman feeds). Muted red on a subtle pill so it
   reads as a warning without screaming. */
.seat-pao { color: var(--pao); font: 700 10px ui-sans-serif; letter-spacing: .08em;
  padding: 1px 5px; border: 1px solid var(--pao); border-radius: 9px;
  background: rgba(226, 106, 106, 0.08); }
/* Beijing (北京麻将) 相公 badge — this seat discarded the 混兒 wildcard and can
   no longer win this hand. Gold pill (matches the wildcard accent), distinct
   from the muted-red pao warning. */
/* 相公 badge — a circular gold marker pushed to the RIGHT edge of the plate so
   it reads clearly as a status flag (the earlier inline pill blended in). */
.seat-barred {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid var(--gold);
  color: var(--gold);
  background: rgba(var(--gold-rgb), 0.14);
  font: 700 9px/1 ui-sans-serif; letter-spacing: 0;
  box-shadow: 0 0 7px rgba(var(--gold-rgb), 0.7);
  cursor: help;
}
/* The 混兒 wildcard tile, once discarded into a pond — gold ring + glow so all
   players see the 相公-triggering discard. */
.seat-pond .lt-tile.beijing-wild-discard {
  outline: 2px solid var(--gold);
  outline-offset: -1px; border-radius: 3px;
  box-shadow: 0 0 6px 1px rgba(var(--gold-rgb), 0.7);
}
/* Iteration 22: per-seat 1000-point riichi stick sits inline in the
   nameplate row. Wider than the hub stick (which is compressed for
   stacking) so the player whose riichi it is reads obviously. The red
   centre dot is a 1000-point stick's defining visual; the inner box-
   shadow already provides a red border, the ::after pseudo adds the
   dot. */
.seat-riichi-stick { position: relative; display: inline-block;
  width: 32px; height: 6px; border-radius: 3px;
  background: #d8d2c4; box-shadow: 0 0 0 1px var(--pao-dot) inset; }
.seat-riichi-stick::after { content: ""; position: absolute;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 5px; height: 5px; border-radius: 50%; background: var(--pao-dot); }

.seat-hand { display: flex; gap: 2px; flex-wrap: nowrap; }
.seat-hand .hand-tile { cursor: default; transition: transform .12s; }
/* Stays on the same baseline as the rest of the hand (no translateY); the
   gold glow alone marks it. Symmetric 14px gap on BOTH sides so the tile
   reads as "set apart" rather than "leaning right". */
.seat-hand .hand-tile.just-drawn { margin-left: 14px; margin-right: 14px;
  filter: drop-shadow(0 0 5px var(--accent-gold)) drop-shadow(0 4px 6px #000a); }
.seat-hand .hand-tile.armed { transform: translateY(-10px);
  filter: drop-shadow(0 4px 6px #000a); }
/* American jokers are wildcards — never discardable. Read as "no": a
   not-allowed cursor and no arm-lift (the click handler also blocks arming). */
.seat-hand .hand-tile--locked { cursor: not-allowed; }
.seat-hand .hand-tile--locked.armed { transform: none; }

/* Fixed 2×2 meld grid — each cell is a reserved "slot" that's wide enough
   for the widest meld (kan = 4 upright + 1 rotated layout box ≈ 4 row-w
   plus ~0.44 row-w spillover for the rotated tile = 4.5 row-w). Slots
   never resize, so adding/removing a meld never shifts the others. Auto-
   flow row: meld 1 → top-left, 2 → top-right, 3 → bottom-left, 4 →
   bottom-right. justify-items:start pins each meld to its slot's left
   edge so smaller melds (pon/chi = 3 tiles) leave the empty space on the
   slot's right rather than drifting. Slot height is exactly one tile-
   line (var(--row-w) * var(--ar)) since each meld is a single row even
   with the rotated tile (transform:rotate keeps the layout box upright). */
/* Melds stack top-down, ONE per line, up to 4 lines (kan-limit caps
   exposed sets at 4). align-self:start (above) pins the first meld to
   the top of the band and further melds grow downward; placed melds
   never move. The band height (grid row 1) reserves 3 tile-lines for
   the max-3-row pond — a rare 4th meld overflows ~1 line into the hand
   row, but a 4-meld hand has only 1 concealed tile so they can't
   collide. align-items:flex-end keeps each meld right-aligned within
   the melds box (the box itself is centred in its half-column). */
.seat-melds { display: flex; flex-direction: column; gap: 4px;
  align-items: flex-end; }
/* Taiwanese can expose 5 melds (5 sets + 1 pair) vs the 4-meld max of every
   other ruleset. Lay them out in TWO columns instead of one tall stack: fill
   the right column top-down (3 sets), then wrap to a second column on the LEFT
   — so all 5 fit the 3-line meld band without spilling into the hand row.
   `direction: rtl` makes the column auto-flow fill the rightmost column first
   and wrap leftward; each meld's own tiles stay left-to-right (override below). */
main.table-page[data-rule-code="taiwanese"] .seat-melds {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(3, auto);
  direction: rtl;
  align-content: start;
  justify-items: end;
  gap: 3px 2px;   /* small row gap; tight column gap so columns sit close */
}
main.table-page[data-rule-code="taiwanese"] .seat-melds > .meld { direction: ltr; }
.seat-melds .meld { display: flex; gap: 1px; align-items: flex-end; }
/* The called tile renders rotated 90° (lt-tile--rot90) to mark which
   copy was acquired from another seat. transform: rotate keeps the
   layout box at upright dimensions, so reserve the post-rotation
   spillover via margin-inline (same idiom as the claim popup rows —
   see Iteration 16). 0.22 is a small cushion above the exact
   (--w * --ar - --w) / 2 ≈ --w * 0.195 spillover. */
.seat-melds .meld .lt-tile--rot90 {
  margin-inline: calc(var(--sm-w) * 0.22);
}

/* Pond fills its top grid-row first (auto-flow row) and grows downward;
   align-self:start (above) pins the top discard line so placed tiles
   never move. The band height is sized so a full 3-line pond's bottom
   line lands right above the hand. Max 3 lines. */
/* 8 cells per row × max 3 rows = up to 24 discards visible. Departs from
   the Riichi-tray convention (6×3) but pairs with the larger --sm-w cap
   to give bigger, easier-to-read pond tiles. */
.seat-pond { display: grid; grid-template-columns: repeat(8, auto);
  gap: 1px; justify-content: center; transform-origin: center center; }
.seat-pond .tsumogiri { opacity: 0.78; }

/* Just-discarded grow-shrink + glow. The tile renders at 2× size with a
   gold drop-shadow, then eases back to its normal layout box over 2s.
   transform-origin: bottom keeps the growth direction "up out of the
   pond" rather than blowing up in place. z-index lifts the animating
   tile above neighbouring pond tiles (and across the table-grid the
   .player-card z-index:5 idiom — lift the child, not the parent — so we
   don't create a stacking context that traps siblings). animation-delay
   is set inline (negative) by renderPond so an intervening render keeps
   the animation continuous instead of restarting at frame 0. */
/* --pop-scale defaults to 2x for self (slot0 — directly under the
   player's eye); opponent ponds (slots 1/2/3 — across the table) get
   a larger 2.75x so the cue still grabs the eye at distance. The var
   is read inside the keyframe so one @keyframes serves all four. */
.seat-pond .lt-tile.just-discarded {
  --pop-scale: 2;
  transform-origin: center bottom;
  position: relative;
  z-index: 5;
  animation: discard-pop 2.4s ease-out forwards;
}
.seat-pond--slot1 .lt-tile.just-discarded,
.seat-pond--slot2 .lt-tile.just-discarded,
.seat-pond--slot3 .lt-tile.just-discarded {
  --pop-scale: 2.75;
}
/* 0..83.3% (= 2s of 2.4s) holds at --pop-scale + gold glow steady;
   the last ~400ms shrinks back to normal and fades the glow out. */
@keyframes discard-pop {
  0%, 83.3% { transform: scale(var(--pop-scale));
              filter: drop-shadow(0 0 8px var(--accent-gold))
                      drop-shadow(0 0 14px rgba(212, 168, 71, 0.6)); }
  100%      { transform: scale(1);
              filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .seat-pond .lt-tile.just-discarded { animation: none; }
}

.game-hub {
  grid-column: 2; grid-row: 2;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 6px;
  color: var(--muted); font: 600 10px ui-sans-serif; text-align: center;
}
.hub-meta { display: flex; gap: 8px; align-items: baseline; }
.hub-round { color: var(--accent-gold); font-size: 13px; font-weight: 700; }
.hub-honba { color: var(--accent); }
.hub-sticks { display: flex; gap: 8px; align-items: center; }
/* The visual wall (.table-wall) is now the primary remaining-tiles cue;
   keep the number as a small caption — useful when the wall is short. */
.hub-wall { color: var(--muted); font: 700 10px ui-monospace;
  letter-spacing: .04em; }
.hub-riichi { display: flex; gap: 3px; }
.riichi-stick { width: 16px; height: 3px; background: #d8d2c4;
  border-radius: 2px; box-shadow: 0 0 0 1px var(--pao-dot) inset; }
.hub-dora { display: flex; gap: 2px; align-items: center; }
.hub-label { color: var(--muted); margin-right: 2px; text-transform: uppercase;
  letter-spacing: .08em; font-size: 9px; }
.hub-fair { color: #6fae84; font-size: 9px; cursor: help; }

/* Bottom action bar removed entirely (per product decision): the bottom
   edge is freed for the player name tag and the reserved space is folded
   back into --board. Discard is still tap-to-arm on a hand tile;
   Riichi/Tsumo/Kan/Abort are intentionally not surfaced. */
.game-actionbar { display: none !important; }

/* In-overlay Exit (top-left so it never collides with the connection pill
   at top-right). Submits the existing #leave-form. */
.game-exit { position: absolute; top: .6rem; left: .75rem; z-index: 2;
  padding: 6px 14px; border-radius: 7px; border: 1px solid var(--border);
  background: #0f0d0bcc; color: var(--muted);
  font: 700 12px ui-sans-serif; cursor: pointer; }
.game-exit:hover { color: var(--fg); border-color: var(--accent); }

/* Game-instance id badge, bottom-right of the in-game board. Mirrors the
   .game-exit corner-badge idiom (position: absolute inside #game-board,
   muted resting state, accent-on-hover). Click-to-copy with a brief gold
   flash on success. */
.game-id-badge { position: absolute; bottom: .6rem; right: .75rem; z-index: 2;
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 5px 11px 6px; border-radius: 7px; border: 1px solid var(--border);
  background: #0f0d0bcc; color: var(--muted);
  font: 600 11px ui-monospace, SFMono-Regular, Menlo, monospace;
  cursor: pointer; user-select: text;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease; }
.game-id-badge:hover { color: var(--fg); border-color: var(--accent); }
.game-id-label { font: 700 9px ui-sans-serif; letter-spacing: .12em;
  color: var(--muted); }
.game-id-value { color: var(--accent); }
.game-id-badge.copied { border-color: var(--accent-gold); color: var(--fg); }
.game-id-badge.copied .game-id-value { color: var(--accent-gold); }
/* Table-page id badge — same chrome as the in-game .game-id-badge, but
   fixed to the viewport corner (the table page isn't a full-screen
   overlay, so position:absolute would scroll away). */
.table-id-badge { position: fixed; }
.game-id-badge.copied::after { content: "copied"; margin-left: 6px;
  font: 700 9px ui-sans-serif; letter-spacing: .12em;
  color: var(--accent-gold); }
.act { padding: 8px 16px; border-radius: 7px; border: 1px solid var(--border);
  background: var(--card-bg); color: var(--accent);
  font: 700 12px ui-sans-serif; cursor: pointer; }
.act:hover { border-color: var(--accent); }
.act-discard { background: var(--accent-gold); color: var(--bg);
  border-color: var(--accent-gold); }
.act-ron { background: #7a2f2f; color: var(--fg); border-color: #a14444; }
.act:disabled { opacity: .45; cursor: not-allowed; }
.act.is-on { background: var(--accent-gold); color: var(--bg);
  border-color: var(--accent-gold); box-shadow: 0 0 0 2px rgba(212,168,71,.4); }
/* Depleting ring (no rotation): the conic arc shrinks as --claim-frac
   (timeLeft / total, set by the countdown ticker) goes 1 -> 0; the inner
   disc carves the ring and seats the seconds number. */
.claim-countdown { display: grid; place-items: center; align-self: center;
  width: 30px; height: 30px; border-radius: 50%; color: var(--accent-gold);
  font: 700 12px ui-monospace;
  background:
    radial-gradient(closest-side, var(--card-bg) 68%, transparent 69%),
    conic-gradient(var(--accent-gold) calc(var(--claim-frac, 1) * 360deg),
                   var(--border) 0); }

/* Shared depleting-ring countdown — same conic mechanism as the claim
   popup ring (56px), reused by the scoring-board auto-advance and the
   table readiness modal so all three countdowns read identically. The
   ticker sets --claim-frac (1 -> 0) and the element's textContent. */
.countdown-ring { display: grid; place-items: center;
  width: 56px; height: 56px; border-radius: 50%; color: var(--accent-gold);
  font: 700 20px ui-monospace; font-variant-numeric: tabular-nums;
  background:
    radial-gradient(closest-side, var(--card-bg) 68%, transparent 69%),
    conic-gradient(var(--accent-gold) calc(var(--claim-frac, 1) * 360deg),
                   var(--border) 0); }

/* ---- claim experience -------------------------------------------------- */
/* Centered, large, NON-blocking: the overlay ignores pointer events so the
   board stays readable/interactive behind it; only the box catches clicks. */
.claim-overlay { position: absolute; inset: 0; display: grid;
  place-items: center; pointer-events: none; z-index: 905; }
.claim-box { pointer-events: auto; display: flex; flex-direction: column;
  align-items: center; gap: 14px; padding: 22px 30px; min-width: 280px;
  background: var(--card-bg); border: 2px solid var(--accent-gold);
  border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.55);
  animation: claim-pop .22s cubic-bezier(.34,1.56,.64,1) both; }
@keyframes claim-pop { from { opacity: 0; transform: scale(.82) }
  to { opacity: 1; transform: scale(1) } }
.claim-box .claim-countdown { width: 56px; height: 56px;
  font: 700 20px ui-monospace; }
/* Compact depleting ring on the active seat plate. Standalone (NOT sharing
   the .claim-countdown class) so the claim-popup countdown's
   `.claim-box .claim-countdown` selector can't collide with it. Same
   conic-depletion mechanism; inner disc tuned to the plate's dark bg. */
.turn-countdown { display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: 50%; color: var(--accent-gold);
  font: 700 10px ui-monospace;
  background:
    radial-gradient(closest-side, #16130f 66%, transparent 67%),
    conic-gradient(var(--accent-gold) calc(var(--claim-frac, 1) * 360deg),
                   var(--border) 0); }
.claim-waiting { color: var(--accent-gold); font: 700 15px ui-sans-serif;
  letter-spacing: .06em; }
.claim-opts { display: flex; flex-direction: column; gap: 10px;
  align-items: stretch; min-width: 320px; }
.claim-box .act { padding: 12px 22px; font-size: 15px; }
.claim-box .act.is-chosen { background: var(--accent-gold); color: var(--bg);
  border-color: var(--accent-gold);
  box-shadow: 0 0 0 3px rgba(212,168,71,.45); opacity: 1; }
/* One row per option: label on the left, tile sequence on the right. The
   button itself is the click target so screen-reader/keyboard semantics are
   unchanged from the old text-only variant. */
.claim-box .claim-option { display: flex; align-items: center;
  justify-content: flex-start; gap: 12px; padding: 8px 14px; }
.claim-option-label { min-width: 64px; font-weight: 700; letter-spacing: .04em;
  text-align: left; font-size: 16px; }
.claim-option-tiles { display: flex; align-items: center; gap: 6px;
  margin-left: auto; }
/* The called tile is rotated via the standard --rot90 class. transform:rotate
   keeps the layout box at the tile's upright dimensions, so a rotated tile's
   painted face spills horizontally past its slot by ((--md-w * --ar) - --md-w)
   / 2 on each side; reserve that as margin so it doesn't overlap neighbours. */
.claim-option-tiles .lt-tile--rot90 {
  margin: 0 calc(var(--md-w) * 0.22); }
.claim-box .claim-pass { margin-top: 4px; align-self: center; }
/* #55 keyboard play — focus ring on the keyboard-selected claim option
   (←/→ move it, Enter/Space accepts). The .armed class already highlights
   the keyboard-selected hand tile in 2D, so no extra rule is needed there. */
.claim-box .kbd-focus { outline: 2px solid var(--accent-gold); outline-offset: 2px; }

/* ---- American Charleston panel (Part B) -------------------------------- */
/* Same centered non-blocking idiom as the claim panel, but it renders the
   player's own hand as selectable tiles INSIDE the box (works in 2D + 3D). */
.charleston-overlay { position: absolute; inset: 0; display: grid;
  place-items: center; pointer-events: none; z-index: 905; }
.charleston-box { pointer-events: auto; display: flex; flex-direction: column;
  align-items: center; gap: 12px; padding: 20px 26px;
  max-width: min(92vw, 560px);
  background: var(--card-bg); border: 2px solid var(--accent-gold);
  border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.55);
  animation: claim-pop .22s cubic-bezier(.34,1.56,.64,1) both; }
.charleston-title { color: var(--accent-gold); font: 800 17px ui-sans-serif;
  letter-spacing: .1em; text-transform: uppercase; }
.charleston-prompt { color: var(--fg); font: 700 15px ui-sans-serif;
  letter-spacing: .03em; text-align: center; }
.charleston-hint { color: var(--muted); font: 500 12px ui-sans-serif;
  margin-top: -6px; }
.charleston-waiting { color: var(--accent-gold); font: 700 15px ui-sans-serif;
  letter-spacing: .06em; }
.charleston-box .charleston-countdown { width: 52px; height: 52px;
  font: 700 18px ui-monospace; }
.charleston-hand { display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px; max-width: 520px; }
/* Each hand tile is its own button so selection has clean keyboard/SR
   semantics; the tile SVG sits inside. */
.charleston-tile { padding: 2px; background: transparent;
  border: 2px solid transparent; border-radius: 8px; cursor: pointer;
  line-height: 0;
  transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease; }
.charleston-tile:hover:not(:disabled) { transform: translateY(-3px); }
.charleston-tile.is-selected { border-color: var(--accent-gold);
  transform: translateY(-6px);
  box-shadow: 0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(234,197,88,.45); }
.charleston-tile.is-locked, .charleston-tile:disabled { opacity: .42;
  cursor: not-allowed; filter: grayscale(.4); }
/* No-pass-back hint: the just-received tiles the player should avoid passing
   back on a non-across pass. Dimmed + a subtle warning tint — informational,
   not interactive. */
.charleston-nopassback { display: flex; align-items: center; gap: 8px;
  padding: 4px 10px; border-radius: 8px;
  background: rgba(176, 48, 48, .12); border: 1px solid rgba(176, 48, 48, .35); }
.charleston-nopassback-label { color: var(--muted); font: 600 11px ui-sans-serif;
  letter-spacing: .02em; }
.charleston-nopassback-tiles { display: flex; gap: 4px; opacity: .85; }
.charleston-received { display: flex; gap: 8px; }
.charleston-vote-btns { display: flex; gap: 14px; }
.charleston-box .charleston-submit { margin-top: 4px; }

/* ---- Sichuan 定缺 (void-suit) choice panel (B1) ------------------------- */
/* Same centered, non-blocking overlay idiom as the Charleston panel. */
.voidsuit-overlay { position: absolute; inset: 0; display: grid;
  place-items: center; pointer-events: none; z-index: 905; }
.voidsuit-box { pointer-events: auto; display: flex; flex-direction: column;
  align-items: center; gap: 12px; padding: 20px 26px;
  max-width: min(92vw, 560px);
  background: var(--card-bg); border: 2px solid var(--accent-gold);
  border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.55);
  animation: claim-pop .22s cubic-bezier(.34,1.56,.64,1) both; }
.voidsuit-title { color: var(--accent-gold); font: 800 17px ui-sans-serif;
  letter-spacing: .1em; text-transform: uppercase; }
.voidsuit-prompt { color: var(--fg); font: 700 15px ui-sans-serif;
  letter-spacing: .03em; text-align: center; }
.voidsuit-waiting { color: var(--accent-gold); font: 700 15px ui-sans-serif;
  letter-spacing: .06em; }
.voidsuit-box .voidsuit-countdown { width: 52px; height: 52px;
  font: 700 18px ui-monospace; }
.voidsuit-hand { display: flex; flex-wrap: wrap; justify-content: center;
  gap: 5px; max-width: 520px; opacity: .9; }
.voidsuit-choices { display: flex; gap: 16px; margin-top: 2px; }
/* One big button per suit: a representative tile over a translated label. */
.voidsuit-choice { display: flex; flex-direction: column; align-items: center;
  gap: 6px; padding: 10px 14px; background: var(--bg);
  border: 2px solid var(--border); border-radius: 12px; cursor: pointer;
  transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease; }
.voidsuit-choice:hover { transform: translateY(-4px); border-color: var(--accent-gold);
  box-shadow: 0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(234,197,88,.4); }
/* The read-only hand row + the tile SVG/label inside each suit button must not
   intercept clicks — only the .voidsuit-choice button itself is the hit target
   (matters in 3D where #game-board is pointer-events:none; SVG <image> children
   otherwise swallow the hit-test). */
.voidsuit-hand, .voidsuit-hand *,
.voidsuit-choice * { pointer-events: none; }
.voidsuit-choice-label { color: var(--muted); font: 700 12px ui-sans-serif;
  letter-spacing: .04em; }
/* Per-seat 缺 marker on the nameplate (public once play starts). */
.seat-void-marker { display: inline-block; margin-left: 6px; padding: 0 5px;
  border-radius: 5px; background: rgba(176, 48, 48, .18);
  border: 1px solid rgba(176, 48, 48, .5); color: #e8a0a0;
  font: 700 12px ui-sans-serif; letter-spacing: .04em; vertical-align: middle; }

/* Sichuan 血战到底 — a 和 marker on a seat that has already won and dropped
   out while the hand continues. 2D parity with the 3D spinning-和 indicator;
   a gentle gold pulse so it reads as "still standing as a winner". */
.seat-won-marker { display: inline-block; margin-left: 6px; padding: 0 6px;
  border-radius: 5px; background: rgba(var(--gold-rgb), .2);
  border: 1px solid rgba(var(--gold-rgb), .65); color: var(--accent-gold);
  font: 800 13px serif; vertical-align: middle;
  animation: seat-won-pulse 1.4s ease-in-out infinite; }
@keyframes seat-won-pulse { 0%, 100% { opacity: .7 } 50% { opacity: 1 } }

/* ---- self-turn action panel (Iteration 21) ----------------------------- */
/* Engine-authoritative legal-options panel for the active seat. Mirrors the
   claim-window visual idiom (centered, non-blocking, large pop-in spring)
   because the rendered shape — labelled rows with optional tile previews —
   is identical. They're mutually exclusive at render time: claim window
   only opens to non-discarder seats, so the active seat (the only one that
   sees turn-options) is never simultaneously in a claim. */
.turn-options-overlay { position: absolute; inset: 0; display: grid;
  place-items: center; pointer-events: none; z-index: 905; }
.turn-options-box { pointer-events: auto; display: flex; flex-direction: column;
  align-items: center; gap: 14px; padding: 22px 30px; min-width: 280px;
  background: var(--card-bg); border: 2px solid var(--accent-gold);
  border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.55);
  animation: claim-pop .22s cubic-bezier(.34,1.56,.64,1) both; }
.turn-options-opts { display: flex; flex-direction: column; gap: 10px;
  align-items: stretch; min-width: 320px; }
.turn-options-box .act { padding: 12px 22px; font-size: 15px; }
/* Iteration 21.1: scale the in-panel countdown ring to match the claim
   panel's size (the seat-plate ring stays at its compact 22px size for
   observers via the default .turn-countdown rule). Both rings share the
   same .turn-countdown class and tick in sync. */
.turn-options-box .turn-countdown { width: 56px; height: 56px;
  font: 700 20px ui-monospace; }
/* Iteration 21.1: Skip button — visually distinct from the action rows
   (no tile preview, centered, slightly lower-key palette). Mirrors the
   claim panel's Pass button. */
.turn-options-box .turn-option-skip { margin-top: 4px; align-self: center;
  background: var(--card-bg); color: var(--muted);
  border-color: var(--border); }
.turn-options-box .turn-option-skip:hover { color: var(--fg);
  border-color: var(--accent); }
.turn-options-box .turn-option { display: flex; align-items: center;
  justify-content: flex-start; gap: 12px; padding: 8px 14px; }
.turn-option-label { min-width: 110px; font-weight: 700; letter-spacing: .04em;
  text-align: left; font-size: 16px; }
.turn-option-tiles { display: flex; align-items: center; gap: 6px;
  margin-left: auto; }
/* Same rotated-tile spillover reservation as .claim-option-tiles. */
.turn-option-tiles .lt-tile--rot90 {
  margin: 0 calc(var(--md-w) * 0.22); }

/* Spoken-call shout: big glyph over the calling player's seat. */
/* z-index: 1 lifts the whole seat block above .table-wall (auto, painted in
   DOM order which would otherwise win since the wall is appended to
   .board-grid AFTER the seat-zones). Without this, an opponent's 2.75x
   .just-discarded pop extends INTO the wall's edge region and gets
   overpainted by .wall-pair tiles (visible on the top seat first since its
   wall-edge sits closest to the popped tile). Stays below .game-hub
   (z-index 2) so the central hub label/dora always wins.
   The transform: rotate(...) already creates a stacking context per seat
   so .just-discarded (z-index 5) and .call-shout (z-index 6) remain local
   to the seat — no trapping regression. */
.seat-zone { position: relative; z-index: 1; }
.call-shout { position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); z-index: 6;
  padding: 6px 18px; border-radius: 10px; pointer-events: none;
  font: 800 30px ui-sans-serif; letter-spacing: .04em;
  color: var(--bg); background: var(--accent-gold);
  box-shadow: 0 4px 18px rgba(0,0,0,.5);
  animation: call-shout-in .3s cubic-bezier(.34,1.56,.64,1) both; }
.call-shout.call-ron { background: #a14444; color: #fff; }
/* 抢杠 (rob-the-kong) — a robbed-kong win; deep red like ron, gold ring. */
.call-shout.call-chankan { background: #8a2f2f; color: #fff;
  border: 2px solid var(--accent-gold); }
@keyframes call-shout-in { 0% { opacity: 0; transform: translate(-50%,-50%) scale(.4) }
  70% { transform: translate(-50%,-50%) scale(1.12) }
  100% { opacity: 1; transform: translate(-50%,-50%) scale(1) } }

/* Mid-game disconnect overlay (Iteration 23).
   Painted over a seat-zone whose reducer flags it paused or autoplayed.
   .seat-zone already declares position:relative above, so absolute+inset:0
   pins the overlay to that seat. pointer-events:none keeps the overlay
   click-through so it never steals events from the seat below. */
.seat-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    background: rgba(0, 0, 0, 0.6);
    color: #f0e4c3;
    font-size: 0.9rem;
    pointer-events: none;
    z-index: 5;
    text-align: center;
}
.seat-overlay--paused        { color: #f0e4c3; }
.seat-overlay--autoplayed    { color: #c3d8f0; font-style: italic; }
.seat-pause-deadline         { font-size: 0.75rem; opacity: 0.8; margin-top: 4px; }
.seat-overlay-label          { font-weight: 600; }

/* Learner-gated OVERRIDDEN decline notice. */
.claim-decline-overlay { position: absolute; inset: 0; display: grid;
  place-items: center; pointer-events: none; z-index: 906; }
.claim-decline { pointer-events: auto; display: flex; flex-direction: column;
  align-items: center; gap: 12px; padding: 20px 26px; max-width: 340px;
  text-align: center; background: var(--card-bg);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0,0,0,.55);
  animation: claim-pop .22s cubic-bezier(.34,1.56,.64,1) both; }
.claim-decline-text { color: var(--muted); font: 600 13px ui-sans-serif;
  line-height: 1.5; }
.claim-decline-ok { padding: 8px 20px; }

.game-ceremony { position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(10,8,6,0.55); animation: ceremony-in .25s ease both; }
@keyframes ceremony-in { from { opacity: 0 } to { opacity: 1 } }
.ceremony-card { display: flex; flex-direction: column; align-items: center;
  gap: 10px; color: var(--fg); text-align: center; }
.ceremony-title { font: 700 18px ui-sans-serif; color: var(--accent-gold);
  letter-spacing: .04em; }
.ceremony-sub { font: 600 11px ui-sans-serif; color: var(--muted); }
.ceremony-wall { display: flex; gap: 4px; }
.wall-stack { width: calc(10px * var(--tile-scale) * var(--wall-scale));
  height: calc(26px * var(--tile-scale) * var(--wall-scale));
  border-radius: 2px;
  background: linear-gradient(#2c6b4f, #1f4d39); border: 1px solid #143427;
  animation: wall-rise .4s ease both; }
@keyframes wall-rise { from { transform: scaleY(0); opacity: 0 }
  to { transform: scaleY(1); opacity: 1 } }
/* Live perimeter wall — a square frame sharing the center grid cell with
   the hub (hub raised above via z-index). Four edges of double-high stacks
   that deplete as wallRemaining drops; last ~14 tiles tinted as dead wall. */
.table-wall { grid-column: 2; grid-row: 2; position: relative;
  align-self: stretch; justify-self: stretch; pointer-events: none; }
.game-hub { position: relative; z-index: 2; }
.wall-edge { position: absolute; display: flex; gap: 2px; }
/* Flex direction per edge encodes the wall's (edge, stack) → screen
   position mapping. Stack 0 of each edge sits at the edge's local -X
   end (matches the 3D buildWall); after each edge is rotated to its
   world position, "stack 0" lands at a specific screen corner:
     Edge 0 (south): stack 0 at WEST end  (screen LEFT)  → row
     Edge 1 (east):  stack 0 at NORTH end (screen TOP)   → column
     Edge 2 (north): stack 0 at EAST end  (screen RIGHT) → row-reverse
     Edge 3 (west):  stack 0 at SOUTH end (screen BOTTOM)→ column-reverse
   Drawing direction (LEFT from each player's POV) is encoded by
   edgeLeftDir / edgeRightEnd in game-render.js — the JS computes the
   `placed` ring index for each stack; CSS just places the wall-pair
   children at the right physical position. */
.wall-edge--bottom { left: 50%; bottom: 3%; transform: translateX(-50%);
  flex-direction: row; }
.wall-edge--right  { top: 50%; right: 3%; transform: translateY(-50%);
  flex-direction: column; }
.wall-edge--top    { left: 50%; top: 3%; transform: translateX(-50%);
  flex-direction: row-reverse; }
.wall-edge--left   { top: 50%; left: 3%; transform: translateY(-50%);
  flex-direction: column-reverse; }
/* Wall-pair sizes were tuned for the depleting model (max ~10 stacks
   per edge). The fixed-68-stack physical model packs 17 per edge, so
   the upper clamp bound is reduced (14→11 thin, 26→20 cross) and the
   vmin midpoint dropped (1.1→0.85 thin, 2.0→1.5 cross). Aspect ratio
   preserved at ~1.8 cross/thin. */
.wall-pair { width: clamp(calc(6px * var(--tile-scale) * var(--wall-scale)),
    calc(0.85vmin * var(--tile-scale) * var(--wall-scale)),
    calc(11px * var(--tile-scale) * var(--wall-scale)));
  height: clamp(calc(10px * var(--tile-scale) * var(--wall-scale)),
    calc(1.5vmin * var(--tile-scale) * var(--wall-scale)),
    calc(20px * var(--tile-scale) * var(--wall-scale)));
  border-radius: 2px;
  background: linear-gradient(#2c6b4f, #1f4d39); border: 1px solid #143427;
  box-shadow: inset 0 -3px 0 rgba(0,0,0,0.28); }
.wall-edge--left .wall-pair, .wall-edge--right .wall-pair {
  width: clamp(calc(10px * var(--tile-scale) * var(--wall-scale)),
    calc(1.5vmin * var(--tile-scale) * var(--wall-scale)),
    calc(20px * var(--tile-scale) * var(--wall-scale)));
  height: clamp(calc(6px * var(--tile-scale) * var(--wall-scale)),
    calc(0.85vmin * var(--tile-scale) * var(--wall-scale)),
    calc(11px * var(--tile-scale) * var(--wall-scale)));
  box-shadow: inset -3px 0 0 rgba(0,0,0,0.28); }
.wall-pair.wall-dead { background: linear-gradient(#5a4a2c, #3d3320);
  border-color: #2a2113; }
.wall-pair.wall-break { box-shadow: 0 0 0 2px var(--accent-gold); }
/* Iteration 20.x: tiles in a built wall don't move when consumed — the
   stack vacates its slot but the perimeter layout stays put. The break-
   point ring keeps a dashed-outline marker after vacating so the
   ORIGINAL break is still visible as a reference. */
.wall-pair.wall-consumed { visibility: hidden; }
.wall-pair.wall-consumed.wall-break {
  visibility: visible;
  background: transparent;
  border-style: dashed;
  border-color: rgba(212,168,71,0.55);
  box-shadow: 0 0 0 2px rgba(212,168,71,0.55); }
/* Half-consumed: only the top tile of the stack has been drawn; the
   bottom tile remains. We keep the slot at full dimensions (no
   transform — earlier "half size" reads as a weird truncated tile) and
   instead darken the fill + add an all-around inset shadow so the
   slot reads as a recessed "hole" where the top tile used to be.
   Same look on every edge, so no per-edge overrides needed. */
.wall-pair.wall-half-consumed {
  background: linear-gradient(#0e2a1f, #061309);
  border-color: #051712;
  box-shadow: inset 0 0 6px 2px rgba(0,0,0,0.65),
              inset 0 -1px 0 rgba(255,255,255,0.04); }
/* Dora indicator(s) face-up on the dead wall. Inline in the wall-edge
   flex (row for top/bottom edges, column for left/right) so it sits on
   the wall for whichever edge holds the dead wall. Sized to the wall
   scale, ~1.6x a wall-pair so the face is legible, gold-ringed. */
.wall-dora { display: flex; gap: 1px; padding: 1px;
  background: #1f4d39; border: 1px solid var(--accent-gold);
  border-radius: 2px; box-shadow: 0 0 0 1px rgba(212,168,71,.35); }
/* Match the wall-pair's CROSS-edge thickness (its long axis on each
   edge: 12..26 × scales) so the dora face reads as full-thickness on the
   wall rather than a small tile floating on top. .lt-tile keeps its
   36:50 face aspect-ratio, so we drive the cross-edge size by setting
   width = wall-pair-thickness × 36/50 (≈ 8.64..18.72) — the derived
   height then equals 12..26, matching the wall-pair thickness exactly.
   The face is slightly wider along the edge than one wall-pair (by the
   36:50 ratio), which sits cleanly in the wall-edge flex row. */
.wall-dora .lt-tile { width: clamp(
    calc(10px * 36 / 50 * var(--tile-scale) * var(--wall-scale)),
    calc(1.5vmin * 36 / 50 * var(--tile-scale) * var(--wall-scale)),
    calc(20px * 36 / 50 * var(--tile-scale) * var(--wall-scale))); }
/* Bug 2: face the player who owns that edge — same rotation as that
   seat-zone (bottom 0, top 180, left +90, right -90); left/right run
   as a column down their edge like the wall-pairs there. */
.wall-dora--top, .wall-dora--bottom { flex-direction: row; }
.wall-dora--left, .wall-dora--right { flex-direction: column; }
.wall-dora--top .lt-tile   { transform: rotate(180deg); }
.wall-dora--left .lt-tile  { transform: rotate(90deg); }
.wall-dora--right .lt-tile { transform: rotate(-90deg); }

.ceremony-dice { display: flex; gap: 8px; font-size: 34px; color: #f4eede; }
.ceremony-dice .die.die--rolling {
  /* Iteration 20: spin for 1.6s, then settle on the rolled face for the
     remaining ~400ms with a small bounce. fill-mode: both holds the final
     transform so the die doesn't snap back. */
  animation: dice-tumble-land 2s cubic-bezier(.18,.7,.21,1.01) both; }
@keyframes dice-tumble-land {
  0%   { transform: rotate(-90deg) scale(.55); }
  35%  { transform: rotate(540deg) scale(1.12); }
  70%  { transform: rotate(870deg) scale(.95); }
  85%  { transform: rotate(900deg) scale(1.18); }
  100% { transform: rotate(900deg) scale(1); } }
.deal-spinner { width: 26px; height: 26px; border-radius: 50%;
  border: 3px solid var(--border); border-top-color: var(--accent-gold);
  display: inline-block; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg) } }
.ceremony-fair { color: #6fae84; font: 600 10px ui-monospace; cursor: help; }

/* ============================================================================
 * Iteration 20 — game-start ceremony animations
 * ----------------------------------------------------------------------------
 * Each opening phase paces server-side (mahjong-engine MahjongGameImpl
 * SHUFFLE/BUILD/ROLL/DEAL_STEP/DORA dwell constants). The web renders:
 *   - shuffling: swarming tile-back mesh
 *   - building : per-stack rise (existing wall-rise) staggered 30ms
 *   - rolling  : dice tumble-land (above)
 *   - dealing  : per-step .deal-arriving fade+slide on freshly-appended tiles
 *   - dora flip: .dora-flipping rotateY on the wall-dora child
 *   - east cue : .east-nudge translateX on the dealer's rightmost hand tile
 * ========================================================================= */

/* ShufflingWall mesh — a 5x6-ish grid of tile-back sprites swarming around
   their own coordinates via @keyframes mesh-swap. Per-sprite animation-delay
   (set inline in JS) randomises the phase so they don't lockstep. */
.ceremony-mesh {
  display: grid; grid-template-columns: repeat(7, auto);
  gap: 4px; padding: 4px; }
.mesh-tile {
  width: calc(14px * var(--tile-scale) * var(--wall-scale));
  height: calc(20px * var(--tile-scale) * var(--wall-scale));
  border-radius: 2px;
  background: linear-gradient(#2c6b4f, #1f4d39);
  border: 1px solid #143427;
  box-shadow: inset 0 -3px 0 rgba(0,0,0,0.28);
  animation: mesh-swap 1.0s ease-in-out infinite alternate; }
@keyframes mesh-swap {
  0%   { transform: translate(0, 0)   rotate(0deg);   opacity: .85; }
  35%  { transform: translate(-7px, 6px) rotate(-12deg); opacity: 1; }
  70%  { transform: translate(8px, -4px) rotate(11deg);  opacity: 1; }
  100% { transform: translate(0, 0)   rotate(0deg);   opacity: .85; } }

/* Per-step deal — applied by renderHand to the freshly-appended tile(s).
   Slides in from a small offset toward the seat-edge ("toward the player"
   in each rotated seat-zone — the rotation propagates through transform).
   Negative animation-delay set inline keeps the visual continuous across
   re-renders that happen during the 220ms envelope (Iteration 17 idiom). */
.hand-tile.deal-arriving {
  animation: deal-arrive 220ms cubic-bezier(.2,.7,.22,1) both; }
@keyframes deal-arrive {
  from { transform: translateY(-28px) scale(.78); opacity: 0; }
  to   { transform: translateY(0)     scale(1);   opacity: 1; } }
/* Group separator — first tile of each non-first deal-step block
   (block-of-4, final single, dealer jump) carries a small left gap during
   phase="dealing" so the hand reads as discrete groups instead of one
   long row. Applied by renderHand via state.dealGroupStartsBySeat on
   EVERY seat (faced for self, backs for opponents). Opponent backs are
   bare .lt-tile (no .hand-tile class — that's reserved for clickable
   self-hand tiles in game-actions); the selector below covers both.
   Removed automatically when the hand sorts at phase→play. */
.seat-hand > .lt-tile.hand-tile--group-start { margin-left: 10px; }

/* Issue #2 — hand sort animation. Stamped at the dealing→play handoff
   (state.handSortStartedAt). Each tile sets --sort-dx inline = the X
   delta from its deal-order position (with group gaps, computed against
   var(--hand-w) so each zone's tile width is correct) to its sorted-
   order position. CSS lerps from translateX(var(--sort-dx)) → 0 in
   600ms with smoothstep ease. Negative animation-delay keeps the visual
   continuous across re-renders inside the window. */
@keyframes hand-sort {
  0%   { transform: translateX(var(--sort-dx, 0)); }
  100% { transform: translateX(0); }
}
.seat-hand > .lt-tile.hand-sort {
  animation: hand-sort 600ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .seat-hand > .lt-tile.hand-sort { animation: none; }
}

/* Dealer jump (the 14th tile): arc up-over-and-down instead of straight in.
   Reads as the real-world "hop" the dealer makes to grab the extra tile. */
.hand-tile.deal-arriving--jump {
  animation: deal-arrive-jump 320ms cubic-bezier(.3,.7,.4,1.0) both; }
@keyframes deal-arrive-jump {
  0%   { transform: translate(-40px,-18px) rotate(-12deg); opacity: 0; }
  55%  { transform: translate(-10px,-28px) rotate(-6deg);  opacity: 1; }
  100% { transform: translate(0,0)         rotate(0deg);   opacity: 1; } }

/* Dora-flip — rotateY across 800ms, hold for the remainder of the 1.2s
   reveal window so the player has time to read the tile. Backface keeps the
   pre-flip face hidden. */
.wall-dora .lt-tile.dora-flipping {
  animation: dora-flip 1200ms cubic-bezier(.3,.6,.3,1) both;
  transform-style: preserve-3d; backface-visibility: hidden; }
@keyframes dora-flip {
  0%   { transform: rotateY(180deg) scale(.92); }
  40%  { transform: rotateY(0deg)   scale(1.08); }
  60%  { transform: rotateY(0deg)   scale(1.08); filter: drop-shadow(0 0 8px var(--accent-gold)); }
  100% { transform: rotateY(0deg)   scale(1);    filter: none; } }
/* Dora-flip inside a rotated seat-zone: the wall-dora--top/left/right
   parent already rotates the .lt-tile; compose by adding rotateY to the
   existing rotate. */
.wall-dora--top .lt-tile.dora-flipping {
  animation-name: dora-flip-top; }
@keyframes dora-flip-top {
  0%   { transform: rotate(180deg) rotateY(180deg); }
  100% { transform: rotate(180deg) rotateY(0deg); } }
.wall-dora--left .lt-tile.dora-flipping {
  animation-name: dora-flip-left; }
@keyframes dora-flip-left {
  0%   { transform: rotate(90deg) rotateY(180deg); }
  100% { transform: rotate(90deg) rotateY(0deg); } }
.wall-dora--right .lt-tile.dora-flipping {
  animation-name: dora-flip-right; }
@keyframes dora-flip-right {
  0%   { transform: rotate(-90deg) rotateY(180deg); }
  100% { transform: rotate(-90deg) rotateY(0deg); } }

/* East nudge — once the dealing is done and the dealer's opening turn is
   unspent, push their rightmost concealed tile 6px aside (Iteration 20
   item 4). Persists for the full opening turn (renderHand re-applies the
   class on every re-render until they draw/discard); the small ease-in
   gives it a felt "settle" instead of a snap. */
.hand-tile.east-nudge {
  animation: east-nudge 200ms ease-out both; }
@keyframes east-nudge {
  from { transform: translateX(0)   }
  to   { transform: translateX(8px) } }

@media (prefers-reduced-motion: reduce) {
  .mesh-tile,
  .ceremony-dice .die.die--rolling,
  .hand-tile.deal-arriving,
  .hand-tile.deal-arriving--jump,
  .wall-dora .lt-tile.dora-flipping,
  .hand-tile.east-nudge { animation: none; }
  .hand-tile.east-nudge { transform: translateX(6px); }
}

/* z-index 950 lifts the whole result overlay above every in-board layer:
   .seat-zone (1), .game-hub (2), .player-card / .just-discarded (5),
   .call-shout (6) and the claim/turn-options panels (905/906) — the
   result card is a modal and must cover all of them. Stays below the
   fixed connection pill (1000), which lives outside the board context.
   Without this the overlay paints at z-index auto and the Iteration-23
   seat-zone/hub z-indexes paint straight over it. */
.game-result { position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(10,8,6,0.72); z-index: 950; }
/* Iter 30: the result card is much wider so the per-set hand grouping
   and the item-by-item score breakdown can sit side-by-side without
   either pane wrapping. min-width = comfortable single-pane bound on
   narrow viewports; max-width caps the layout on ultrawide screens. */
.result-card { background: var(--card-bg); border: 1px solid var(--accent-gold);
  border-radius: 12px; padding: 26px 32px; text-align: center;
  display: flex; flex-direction: column; gap: 12px;
  min-width: min(720px, 94vw);
  max-width: min(1100px, 96vw);
  max-height: 92vh; overflow: auto;
  position: relative; }
.result-header { display: flex; flex-direction: column; gap: 4px; }
.result-title { color: var(--accent-gold); font: 700 22px ui-sans-serif; }
.result-sub { color: var(--accent); font-size: 12px; }

/* Sichuan 血战到底/血流成河 per-seat draw-settlement status on the 黄庄 card.
   花猪 (flowery-pig) is the headline — red, since it pays the cap to everyone. */
.result-draw-status { display: flex; flex-direction: column; gap: 4px; }
.draw-status-row { display: flex; align-items: center; gap: 8px;
  justify-content: space-between; }
.draw-status-name { color: var(--fg); font-size: 12px; }
.draw-status-badge { padding: 1px 8px; border-radius: 999px;
  font: 700 11px ui-sans-serif; letter-spacing: .03em; white-space: nowrap; }
.draw-status-badge.status-flowerpig { background: rgba(176, 48, 48, .22);
  border: 1px solid rgba(176, 48, 48, .6); color: #f0a8a8; }
.draw-status-badge.status-tenpai { background: rgba(var(--gold-rgb), .18);
  border: 1px solid rgba(var(--gold-rgb), .55); color: var(--accent-gold); }
.draw-status-badge.status-noten { background: rgba(255, 255, 255, .07);
  border: 1px solid rgba(255, 255, 255, .22); color: var(--muted); }
.result-rules { color: var(--muted); font-size: 11px; letter-spacing: 0.02em;
  margin: -2px 0 8px 0; text-align: center; opacity: 0.85; }
.result-body { display: grid; grid-template-columns: 1fr 1fr; gap: 28px;
  align-items: start; text-align: left; }
.result-hand-pane { display: flex; flex-direction: column; gap: 10px;
  align-items: center; }
.result-score-pane { display: flex; flex-direction: column; gap: 14px; }
@media (max-width: 760px) {
  .result-body { grid-template-columns: 1fr; }
}
/* Sichuan 血战到底 multi-winner panels (B2) — full-width stacked rows, one per
   winner (name + WON badge + net delta), since a hand can have 2–3 winners. */
.result-winner-panel { grid-column: 1 / -1; display: flex; flex-direction: column;
  gap: 8px; padding: 10px 12px; border-radius: 8px;
  background: rgba(234, 197, 88, 0.06); border: 1px solid var(--border); }
.result-winner-head { display: flex; align-items: center; gap: 10px; }
.result-winner-name { color: var(--fg); font: 700 15px ui-sans-serif; flex: 1; }
.result-winner-badge { color: var(--accent-gold); font: 800 11px ui-sans-serif;
  letter-spacing: 0.08em; padding: 1px 7px; border-radius: 4px;
  border: 1px solid var(--accent-gold); }
.result-winner-delta { font: 700 15px ui-monospace; }
.result-winner-delta.up { color: #6fae84; }
.result-winner-delta.down { color: var(--down); }
/* Per-winner win source (Ron · off <name>, or Tsumo · self-draw). */
.result-winner-mode { color: var(--muted); font: 600 12px ui-sans-serif;
  letter-spacing: 0.01em; }
/* #46 — winners section on a Sichuan draw card (a 血战到底 wall-exhaust that
   still had 1-2 earlier winners). Spans the full result-body grid, above the
   花猪/听/未听 settlement badges; the winner panels stack full-width inside it. */
.result-draw-winners { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 8px; }
.result-draw-winners-title { color: var(--accent-gold); font: 800 12px ui-sans-serif;
  letter-spacing: 0.08em; text-transform: uppercase; }
.result-draw-winners .result-winner-panel { grid-column: auto; }
/* Inter-set separator: large gap between sets, small gap within a set. */
.result-hand-grouped { display: flex; flex-wrap: wrap; gap: 14px;
  justify-content: center; align-items: flex-end; }
.result-hand-set { display: flex; gap: 3px; padding: 4px 6px;
  border-radius: 6px; }
.result-hand-set.wait { background: rgba(212, 168, 71, 0.12);
  outline: 1px solid rgba(212, 168, 71, 0.4); }
/* Win-source cue on the winning set (issue #35): gold = ron (default .wait),
   jade-green = self-draw (tsumo). Visible in both 2D and 3D (set container). */
.result-hand-set.wait--tsumo { background: rgba(63, 174, 143, 0.14);
  outline-color: rgba(63, 174, 143, 0.55); }
.result-hand-set .winning-tile {
  filter: drop-shadow(0 0 4px var(--accent-gold)); }
.result-melds, .result-dora, .result-flowers { display: flex; gap: 2px;
  align-items: center; justify-content: center; flex-wrap: wrap; }
/* Revealed melds grouped per meld (issue #35): each .meld is a tight block,
   with clear gaps between melds so chi/pon/kan/shouminkan read as distinct. */
.result-melds { opacity: .85; gap: 10px; align-items: flex-end; }
.result-melds .meld { display: flex; gap: 1px; align-items: flex-end; }
.result-flowers { opacity: .9; }
/* 混兒 (Beijing wild) — jade ring on wild-face tiles in the grouped hand and on
   the card's wild indicator, distinct from the gold winning-tile glow. */
.result-hand-set .beijing-wild,
.result-wild .beijing-wild {
  outline: 2px solid #3fae8f; outline-offset: -1px; border-radius: 3px;
  box-shadow: 0 0 5px 1px rgba(63, 174, 143, 0.6); }
/* Per-yaku / per-fu rows. Three-column grid: label / anchor tiles / value. */
.result-breakdown { display: flex; flex-direction: column; gap: 12px; }
.breakdown-section { display: flex; flex-direction: column; gap: 3px; }
.breakdown-section-title { color: var(--accent); font: 600 11px ui-sans-serif;
  letter-spacing: 0.06em; text-transform: uppercase; }
.breakdown-row { display: grid;
  grid-template-columns: minmax(90px, max-content) 1fr auto;
  gap: 10px; align-items: center; padding: 3px 6px;
  background: #0f0d0b60; border-radius: 4px;
  font: 500 13px ui-sans-serif; }
.breakdown-label { color: var(--fg); }
.breakdown-tiles { display: flex; gap: 2px; flex-wrap: wrap;
  align-items: center; }
.breakdown-value { color: var(--accent); font: 700 13px ui-monospace;
  justify-self: end; }
.breakdown-total { padding: 4px 6px 0;
  border-top: 1px dashed rgba(212,168,71,0.3);
  color: var(--accent-gold); font: 700 13px ui-monospace; text-align: right; }
.breakdown-total-score { color: var(--accent-gold);
  font: 700 17px ui-monospace; text-align: right;
  padding: 6px 6px 0; border-top: 1px solid var(--accent-gold); }
/* Pao (sekinin barai) — small attribution line under the grand total.
   Muted red to signal a payout liability that's bigger than the row's
   normal breakdown text; not a hard-warning red. */
.breakdown-pao { color: var(--pao); font: 600 12px ui-sans-serif;
  text-align: right; padding: 2px 6px 0; }
.result-meta { color: var(--muted); font-size: 11px; text-align: center; }
.score-table { display: flex; flex-direction: column; gap: 3px;
  width: 100%; margin-top: 4px; }
.score-row { display: grid; grid-template-columns: 1fr auto auto; gap: 10px;
  align-items: baseline; padding: 3px 8px; background: #0f0d0b80;
  border-radius: 6px; }
.score-who { color: var(--fg); font: 600 12px ui-monospace; text-align: left; }
.score-now { color: var(--accent); font: 700 12px ui-monospace; }
.score-delta { font: 700 12px ui-monospace; }
.score-delta.up { color: #6fae84; }
.score-delta.down { color: var(--down); }
/* Engine-authoritative auto-advance countdown ring, shown above Continue
   (the .countdown-ring class supplies the depleting-ring visual). */
/* Auto-advance countdown ring — pinned to the card's top-right corner,
   level with the result title. */
.result-countdown,
.summary-countdown { position: absolute; top: 22px; right: 26px; margin: 0; }
.result-continue { margin-top: 6px; }
.result-error { color: var(--down); font: 600 12px ui-sans-serif;
  margin-top: 4px; }

/* ---- end-of-session summary screen ------------------------------------ */
/* Reuses the .game-result overlay + .result-card chrome; the summary body
   is final standings (placement-ranked) + a hand-by-hand delta grid. */
.summary-card { gap: 16px; }
.summary-standings { display: flex; flex-direction: column; gap: 4px;
  width: 100%; }
.standing-row { display: grid; grid-template-columns: auto 1fr auto;
  gap: 12px; align-items: baseline; padding: 6px 12px;
  background: #0f0d0b80; border-radius: 6px; }
.standing-row.rank-1 { background: rgba(212,168,71,0.16);
  outline: 1px solid rgba(212,168,71,0.4); }
.standing-place { color: var(--accent-gold); font: 700 14px ui-monospace;
  min-width: 34px; text-align: left; }
.standing-name { color: var(--fg); font: 600 13px ui-sans-serif;
  text-align: left; }
.standing-score { color: var(--accent); font: 700 14px ui-monospace; }
.summary-history-title { text-align: left; }
.summary-history { display: grid; grid-template-columns: auto repeat(4, 1fr);
  gap: 2px; width: 100%; }
.summary-history > div { padding: 4px 8px; background: #0f0d0b80;
  font: 600 12px ui-monospace; text-align: right; }
.history-corner { background: transparent !important; }
.history-head { color: var(--accent); font-weight: 700;
  text-align: center !important; }
.history-seq { color: var(--muted); text-align: left !important; }
.history-cell { color: var(--fg); }
.history-cell.up { color: #6fae84; }
.history-cell.down { color: var(--down); }
.history-cell.win { outline: 1px solid var(--accent-gold);
  border-radius: 3px; }

/* No game-board media query needed: the board is a fixed full-viewport
   overlay and tile widths are clamp(min, vmin, max), so small screens are
   handled by the clamp lower bounds. */
body.in-game { overflow: hidden; }

/* Jade balance chip in the header (Phase 2a wallet). Sits left of the user menu. */
.jade-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .2rem .6rem;
  margin-right: .6rem;
  border: 1px solid var(--border, #2a3a33);
  border-radius: 999px;
  background: rgba(217, 176, 74, .10);
  color: var(--text, #e8edf2);
  font-weight: 700;
  font-size: .95rem;
  vertical-align: middle;
}

/* Login-streak badge beside the Jade chip (retention 🔥 N). Warm, status-y, distinct from Jade. */
.streak-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .55rem; margin-right: .6rem;
  border: 1px solid rgba(240, 130, 60, .35); border-radius: 999px;
  background: rgba(240, 130, 60, .12); color: var(--text, #e8edf2);
  font-weight: 800; font-size: .9rem; vertical-align: middle;
}
.streak-chip .streak-flame { font-size: .95rem; line-height: 1; }
.streak-chip .streak-count { font-variant-numeric: tabular-nums; }
.jade-chip .jade-gem { color: var(--gold, #d9b04a); font-size: .9em; line-height: 1; }
.jade-chip .jade-amount { font-variant-numeric: tabular-nums; }

/* Club badge (#56): icon-only pill beside the Jade chip, same visual family as
   .jade-chip. Two forms — a plain <a> (0/1 club → the /clubs hub) and a <details>
   summary (2+ clubs → a dropdown, reusing .header-menu / .header-menu-list). The
   flex header row has no gap, so items self-space with margin-right like .jade-chip. */
.club-badge {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
  margin: 0 .6rem 0 0;
  padding: .2rem .5rem;
  border: 1px solid var(--border, #2a3a33);
  border-radius: 999px;
  background: rgba(217, 176, 74, .10);
  color: var(--text, #e8edf2);
  text-decoration: none;
  cursor: pointer;
  vertical-align: middle;
}
.club-badge:hover { border-color: var(--accent, #d9b04a); color: var(--text, #e8edf2); }
.club-badge-icon { font-size: 1.05rem; line-height: 1; }
.club-menu { margin-right: .6rem; }
/* The dropdown trigger reuses .header-menu summary (padding/radius); re-assert the
   pill shape and drop the summary's own margin so it matches the <a> form. */
.club-menu > summary.club-badge { margin: 0; padding: .2rem .5rem; border-radius: 999px; }

/* Guest sign-up CTA: sits where the Jade chip would be for a signed-in player. A gold
   pill that invites the guest to create an account (links into the Keycloak flow). */
.signup-chip {
  display: inline-flex;
  align-items: center;
  padding: .2rem .7rem;
  margin-right: .6rem;
  border: 1px solid var(--gold, #d9b04a);
  border-radius: 999px;
  background: rgba(217, 176, 74, .12);
  color: var(--gold, #d9b04a);
  font-weight: 700;
  font-size: .9rem;
  text-decoration: none;
  vertical-align: middle;
  white-space: nowrap;
  /* On a cramped worlds topbar this CTA is the one item allowed to ellipsize, so the
     brand and the user controls stay whole and on-screen. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.signup-chip:hover { background: var(--gold, #d9b04a); color: #20180c; }

/* Phase 2b: per-seat Jade earned (gain-only) on the result card + summary board. Gold
   ❖ gem matching the header chip; sits beside the point delta / final score. */
.standing-jade {
  color: var(--gold, #d9b04a);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: .85em;
  margin-left: .4rem;
  white-space: nowrap;
}
.standing-jade::before {
  content: "\2756\00a0"; /* ❖ + nbsp */
}

/* Phase 3 cosmetics sink: a price badge on an unowned (locked) cosmetic card. The card
   stays clickable — clicking opens the buy flow. */
.avatar-tile, .tileset-card { position: relative; }
.cosmetic-price {
  position: absolute;
  top: .35rem;
  right: .35rem;
  display: inline-flex;
  align-items: center;
  padding: .1rem .45rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, .55);
  color: var(--gold, #d9b04a);
  font-weight: 700;
  font-size: .8rem;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.avatar-tile.is-locked, .tileset-card.is-locked { opacity: .82; }
/* Guest variant of the locked badge: a 🔒 (not a Jade price — a guest must create an
   account first, so a price would mislead). Same corner placement as the price badge. */
.cosmetic-lock {
  position: absolute;
  top: .35rem;
  right: .35rem;
  display: inline-flex;
  align-items: center;
  padding: .1rem .4rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, .55);
  font-size: .8rem;
  pointer-events: none;
}
.avatar-tile.is-guest-locked, .tileset-card.is-guest-locked { opacity: .7; }
.avatar-tile.is-locked:hover, .tileset-card.is-locked:hover { opacity: 1; }

/* In-app modal (cosmetic buy confirm / errors) — replaces native confirm()/alert(). */
.lt-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, .58);
  backdrop-filter: blur(2px);
}
.lt-modal {
  width: min(94vw, 22rem);
  background: var(--panel, #16201c);
  color: var(--text, #e8edf2);
  border: 1px solid var(--border, #2a3a33);
  border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
  padding: 1.1rem 1.2rem 1rem;
  animation: lt-modal-in .12s ease-out;
}
@keyframes lt-modal-in { from { transform: translateY(6px); opacity: 0; } to { transform: none; opacity: 1; } }
.lt-modal-title { font-weight: 700; font-size: 1.05rem; margin-bottom: .5rem; }
.lt-modal-body { font-size: .95rem; line-height: 1.45; color: var(--text, #e8edf2); }
.lt-modal-body .lt-jade { color: var(--gold, #d9b04a); font-weight: 700; white-space: nowrap; }
.lt-modal-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: 1.1rem; }
.lt-modal-btn {
  appearance: none;
  cursor: pointer;
  padding: .45rem .95rem;
  border-radius: 8px;
  border: 1px solid var(--border, #2a3a33);
  background: transparent;
  color: var(--text, #e8edf2);
  font-weight: 600;
  font-size: .9rem;
}
.lt-modal-btn:hover { border-color: var(--gold, #d9b04a); }
.lt-modal-btn.is-primary {
  background: var(--gold, #d9b04a);
  border-color: var(--gold, #d9b04a);
  color: #1a1408;
}
.lt-modal-btn.is-primary:hover { filter: brightness(1.08); }

/* Daily-cap signal: a "daily max" marker on the header Jade chip + "cap" in the scoreboard
   Jade column (above), so reaching the cap is never an unexplained disappearance. */
.jade-chip.is-maxed { opacity: .7; }
.jade-chip .jade-max {
  margin-left: .3rem;
  font-size: .72em;
  font-weight: 700;
  color: var(--muted, #8aa);
  text-transform: uppercase;
  letter-spacing: .03em;
}

/* Phase 3 table stakes: a jade-tinted badge on staked tables in the lobby listing
   (all three view modes — list row, tile card, graphical mini). */
.stake-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.2em;
    padding: 0.05em 0.45em;
    border-radius: 999px;
    font-size: 0.78em;
    font-weight: 600;
    color: #0b3b2e;
    background: linear-gradient(180deg, #7fe3b8, #46c391);
    border: 1px solid rgba(11, 59, 46, 0.35);
    white-space: nowrap;
}

/* Phase 3 table stakes: keep the create-table popup a sane width — the stake hint
   <small> is long, and without a max-width its max-content stretches the modal. */
#create-modal form { max-width: 380px; }
#create-modal label small { color: var(--muted); line-height: 1.3; }

/* Phase 3 table stakes: corner jade badge on the table page. Shown only in the immersive
   3D forming + in-game views (which hide the global header + its jade chip); top-right is
   free there (Leave is top-left, the id badge is bottom-right). z above the z-400 scene. */
.table-jade-badge {
    display: none;
    position: fixed;
    top: 0.6rem;
    right: 0.75rem;
    z-index: 906;
    align-items: center;
    gap: 0.6rem;
    padding: 6px 12px;
    border-radius: 7px;
    border: 1px solid var(--border);
    background: #0f0d0bcc;
    color: var(--fg);
    font: 700 12px ui-sans-serif;
    pointer-events: none;
}
.table-jade-badge .tjb-item { white-space: nowrap; }
.table-jade-badge.is-staked { border-color: var(--accent); }
body.forming-3d .table-jade-badge,
body.board-3d.in-game .table-jade-badge { display: flex; }

/* Phase 3 gifting: the "Gift a cosmetic" panel on the Appearance tile-set tab. */
.gift-panel {
    margin-top: 1.25rem;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--card-bg);
}
.gift-panel .gift-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin: 0.5rem 0;
    max-width: 320px;
}
.gift-panel .gift-field input,
.gift-panel .gift-field select {
    padding: 0.5rem;
    background: var(--bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 4px;
}

/* Phase 4 season pass page. */
.pass-page { max-width: 1100px; margin: 0 auto; padding: 1.5rem; }
.pass-page h1 { color: var(--accent); margin: 0.5rem 0; }
.pass-progress { margin: 1rem 0; }
.pass-xp-bar { height: 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.pass-xp-fill { height: 100%; width: 0; background: linear-gradient(90deg, #46c391, #7fe3b8); transition: width 0.4s ease; }
.pass-premium { display: flex; align-items: center; gap: 0.75rem; margin: 0.75rem 0; }
/* A class display rule overrides the `hidden` attribute's UA display:none, so the unlock
   row stayed visible/clickable after premium was unlocked — restore hidden explicitly. */
.pass-premium[hidden] { display: none; }
/* .action-btn has no base style (lobby buttons are styled by context selectors), so style
   the premium-unlock CTA here — gold, matching the premium theme. */
.pass-premium .action-btn {
    padding: 0.5rem 1.1rem;
    background: linear-gradient(180deg, #e6c462, #d9b04a);
    color: #2a2208;
    border: 1px solid rgba(217, 176, 74, 0.6);
    border-radius: 6px;
    font-weight: 700;
    cursor: pointer;
}
.pass-premium .action-btn:hover { filter: brightness(1.05); }
.pass-premium-active { color: var(--gold, #d9b04a); font-weight: 700; margin: 0.75rem 0; }
.pass-track { display: flex; gap: 0.6rem; overflow-x: auto; padding: 0.75rem 0; }
.pass-tier { flex: 0 0 auto; width: 130px; border: 1px solid var(--border); border-radius: 8px; background: var(--card-bg); padding: 0.5rem; opacity: 0.6; }
.pass-tier.is-reached { opacity: 1; border-color: var(--accent); }
.pass-tier-n { font-weight: 700; text-align: center; margin-bottom: 0.4rem; }
.pass-reward { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.4rem; border-radius: 6px; margin-bottom: 0.4rem; }
.pass-reward.is-free { background: rgba(255,255,255,0.03); }
.pass-reward.is-premium { background: rgba(217,176,74,0.10); border: 1px solid rgba(217,176,74,0.35); }
.pass-reward-label { font-size: 0.85em; text-align: center; }
.pass-claim-btn { padding: 4px 8px; border-radius: 5px; border: 1px solid var(--border); background: var(--accent); color: var(--bg); cursor: pointer; font-size: 0.8em; }
.pass-claim-btn:disabled { background: var(--card-bg); color: var(--muted); cursor: default; }
.pass-reward.is-claimed .pass-claim-btn { border-color: var(--accent); }
.pass-page .back-link { color: var(--muted); text-decoration: none; font-size: 0.9em; }
.pass-page .back-link:hover { color: var(--fg); }

/* ---- Private clubs (social spend) ---------------------------------- */
.clubs-page { max-width: 720px; margin: 0 auto; padding: 1rem 1.2rem 3rem; }
.clubs-section { margin-top: 1.6rem; }
.clubs-section h2 { font-size: 1.1rem; margin-bottom: 0.6rem; }
.clubs-list { display: flex; flex-direction: column; gap: 0.5rem; }
.clubs-row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.6rem 0.8rem;
  background: var(--card-bg, #15302a);
  border: 1px solid color-mix(in srgb, var(--accent, #2f6f5e) 50%, transparent);
  border-radius: 8px;
}
.clubs-row-name { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; }
.clubs-row-meta { margin-left: auto; }
.clubs-row-actions { display: inline-flex; gap: 0.5rem; align-items: center; }
.clubs-role-badge {
  padding: 0.05rem 0.45rem; border-radius: 999px;
  font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  background: rgba(255, 255, 255, 0.08); color: var(--muted, #9fb3ab);
}
.clubs-role-badge.role-owner { background: rgba(217, 178, 90, 0.22); color: var(--accent-gold, #d9b25a); }
.clubs-role-badge.role-admin { background: rgba(47, 111, 94, 0.35); color: #cdeede; }
.club-pending-badge {
  padding: 0.05rem 0.45rem; border-radius: 999px; font-size: 0.68rem;
  background: rgba(255, 255, 255, 0.06); color: var(--muted, #9fb3ab); font-style: italic;
}
.club-create { display: flex; gap: 0.5rem; margin-top: 0.8rem; }
.club-create input, .club-invite-input {
  flex: 1 1 auto; padding: 0.5rem 0.7rem;
  background: rgba(0, 0, 0, 0.25); color: var(--fg, #eaf5ef);
  border: 1px solid color-mix(in srgb, var(--accent, #2f6f5e) 50%, transparent); border-radius: 6px;
}
.club-detail {
  background: var(--card-bg, #12241f);
  border: 1px solid var(--accent, #2f6f5e); border-radius: 10px; padding: 1rem 1.1rem;
}
.club-detail-head { display: flex; align-items: center; gap: 0.6rem; }
.club-detail-name { font-size: 1.15rem; margin: 0; margin-right: auto; }
.club-invite { margin-top: 0.9rem; }
.club-invite-label { display: block; font-size: 0.85rem; color: var(--muted, #9fb3ab); margin-bottom: 0.3rem; }
.club-invite-row { display: flex; gap: 0.5rem; }
.club-roster-heading { font-size: 0.95rem; margin: 1rem 0 0.5rem; }
.club-roster { display: flex; flex-direction: column; gap: 0.4rem; }
.club-member-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.club-member-row:last-child { border-bottom: none; }
.club-member-name { display: flex; align-items: center; gap: 0.45rem; }
.club-member-actions { margin-left: auto; display: inline-flex; gap: 0.6rem; align-items: center; }
.club-detail-foot { margin-top: 1rem; display: flex; justify-content: flex-end; }

/* Small action button + a text-style link button (used across clubs UI). */
.action-btn.sm { padding: 0.3rem 0.7rem; font-size: 0.82rem; }
.link-btn {
  background: none; border: none; cursor: pointer; padding: 0.2rem 0.2rem;
  color: var(--accent-gold, #d9b25a); font: inherit; font-size: 0.82rem;
}
.link-btn:hover { text-decoration: underline; }
.link-btn.danger, .action-btn.danger { color: var(--danger, #d9534f); }
.action-btn.danger { border-color: var(--danger, #d9534f); }

/* Auto-shared club tables (on the club detail panel). */
.club-tables { margin-top: 0.4rem; }
.club-tables-create { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.6rem; }
.club-world-select {
  flex: 1 1 auto; padding: 0.4rem 0.6rem;
  background: rgba(0, 0, 0, 0.25); color: var(--fg, #eaf5ef);
  border: 1px solid color-mix(in srgb, var(--accent, #2f6f5e) 50%, transparent); border-radius: 6px;
}
.club-tables-list { display: flex; flex-direction: column; gap: 0.4rem; }
.club-table-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.club-table-row:last-child { border-bottom: none; }
.club-table-name { font-weight: 600; }
.club-table-meta { margin-left: auto; }

/* ===================== Unified settings page (/me/settings) ===================== */
/* Reuses main.avatar-picker (max-width, muted, .appearance-tabs, .tab-panel). */
.settings-identity { display: flex; align-items: center; gap: 1rem; margin: 1rem 0 1.5rem 0; }
.settings-identity-text { display: flex; flex-direction: column; gap: 0.2rem; }
.settings-handle { font-size: 1.15rem; font-weight: 600; color: var(--fg); }
.settings-inline-link { color: var(--accent); text-decoration: none; font-size: 0.9rem; }
.settings-inline-link:hover { text-decoration: underline; }
.settings-form { display: flex; flex-direction: column; gap: 0.5rem; max-width: 24rem; margin: 0.5rem 0 1.75rem 0; }
.settings-field { display: flex; flex-direction: column; gap: 0.3rem; }
.settings-field > span { font-size: 0.9rem; color: var(--muted); }
.settings-field input { padding: 0.55rem 0.7rem; background: var(--card-bg);
    border: 1px solid var(--border); border-radius: 6px; color: var(--fg);
    font-family: inherit; font-size: 0.95rem; }
.settings-field input:focus { outline: none; border-color: var(--accent); }
.settings-form .action-btn { align-self: flex-start; }
.action-btn-secondary { background: transparent; border: 1px solid var(--border);
    color: var(--fg); }
.action-btn-secondary:hover { border-color: var(--accent); }
.settings-account-link { border-top: 1px solid var(--border); padding-top: 1.25rem; }
/* Appearance summary (a compact read-out; the full picker lives at /me/avatar). */
.settings-summary { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem;
    margin: 1rem 0 1.5rem 0; }
.settings-summary > div { display: contents; }
.settings-summary dt { color: var(--muted); font-size: 0.9rem; }
.settings-summary dd { margin: 0; color: var(--fg); font-size: 0.9rem; }
/* Achievements stat cards + owned-cosmetic chips. */
.settings-stats { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1rem 0 1.5rem 0; }
.stat-card { flex: 1 1 8rem; background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 8px; padding: 0.9rem 1rem; text-align: center; }
.stat-value { font-size: 1.6rem; font-weight: 700; color: var(--accent-gold); }
.stat-label { font-size: 0.8rem; color: var(--muted); margin-top: 0.25rem; }
.settings-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0 1.25rem 0; }
.settings-chip { background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 999px; padding: 0.3rem 0.75rem; font-size: 0.85rem; color: var(--fg); }
/* Play report: three time-window columns. */
.settings-report { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 0.75rem; margin: 1rem 0; }
.report-col { background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 8px; padding: 0.9rem 1rem; }
.report-col-title { margin: 0 0 0.6rem 0; color: var(--accent); font-size: 0.95rem; }
/* #44: per-ruleset stats cards show the engine ruleCode humanised (underscores → spaces). */
.stats-rules .report-col-title { text-transform: capitalize; }
.report-row { display: flex; justify-content: space-between; align-items: baseline;
    padding: 0.25rem 0; font-size: 0.9rem; color: var(--muted); }
.report-row b { color: var(--fg); font-variant-numeric: tabular-nums; }
.report-row b.pos { color: var(--accent); }
.report-row b.neg { color: var(--danger); }
.settings-report-note { margin-top: 0.5rem; }

/* Challenges tab (#43/#44 daily/weekly/jackpot): one compact NON-WRAPPING row per
   challenge (a list, not cards). Flexible middle fields (hand name, world) ellipsis;
   the action group pins to the right. An error, when shown, drops below its row. */
.challenge-list { display: flex; flex-direction: column; gap: 0.6rem; margin: 1rem 0 1.5rem 0; }
/* An item is two lines: the info row, then the example-hand tiles. Wrap both in a
   card so each challenge reads as one block. */
.challenge-item { display: flex; flex-direction: column; gap: 0.4rem;
    background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 0.55rem 0.85rem; }
.challenge-row { display: flex; align-items: center; gap: 0.7rem; flex-wrap: nowrap; overflow: hidden; }
/* Second line: reuse lotusScoring.renderHand's card but show ONLY its tiles (the hand
   name/value/description live on the info line / are redundant here). Scrolls if a very
   long example doesn't fit. */
.challenge-hand { overflow-x: auto; }
.challenge-hand .scoring-hand { border: 0; background: none; padding: 0; margin: 0; }
.challenge-hand .scoring-hand-head, .challenge-hand .scoring-hand-desc { display: none; }
/* Keep the example hand on ONE line (the widened drawer fits a full hand); .challenge-hand
   scrolls horizontally if an unusually long example ever overflows, rather than wrapping. */
.challenge-hand .scoring-hand-tiles { margin: 0; flex-wrap: nowrap; }
.challenge-row-kind { flex: 0 0 auto; color: var(--accent); font-weight: 700; font-size: 0.9rem; white-space: nowrap; }
.challenge-row-hand { flex: 1 1 auto; min-width: 3rem; font-weight: 700; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
.challenge-row-world { flex: 0 1 auto; color: var(--accent); font-size: 0.85rem; text-decoration: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-bottom: 1px dotted var(--accent); }
.challenge-row-world:hover, .challenge-row-world:focus-visible { border-bottom-style: solid; }
.challenge-row-reward { flex: 0 0 auto; color: var(--accent-gold); font-weight: 700; font-size: 0.85rem;
    white-space: nowrap; font-variant-numeric: tabular-nums; }
.challenge-row-stake { flex: 0 0 auto; white-space: nowrap; color: var(--muted); font-size: 0.8rem; }
.challenge-row-action { flex: 0 0 auto; margin-left: auto; display: inline-flex; align-items: center;
    gap: 0.5rem; white-space: nowrap; }
.challenge-state { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
    border-radius: 999px; padding: 0.1rem 0.5rem; white-space: nowrap; color: var(--muted);
    border: 1px solid var(--border); flex: 0 0 auto; }
.challenge-state-completed { color: var(--gold); border-color: var(--gold);
    background: color-mix(in srgb, var(--gold) 15%, transparent); }
.challenge-state-claimed { color: var(--accent); border-color: var(--accent); }
.challenge-state-in_progress { color: var(--fg); }
.challenge-state-locked { color: var(--muted); }
.challenge-timeleft { flex: 0 0 auto; font-size: 0.8rem; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.challenge-status-label { font-style: italic; white-space: nowrap; }
.challenge-claim, .challenge-accept, .challenge-enter { padding: 0.28rem 0.7rem; font-size: 0.82rem; white-space: nowrap; }
.challenge-error { color: var(--danger); font-size: 0.8rem; margin: 0 0 0 0.85rem; }
.challenge-recent { list-style: none; margin: 0 0 1rem 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.challenge-recent li { font-size: 0.85rem; color: var(--muted); padding: 0.2rem 0; border-bottom: 1px solid var(--border); }
.challenge-recent li:last-child { border-bottom: 0; }

/* Badges tab (#45): a per-ruleset grid of Sprout/Bud/Blossom chips, the Golden Lotus
   capstone chip, and a row of legendary-hand "special" chips. Fully server-rendered
   from BadgesView — badges.js only marks-seen, it never builds this markup. */
.badges-summary { margin-top: -0.4rem; }
.badges-rulesets { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.75rem; margin: 0.75rem 0 1.25rem 0; }
.badge-ruleset-card { background: var(--card-bg); border: 1px solid var(--border);
    border-radius: 8px; padding: 0.75rem 0.9rem; }
.badge-ruleset-name { margin: 0 0 0.5rem 0; color: var(--accent); font-size: 0.9rem; }
.badge-tier-row { display: flex; flex-direction: column; gap: 0.4rem; }
.badges-specials { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0 1.25rem 0; }
/* One tier/special chip: icon + name (+ optional progress readout). Earned = full colour
   + a gold rim; locked = grayscale + dimmed, so "not yet" reads at a glance. */
.badge-chip { display: flex; align-items: center; gap: 0.45rem;
    padding: 0.35rem 0.65rem; border-radius: 8px; border: 1px solid var(--border);
    background: var(--card-bg); font-size: 0.85rem; transition: opacity 0.15s ease; }
.badge-chip-icon { font-size: 1.05rem; line-height: 1; }
.badge-chip-name { font-weight: 700; color: var(--fg); flex: 1 1 auto; }
.badge-chip-progress { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums;
    white-space: nowrap; }
.badge-chip.is-earned { border-color: var(--gold, #d9b04a);
    background: color-mix(in srgb, var(--gold) 12%, var(--card-bg)); }
.badge-chip.is-earned .badge-chip-name { color: var(--accent-gold); }
.badge-chip.is-locked { opacity: 0.55; filter: grayscale(1); }
.badge-chip-golden { max-width: 22rem; }
.badge-chip-golden.is-earned {
    background: linear-gradient(135deg, color-mix(in srgb, #2f9e6b 22%, var(--card-bg)),
        color-mix(in srgb, var(--gold) 18%, var(--card-bg)));
    border-color: var(--gold, #d9b04a);
}

/* #60 — clickable badge chip → popup (#badge-hands-modal) listing that badge's hands:
   won hands full-colour, not-yet-won greyed (.is-locked). Mirrors the scoreboard modal
   (native <dialog>; ✕/ESC/backdrop close wired in badges.js). */
.badge-chip[role="button"] { cursor: pointer; }
.badge-chip[role="button"]:hover { border-color: var(--accent); }
.badge-chip[role="button"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.modal.badge-hands-doc[open] { display: flex; flex-direction: column;
    width: min(680px, 94vw); max-height: 84vh; padding: 1rem 1.1rem; }
.badge-hands-modal-head { flex: 0 0 auto; display: flex; align-items: flex-start;
    justify-content: space-between; gap: 1rem; }
.badge-hands-modal-title { margin: 0; color: var(--accent); font-size: 1.05rem; }
.badge-hands-legend { flex: 0 0 auto; display: flex; gap: 1.1rem; margin: 0.4rem 0 0.7rem 0;
    color: var(--muted); font-size: 0.8rem; }
.badge-hands-legend-item { display: inline-flex; align-items: center; gap: 0.35rem; }
.badge-hands-swatch { width: 0.85rem; height: 0.85rem; border-radius: 3px;
    border: 1px solid var(--gold, #d9b04a);
    background: color-mix(in srgb, var(--gold) 45%, var(--card-bg)); }
.badge-hands-swatch.is-locked { opacity: 0.5; filter: grayscale(1); }
.badge-hands-body { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; }
.badge-hands-group + .badge-hands-group { margin-top: 0.9rem; }
.badge-hands-group-name { margin: 0 0 0.5rem 0; color: var(--accent); font-size: 0.9rem; }
.badge-hands-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.6rem; }
.badge-hand-card { border: 1px solid var(--border); border-radius: 8px; padding: 0.4rem 0.5rem;
    background: var(--card-bg); }
/* Not-yet-won: STILL show the example hand, only desaturated + slightly dimmed so it
   reads as "not yet" while staying clearly legible (so you can see what to aim for). */
.badge-hand-card.is-locked { opacity: 0.72; filter: grayscale(0.85); }
.badge-hand-card.is-locked .scoring-hand-name { color: var(--muted); }
/* Inner scoring-hand: strip its own card chrome + drop the long description → just the
   localized name + example tiles (the challenge-hand idiom, but keeping the name). */
.badge-hand-card .scoring-hand { border: none; padding: 0; margin: 0; background: none; }
.badge-hand-card .scoring-hand-desc { display: none; }
/* …EXCEPT situational hands (riichi, ippatsu, self-draw, …) which have no example tiles:
   show their description so the card conveys the rule instead of a bare name. */
.badge-hand-card.badge-hand-no-tiles .scoring-hand-desc { display: block;
    margin: 0.25rem 0 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.35; }
/* Golden Lotus champion popup — a celebration, not a hand list: a trophy (earned) / golden
   lotus (chasing) emblem + how many rulesets are Blossomed. */
.badge-golden-hero { text-align: center; padding: 1.4rem 1rem 1rem; }
.badge-golden-emblem { font-size: 4.5rem; line-height: 1;
    filter: drop-shadow(0 0 14px color-mix(in srgb, var(--gold, #d9b04a) 55%, transparent)); }
.badge-golden-hero.is-earned .badge-golden-emblem {
    filter: drop-shadow(0 0 24px color-mix(in srgb, var(--gold, #d9b04a) 85%, transparent)); }
.badge-golden-desc { margin: 0.85rem auto 0; max-width: 24rem; color: var(--muted);
    font-size: 0.9rem; line-height: 1.4; }
.badge-golden-progress { margin-top: 1rem; display: inline-block; font-weight: 800;
    font-size: 1.05rem; color: var(--accent-gold); padding: 0.4rem 1rem; border-radius: 999px;
    background: color-mix(in srgb, var(--gold, #d9b04a) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--gold, #d9b04a) 45%, transparent); }
@media (prefers-reduced-motion: no-preference) {
    .badge-golden-hero.is-earned .badge-golden-emblem {
        animation: badge-golden-pulse 2.4s ease-in-out infinite; }
}
@keyframes badge-golden-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }

/* When the lotus SVG loads, the emblem becomes a square illustration host (badges.js
   mountGoldenLotusSvg): drop the emoji-only drop-shadow/pulse and let the SVG fill it. */
.badge-golden-emblem.badge-golden-svg {
    font-size: 0; line-height: 0; filter: none; animation: none;
    width: min(320px, 76vw); height: min(320px, 76vw); margin: 0.2rem auto 0; }
.badge-golden-emblem.badge-golden-svg svg { display: block; width: 100%; height: 100%; }

/* Challenge-achieved celebration on the result card (#43/#44) — a gold banner
   prepended to .result-card + a short confetti burst, built by game-render.js's
   celebrateAchievedChallenges when the local player wins a hand that satisfies
   an enrolled, this-ruleset challenge. The drawer badge flips to COMPLETED at
   the same moment (reuses .challenge-state-completed above). */
.challenge-celebrate { position: relative; margin: 0 0 0.75rem 0; padding: 0.55rem 0.85rem;
    border-radius: 10px; text-align: center; overflow: hidden; z-index: 2;
    color: #1a1408; background: linear-gradient(135deg, #f5e6a8, #d4af37 55%, #b8860b);
    border: 1px solid var(--accent-gold);
    box-shadow: 0 0 18px color-mix(in srgb, var(--gold) 45%, transparent);
    animation: cc-pop 0.5s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes cc-pop { from { transform: scale(0.82); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.challenge-celebrate-row { display: flex; flex-direction: column; gap: 0.05rem; }
.challenge-celebrate-row + .challenge-celebrate-row { margin-top: 0.3rem; padding-top: 0.3rem;
    border-top: 1px solid color-mix(in srgb, #1a1408 22%, transparent); }
.cc-title { font-weight: 800; font-size: 1rem; letter-spacing: 0.01em; }
.cc-detail { font-size: 0.85rem; font-weight: 700; opacity: 0.92; }
.cc-claim { margin-top: 0.35rem; font-size: 0.8rem; font-weight: 700; opacity: 0.9; }
/* Confetti — absolutely-positioned bits falling across the card top; purely cosmetic. */
.cc-confetti { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 1; }
.cc-confetti-bit { position: absolute; top: -12px; width: 7px; height: 12px; border-radius: 1px;
    opacity: 0.95; animation-name: cc-fall; animation-timing-function: ease-in;
    animation-iteration-count: 1; animation-fill-mode: forwards; }
@keyframes cc-fall { 0% { transform: translateY(-12px) rotate(0deg); opacity: 1; }
    100% { transform: translateY(240px) rotate(540deg); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    .challenge-celebrate { animation: none; }
    .cc-confetti { display: none; }
}

/* Global winner-announcement toast (announce.js) — shown on every page, incl. in-game.
   Fixed top-center, above everything (the highest existing z-index in the app is 1000 —
   the cosmetic-buy modal + the connection-status pill — so this sits above both, and
   above the in-game 3D canvas/overlays which top out around 950-980). The container
   itself is pointer-events:none (never blocks clicks on the page under it); individual
   toasts don't need pointer-events since they carry no interactive controls. */
#global-announce {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1100;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  pointer-events: none;
  max-width: min(92vw, 30rem);
}
.announce-hero, .announce-toast, .announce-feed,
.announce-badge-hero, .announce-badge-toast {
  border-radius: 10px;
  padding: 0.6rem 1.1rem;
  text-align: center;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  animation: announce-in 220ms ease-out;
  transition: opacity 380ms ease, transform 380ms ease;
}
.announce-hero.is-leaving, .announce-toast.is-leaving, .announce-feed.is-leaving,
.announce-badge-hero.is-leaving, .announce-badge-toast.is-leaving {
  opacity: 0;
  transform: translateY(-6px);
}
/* JACKPOT — big, gold, celebratory. */
.announce-hero {
  font-size: 1.15rem;
  font-weight: 800;
  color: #1a1408;
  background: linear-gradient(135deg, var(--gold, #d9b04a), var(--accent, #f3c552));
  border: 1px solid var(--gold, #d9b04a);
  box-shadow: 0 10px 34px rgba(217, 176, 74, 0.45);
}
/* WEEKLY — medium. */
.announce-toast {
  font-size: 0.98rem;
  font-weight: 700;
  color: var(--fg, #e8edf2);
  background: var(--card-bg, #193040);
  border: 1px solid var(--accent, #f3c552);
}
/* Login-streak "welcome back" toast — two lines (headline + streak/reward), reuses the
   announce container/animations. Milestones use the gold .announce-hero base. */
.welcome-toast { min-width: 12rem; }
.welcome-toast .welcome-toast-head { font-weight: 800; }
.welcome-toast .welcome-toast-sub { margin-top: .15rem; font-size: .82rem; font-weight: 600; opacity: .9; }
.wb-confetti { position: fixed; inset: 0; pointer-events: none; z-index: 1101; overflow: hidden; }
.wb-confetti i {
  position: absolute; top: -12px; width: 8px; height: 12px; border-radius: 2px;
  animation: wb-fall 2.4s linear forwards;
}
@keyframes wb-fall {
  0%   { transform: translateY(0) rotate(0); opacity: 1; }
  100% { transform: translateY(70vh) rotate(360deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .wb-confetti { display: none; } }
/* DAILY — small/subtle, lobby-only (announce.js skips it in-game). */
.announce-feed {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted, #8aa);
  background: var(--card-bg, #193040);
  border: 1px solid var(--border);
}
/* GOLDEN_LOTUS (#45 capstone — Blossom in every ruleset) — hero-weight like JACKPOT
   (rare + exciting enough to interrupt a game) but jade-toned with a gold rim, so it
   reads as its own kind of celebration rather than a currency win. */
.announce-badge-hero {
  font-size: 1.15rem;
  font-weight: 800;
  color: #0d1f16;
  background: linear-gradient(135deg, #6fcf9e, #2f9e6b 55%, #1f7a52);
  border: 1px solid var(--accent-gold, #eac558);
  box-shadow: 0 10px 34px rgba(47, 158, 107, 0.45);
}
/* SPECIAL legendary-hand badge (#45) — toast-weight, jade accent; not suppressed
   in-game (landing e.g. Thirteen Orphans mid-game is exactly when to celebrate it). */
.announce-badge-toast {
  font-size: 0.98rem;
  font-weight: 700;
  color: var(--fg, #e8edf2);
  background: var(--card-bg, #193040);
  border: 1px solid #2f9e6b;
}
@keyframes announce-in {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Header reward-reminder (🎁): sits between the Jade chip and the user menu. Links
   straight into Settings → Achievements → Challenges. header-badge.js toggles the
   `hidden` attribute live; the count badge starts server-rendered from
   challengeUnclaimed so it's correct on first paint with no flash. */
.reward-reminder {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-right: 0.6rem;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  border: 1px solid var(--gold, #d9b04a);
  background: rgba(217, 176, 74, 0.12);
  text-decoration: none;
  vertical-align: middle;
  animation: reward-reminder-pulse 2.2s ease-in-out infinite;
}
.reward-reminder[hidden] { display: none; }
.reward-reminder:hover { background: rgba(217, 176, 74, 0.22); }
.reward-reminder .reward-gift { font-size: 1rem; line-height: 1; }
.reward-reminder .reward-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: var(--danger, #d66);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.1rem;
}
@keyframes reward-reminder-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(217, 176, 74, 0.35); }
  50%      { box-shadow: 0 0 0 5px rgba(217, 176, 74, 0); }
}

/* Header new-badge reminder (🏵️, issue #45): sits beside the 🎁 reward-reminder. Links
   straight into Settings → Badges. badges.js hides it + zeroes the count the instant the
   Badges tab is opened (POST /api/badges/seen); the count badge starts server-rendered
   from badgeNewCount so it's correct on first paint with no flash. Jade-toned (vs the
   reward-reminder's gold) so the two pills read as distinct kinds of "you have something". */
.badge-reminder {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-right: 0.6rem;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  border: 1px solid #2f9e6b;
  background: rgba(47, 158, 107, 0.14);
  text-decoration: none;
  vertical-align: middle;
  animation: badge-reminder-pulse 2.2s ease-in-out infinite;
}
.badge-reminder[hidden] { display: none; }
.badge-reminder:hover { background: rgba(47, 158, 107, 0.24); }
.badge-reminder .badge-reminder-icon { font-size: 1rem; line-height: 1; }
.badge-reminder .badge-reminder-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: var(--danger, #d66);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.1rem;
}
@keyframes badge-reminder-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(47, 158, 107, 0.35); }
  50%      { box-shadow: 0 0 0 5px rgba(47, 158, 107, 0); }
}

/* ── Scrolling challenge banner (issue #43/#44 follow-up) ───────────────────
   Reused on `/worlds` (all active challenges) and each per-ruleset lobby
   (filtered). It lives INLINE in the header row — a `flex:1` middle child
   between the brand/wordmark (left) and the Jade chip + user menu (right),
   so it sits centered on the same level as those controls. A compact pill
   with one active item crossfading every 3s. On the immersive globe it gets
   a translucent backdrop so it reads over the starfield; it's hidden where
   the header is too tight to hold three groups. */
.challenge-ticker {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 560px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.85rem;
  box-sizing: border-box;
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--fg);
  cursor: pointer;
  overflow: hidden;
  transition: filter 0.15s ease;
}
.challenge-ticker:hover, .challenge-ticker:focus-visible { filter: brightness(1.1); }
.challenge-ticker:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ticker-lead {
  flex: 0 0 auto;
  font-weight: 800;
  color: var(--accent);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.challenge-ticker-track {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  height: 1.5rem;
  overflow: hidden;
  list-style: none;
  margin: 0;
  padding: 0;
}

.challenge-ticker-item {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  white-space: nowrap;
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.challenge-ticker-item.is-active { opacity: 1; }

.ticker-kind {
  flex: 0 0 auto;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: 0.64rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border: 1px solid transparent;
}
.ticker-kind-daily {
  color: var(--accent);
  background: rgba(243, 197, 82, 0.14);
  border-color: rgba(243, 197, 82, 0.4);
}
.ticker-kind-weekly {
  color: var(--gold);
  background: rgba(217, 176, 74, 0.16);
  border-color: rgba(217, 176, 74, 0.45);
}
.ticker-kind-jackpot {
  color: var(--danger);
  background: rgba(221, 102, 102, 0.16);
  border-color: rgba(221, 102, 102, 0.45);
}

.ticker-hand { flex: 0 1 auto; font-weight: 700; overflow: hidden; text-overflow: ellipsis; }
.ticker-world { flex: 0 1 auto; color: var(--muted); font-size: 0.9em; overflow: hidden; text-overflow: ellipsis; }
.ticker-amount {
  flex: 0 0 auto;
  font-weight: 800;
  color: var(--accent-gold);
  font-variant-numeric: tabular-nums;
}

/* Immersive globe: translucent so the starfield still shows through the pill. */
body.worlds-immersive .challenge-ticker {
  background: rgba(5, 10, 18, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border-color: rgba(234, 197, 88, 0.3);
}

/* The header can't hold brand + ticker + account controls on a narrow screen —
   drop the (supplementary) ticker; challenges stay reachable via the header
   reward badge and the Challenges settings tab. */
@media (max-width: 820px) {
  .challenge-ticker { display: none; }
}

/* ============================================================
   Demo-site watermark (#62) + cookie-consent banner (#61)
   ============================================================ */

/* Deployment "DEMO" pill — a small button (shown only on -smoke) that reveals a note explaining the
 * site is a demo deployment on hover / focus / tap. It sits low, inside the footer band BELOW the
 * footer's top-border separator line (the pill is short enough that its top edge stays under the
 * line); the note opens upward. */
.demo-watermark {
  position: fixed;
  bottom: 0.4rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 900;
  pointer-events: auto;
  cursor: pointer;
  padding: 3px 14px;
  font: 800 12px/1 var(--font-ui);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--on-accent);
  background: linear-gradient(180deg, var(--accent), var(--gold));
  border: 1px solid rgba(0, 0, 0, 0.25);
  border-radius: 999px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  opacity: 0.55;
  user-select: none;
  transition: opacity 0.15s ease;
}
.demo-watermark:hover, .demo-watermark:focus-visible, .demo-watermark.is-open { opacity: 1; }
.demo-watermark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The explanatory note — a small card above the pill, hidden until hover / focus / tap. */
.demo-watermark-note {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(280px, 80vw);
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid rgba(var(--gold-rgb), 0.4);
  background: #0f1a24;
  color: var(--fg);
  font: 500 12px/1.45 var(--font-ui);
  letter-spacing: normal;
  text-transform: none;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.demo-watermark:hover .demo-watermark-note,
.demo-watermark:focus-visible .demo-watermark-note,
.demo-watermark.is-open .demo-watermark-note { opacity: 1; visibility: visible; }
@media (max-width: 760px) {
  .demo-watermark { padding: 3px 12px; font-size: 11px; }
}

/* Cookie-consent banner — fixed bottom sheet, themed to match panels. */
.cookie-consent {
  position: fixed;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 1200;
  width: min(760px, calc(100vw - 24px));
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 18px;
  padding: 16px 18px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
  color: var(--fg);
  animation: cookie-consent-in 0.28s ease-out both;
}
@media (prefers-reduced-motion: reduce) { .cookie-consent { animation: none; } }
@keyframes cookie-consent-in {
  from { opacity: 0; transform: translate(-50%, 14px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.cookie-consent-dismissed { opacity: 0; transform: translate(-50%, 14px); transition: opacity 0.25s, transform 0.25s; }
.cookie-consent-main { flex: 1 1 320px; min-width: 0; }
.cookie-consent-title { font: 700 15px/1.2 var(--font-ui); margin-bottom: 4px; }
.cookie-consent-text { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--fg); }
.cookie-consent-link { color: var(--accent); font-weight: 700; text-decoration: underline; }
.cookie-consent-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.cookie-consent-btn {
  font: 700 13px/1 var(--font-ui);
  padding: 9px 14px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--fg);
  cursor: pointer;
}
.cookie-consent-btn:hover { border-color: var(--accent); }
.cookie-consent-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cookie-consent-primary {
  background: linear-gradient(180deg, var(--accent), var(--gold));
  color: var(--on-accent);
  border-color: rgba(0, 0, 0, 0.25);
}
/* Manage panel */
.cookie-consent-panel { width: 100%; margin-top: 12px; border-top: 1px solid var(--hair); padding-top: 10px; }
.cookie-consent-cat { padding: 8px 0; border-bottom: 1px solid var(--hair); }
.cookie-consent-cat:last-child { border-bottom: 0; }
.cookie-consent-cat-head { display: flex; align-items: center; gap: 10px; }
.cookie-consent-cat-name { font-weight: 700; font-size: 13.5px; }
.cookie-consent-cat-state { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.cookie-consent-cat-desc { margin: 3px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--muted); }
.cookie-consent-switch { margin-left: auto; }
.cookie-consent-switch input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }

/* Cookie-policy page (#61). */
.cookies-page { max-width: 760px; margin: 0 auto; padding: 28px 20px 60px; line-height: 1.6; }
.cookies-page h1 { font-family: var(--font-display); color: var(--accent); }
.cookies-page h2 { font-size: 17px; margin-top: 28px; color: var(--fg); }
.cookies-page p { color: var(--fg); }
.cookies-page .cookies-back { display: inline-block; margin-top: 28px; color: var(--accent); font-weight: 700; }

/* ---- About page (#about) --------------------------------------------- */
.about-page { max-width: 860px; margin: 0 auto; padding: 28px 20px 72px; line-height: 1.65; }
.about-hero { text-align: center; padding: 12px 0 8px; }
.about-hero h1 { font-family: var(--font-display); color: var(--accent); font-size: 34px; margin: 0 0 8px; }
.about-tagline { color: var(--accent-gold); font-size: 18px; font-weight: 600; margin: 0 auto 14px; max-width: 40ch; }
.about-intro { color: var(--fg); max-width: 60ch; margin: 0 auto; }
.about-section { margin-top: 40px; }
.about-section > h2 { font-family: var(--font-display); font-size: 21px; color: var(--accent); margin: 0 0 12px;
                      border-bottom: 1px solid rgba(var(--gold-rgb), 0.28); padding-bottom: 8px; }
.about-section > p { color: var(--fg); }
.about-section-intro { color: var(--muted); margin: -4px 0 16px; max-width: 60ch; }
.about-values { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-top: 4px; }
.about-value { background: var(--scrim); border: 1px solid rgba(var(--gold-rgb), 0.22); border-radius: 12px;
               padding: 18px 18px 16px; backdrop-filter: blur(2px); }
.about-value-icon { font-size: 26px; line-height: 1; margin-bottom: 8px; }
.about-value h3 { margin: 0 0 6px; font-size: 16px; color: var(--accent-gold); }
.about-value p { margin: 0; color: var(--fg); font-size: 14px; line-height: 1.55; }
.about-support { background: linear-gradient(135deg, rgba(var(--gold-rgb), 0.14), rgba(var(--gold-rgb), 0.04));
                 border: 1px solid rgba(var(--gold-rgb), 0.35); border-radius: 14px; padding: 22px 22px 18px; }
.about-support > h2 { border-bottom: none; padding-bottom: 0; margin-bottom: 8px; }
.about-contact .about-email { display: inline-block; margin-top: 4px; color: var(--accent); font-weight: 700;
                              font-size: 17px; text-decoration: none; }
.about-contact .about-email:hover { text-decoration: underline; }
.about-back { display: inline-block; margin-top: 40px; color: var(--accent); font-weight: 700; text-decoration: none; }
.about-back::before { content: "\2190\00a0"; }
/* footer links (About / Cookie Policy) — pinned right by the footer's space-between row */
.footer-links { margin: 0; display: flex; gap: 16px; flex-wrap: wrap; }
.footer-links a { color: var(--accent); font-size: 12px; font-weight: 700; text-decoration: none; }
.footer-links a:hover { color: var(--accent-gold); text-decoration: underline; }

/* ---- About modal (footer "About" opens over the page) ---------------- */
.about-modal { max-width: min(680px, 92vw); width: 92vw; max-height: 84vh; overflow-y: auto;
  border: 1px solid rgba(var(--gold-rgb), 0.38); border-radius: 16px; padding: 6px 4px 4px;
  background: #0f1a24; color: var(--fg); box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
  /* The dialog lives inside the immersive footer, which is pointer-events:none — and pointer-events
     INHERITS. Force it back on, or the modal can't be scrolled or closed on the globe page. */
  pointer-events: auto; }
.about-modal * { pointer-events: auto; }
.about-modal::backdrop { background: rgba(4, 8, 14, 0.66); backdrop-filter: blur(2px); }
.about-modal-close { position: sticky; top: 4px; float: right; margin: 0 6px; background: none; border: none;
  color: var(--muted); font-size: 26px; line-height: 1; cursor: pointer; z-index: 2; }
.about-modal-close:hover { color: var(--accent); }
/* Modal-rendered page content (.about-page / .cookies-page keep their class for ancestor-scoped styles;
   this neutralises their outer page max-width/margin). */
.about-modal .content-modal-inner { max-width: none; margin: 0; padding: 4px 22px 26px; }
.about-modal .about-hero h1 { font-size: 27px; }
.about-modal-loading { text-align: center; color: var(--muted); padding: 44px 0; font-size: 22px; }

/* ---- Immersive globe (/worlds) footer fixes -------------------------- */
/* The footer bar is pointer-events:none so the globe stays draggable through it — but the About /
   Cookie links must still be clickable, so opt them back in (like .worlds-topbar > *). */
body.worlds-immersive footer small,
body.worlds-immersive footer .footer-links,
body.worlds-immersive footer .footer-links a { pointer-events: auto; }
/* Layout (single compact row, text left / links right) is inherited from the base footer rule so both
   the globe and the per-world lobby footers match in height. */
body.worlds-immersive .globe-hint { bottom: 48px; }

/* ── How to play (/learn) ────────────────────────────────────────────────
 * The lesson page for someone new to mahjong. Shares the /about page's
 * measure and rhythm so the two public pages read as one site; what is new
 * here is the tile art (rows of real tile SVGs from tiles.js, which may be
 * wider than the text column on a phone, hence the per-row horizontal
 * scroll) and the two tappable practice steps.
 */
.learn-page { max-width: 860px; margin: 0 auto; padding: 28px 20px 72px; line-height: 1.65; }
.learn-hero { text-align: center; padding: 12px 0 8px; }
.learn-hero h1 { font-family: var(--font-display); color: var(--accent); font-size: 34px; margin: 0 0 8px; }
.learn-tagline { color: var(--accent-gold); font-size: 18px; font-weight: 600; margin: 0 auto 14px; max-width: 46ch; }

.learn-steps { list-style: none; margin: 28px 0 0; padding: 0; display: flex; flex-direction: column; gap: 26px; }
.learn-step { background: var(--scrim); border: 1px solid rgba(var(--gold-rgb), 0.22); border-radius: 12px;
              padding: 20px 20px 18px; backdrop-filter: blur(2px); }
.learn-step-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
/* The step number is a landmark, not decoration: the lessons are a sequence. */
.learn-step-n { flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
                display: flex; align-items: center; justify-content: center;
                background: rgba(var(--gold-rgb), 0.16); color: var(--accent-gold);
                font-weight: 700; font-size: 15px; }
.learn-step-head h2 { font-family: var(--font-display); font-size: 21px; color: var(--accent); margin: 0; }
.learn-step > p { color: var(--fg); margin: 0 0 4px; max-width: 62ch; }
.learn-note { color: var(--muted); font-size: 14px; margin-top: 10px !important; }

/* Tile art. Rows scroll inside themselves rather than widening the page. */
.learn-art { margin: 14px 0 4px; overflow-x: auto; }
.learn-tile-row { display: flex; align-items: flex-end; gap: 14px; }
.learn-tile-group { display: flex; gap: 1px; }
.learn-labelled { display: flex; flex-wrap: wrap; gap: 18px 22px; }
.learn-labelled-cell { display: flex; flex-direction: column; gap: 6px; }
.learn-caption { color: var(--muted); font-size: 13px; text-align: center; }
.learn-claim { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.learn-claim-part { display: flex; flex-direction: column; gap: 6px; }
.learn-claim-plus { color: var(--accent-gold); font-size: 20px; font-weight: 700; padding-bottom: 22px; }
.learn-tile-fallback { font-family: ui-monospace, monospace; color: var(--muted); padding: 0 2px; }

/* Practice steps — tappable tiles with a spoken-aloud verdict. */
.learn-practice { margin-top: 14px; border-top: 1px dashed rgba(var(--gold-rgb), 0.26); padding-top: 14px; }
.learn-practice-prompt { color: var(--accent-gold); font-weight: 600; margin: 0 0 10px; }
.learn-practice-hand { overflow-x: auto; margin-bottom: 12px; opacity: 0.92; }
.learn-practice-tiles { display: flex; flex-wrap: wrap; gap: 8px; }
.learn-choice { background: none; border: 2px solid transparent; border-radius: 8px; padding: 2px;
                cursor: pointer; line-height: 0; transition: transform 0.12s ease, border-color 0.12s ease; }
.learn-choice:hover { transform: translateY(-3px); }
.learn-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.learn-choice.is-correct { border-color: var(--accent-gold); transform: translateY(-4px); }
.learn-choice.is-wrong { border-color: var(--danger); }
.learn-practice.is-solved .learn-choice:not(.is-correct) { opacity: 0.45; cursor: default; }
.learn-practice.is-solved .learn-choice:not(.is-correct):hover { transform: none; }
.learn-practice-feedback { min-height: 1.4em; margin: 12px 0 0; font-size: 15px; }
.learn-practice-feedback.is-correct { color: var(--accent-gold); }
.learn-practice-feedback.is-wrong { color: var(--danger); }

.learn-outro { margin-top: 36px; text-align: center; }
.learn-outro h2 { font-family: var(--font-display); font-size: 22px; color: var(--accent); margin: 0 0 8px; }
.learn-outro p { color: var(--fg); max-width: 60ch; margin: 0 auto 18px; }
.learn-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

@media (max-width: 600px) {
    .learn-hero h1 { font-size: 27px; }
    .learn-step { padding: 16px 14px 14px; }
    .learn-step-head h2 { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) {
    .learn-choice, .learn-choice:hover { transition: none; transform: none; }
}
/* Quiet "new to mahjong?" link under the empty-lobby CTAs, and the matching
 * link on the About page's "Learn as you play" card. */
.empty-learn { margin: 12px 0 0; font-size: 14px; }
.empty-learn a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.empty-learn a:hover, .empty-learn a:focus-visible { color: var(--accent); }
.about-value-link { margin: 10px 0 0; font-size: 14px; }
.about-value-link a { color: var(--accent-gold); text-decoration: none; font-weight: 600; }
.about-value-link a:hover, .about-value-link a:focus-visible { text-decoration: underline; }

/* ── Public landing page (/) ────────────────────────────────────────────── */
/* The brand lockup is centred above the hero; the language picker is pinned to the
   viewport's top-right corner, out of the lockup's way. */
.landing-top { display: flex; justify-content: center; padding: 28px 20px 4px; }
.landing-brand { display: flex; align-items: center; gap: 14px; text-decoration: none; }
.landing-brand-icon { width: 88px; height: 88px; display: block; }
/* Same two-tone lockup as the worlds topbar, so the front door and the app read as one brand. */
.landing-wordmark { font-family: var(--font-display); font-size: 2.6rem; line-height: 1; white-space: nowrap; }
.landing-lang { position: fixed; top: 14px; right: 16px; z-index: 20;
                background: var(--card-bg); color: var(--fg); border: 1px solid var(--border);
                border-radius: 6px; padding: 6px 9px; font-size: 14px; font-family: inherit; }
.landing-lang:focus-visible { outline: 2px solid var(--accent-gold); outline-offset: 2px; }

.landing { max-width: 900px; margin: 0 auto; padding: 24px 20px 72px; }
.landing-hero { text-align: center; }
.landing-hero h1 { font-family: var(--font-display); color: var(--accent); font-size: 40px; margin: 0 0 10px; }
.landing-tagline { color: var(--fg); font-size: 18px; max-width: 46ch; margin: 0 auto 26px; line-height: 1.6; }
.landing-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/*
 * Call-to-action buttons. These are <a> elements, so every colour is stated explicitly —
 * including :visited. Without that they inherit the browser's link colours (and a dimmer
 * visited colour once clicked), which is exactly how they lost contrast after a first visit.
 * Styled to match the app's existing `button.primary` (accent fill, background-coloured text)
 * so the front door looks like the product behind it.
 */
.landing-btn {
    display: inline-block;
    padding: 12px 26px;
    border-radius: 8px;
    font-family: inherit;
    font-size: 16px;
    font-weight: 700;
    text-decoration: none;
    border: 1px solid transparent;
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.landing-btn-primary,
.landing-btn-primary:link,
.landing-btn-primary:visited { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.landing-btn-primary:hover,
.landing-btn-primary:focus-visible { background: var(--accent-gold); border-color: var(--accent-gold); color: var(--bg); }

.landing-btn-secondary,
.landing-btn-secondary:link,
.landing-btn-secondary:visited { background: transparent; color: var(--accent); border-color: var(--accent); }
.landing-btn-secondary:hover,
.landing-btn-secondary:focus-visible { background: rgba(var(--gold-rgb), 0.14); color: var(--accent-gold); border-color: var(--accent-gold); }
.landing-btn:focus-visible { outline: 2px solid var(--accent-gold); outline-offset: 3px; }

.landing-signin { margin: 18px 0 0; font-size: 14px; color: var(--muted); }
.landing-signin a,
.landing-signin a:link,
.landing-signin a:visited { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.landing-signin a:hover, .landing-signin a:focus-visible { color: var(--accent-gold); }

.landing-points { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
                  gap: 16px; margin-top: 46px; }
.landing-point { background: var(--scrim); border: 1px solid rgba(var(--gold-rgb), 0.22);
                 border-radius: 12px; padding: 18px; backdrop-filter: blur(2px); }
.landing-point-icon { font-size: 26px; line-height: 1; margin-bottom: 8px; }
.landing-point h2 { font-family: var(--font-display); font-size: 17px; color: var(--accent); margin: 0 0 6px; }
.landing-point p { color: var(--fg); font-size: 14px; line-height: 1.6; margin: 0; }

@media (max-width: 600px) {
    .landing-top { padding: 44px 14px 4px; }   /* clear of the fixed corner picker */
    .landing-wordmark { font-size: 1.9rem; }
    .landing-brand-icon { width: 62px; height: 62px; }
    .landing-lang { top: 10px; right: 10px; font-size: 13px; }
    .landing { padding: 18px 16px 60px; }
    .landing-hero h1 { font-size: 30px; }
    .landing-tagline { font-size: 16px; }
    .landing-btn { padding: 12px 20px; font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) { .landing-btn { transition: none; } }
