/* ================================================================
   DRAUGHTS: EMBER & SLATE  v2.0
   Premium Board · 8 Themes + Custom · Lobby · Full Overlay Suite
   Custom Grid System · Reversi Architecture Port
================================================================ */
/* Arial everywhere — no webfont dependency, guaranteed to render (and render
   legibly, in bold) on every desktop/Android/iOS target this game ships to. */

/* ══════════════════════════════════════════════════════════════
   CSS CUSTOM GRID SYSTEM
══════════════════════════════════════════════════════════════ */
.rs-grid          { display: grid; width: 100%; }
.rs-grid-2        { grid-template-columns: repeat(2, 1fr); }
.rs-grid-3        { grid-template-columns: repeat(3, 1fr); }
.rs-grid-4        { grid-template-columns: repeat(4, 1fr); }
.rs-grid-auto     { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
.rs-col-1         { grid-column: span 1; }
.rs-col-2         { grid-column: span 2; }
.rs-gap-xs        { gap: 4px; }
.rs-gap-sm        { gap: 6px; }
.rs-gap-md        { gap: 8px; }
.rs-gap-lg        { gap: 14px; }
.rs-align-center  { align-items: center; }
.rs-align-start   { align-items: start; }

/* ══════════════════════════════════════════════════════════════
   BASE THEME TOKENS  (Ember default)
══════════════════════════════════════════════════════════════ */
:root {
  --font-ui: Arial, "Helvetica Neue", Helvetica, sans-serif;
  --bg:             #1a1410;
  --surface:        #221c16;
  --panel:          #1e1812;
  --brass:          #d4a843;
  --brass-light:    #f0c85a;
  --brass-dark:     #8a6a20;
  --text-primary:   #f0e0c8;
  --text-muted:     #7a6a55;
  --border:         rgba(212,168,67,.2);
  --shadow-deep:    0 20px 60px rgba(0,0,0,.8);
  --radius:         4px;

  /* Board vars */
  --board-light:    #f5e8d0;
  --board-dark:     #7a3810;
  --board-frame:    #5c3010;
  --board-accent:   rgba(212,168,67,.5);

  /* Piece vars */
  --piece-c0: #f0a050;
  --piece-c1: #c87533;
  --piece-c2: #6a2e08;
  --piece-s0: #7090b0;
  --piece-s1: #3d5068;
  --piece-s2: #18222e;

  --score-active-glow: rgba(212,168,67,.2);
}

/* ── Theme: Slate Night ─────────────────────────────────────── */
body[data-theme="slate"] {
  --bg:             #0d0f12;
  --surface:        #141820;
  --panel:          #0f1318;
  --brass:          #8090a8;
  --brass-light:    #a8bcd0;
  --brass-dark:     #405060;
  --text-primary:   #c8d8e8;
  --text-muted:     #4a5a6a;
  --border:         rgba(120,150,180,.2);

  --board-light:    #d0dce8;
  --board-dark:     #2a3848;
  --board-frame:    #1a2430;
  --board-accent:   rgba(120,150,180,.5);

  --piece-c0: #6080a0;
  --piece-c1: #405870;
  --piece-c2: #182030;
  --piece-s0: #d0e0f0;
  --piece-s1: #a8c0d8;
  --piece-s2: #708090;
  --score-active-glow: rgba(120,150,180,.2);
}

/* ── Theme: Forest ──────────────────────────────────────────── */
body[data-theme="forest"] {
  --bg:             #0a100a;
  --surface:        #121a12;
  --panel:          #0e160e;
  --brass:          #6ab050;
  --brass-light:    #90d070;
  --brass-dark:     #3a6030;
  --text-primary:   #d0e8c8;
  --text-muted:     #4a6040;
  --border:         rgba(100,170,80,.2);

  --board-light:    #c8ddb8;
  --board-dark:     #3a5a28;
  --board-frame:    #1e3412;
  --board-accent:   rgba(100,170,80,.5);

  --piece-c0: #d08840;
  --piece-c1: #a05a18;
  --piece-c2: #4a2808;
  --piece-s0: #b0e080;
  --piece-s1: #70a840;
  --piece-s2: #306020;
  --score-active-glow: rgba(100,170,80,.2);
}

/* ── Theme: Arctic ──────────────────────────────────────────── */
body[data-theme="arctic"] {
  --bg:             #080c12;
  --surface:        #0d1520;
  --panel:          #080e18;
  --brass:          #5ab4e8;
  --brass-light:    #88d0ff;
  --brass-dark:     #2a6090;
  --text-primary:   #c8ddf0;
  --text-muted:     #4a6080;
  --border:         rgba(80,160,220,.2);

  --board-light:    #d8eef8;
  --board-dark:     #1a3860;
  --board-frame:    #0a1e38;
  --board-accent:   rgba(80,160,220,.5);

  --piece-c0: #a0c8e8;
  --piece-c1: #5088b0;
  --piece-c2: #1a3858;
  --piece-s0: #e8f8ff;
  --piece-s1: #c0dff8;
  --piece-s2: #80b0d0;
  --score-active-glow: rgba(80,160,220,.2);
}

/* ── Theme: Midnight ────────────────────────────────────────── */
body[data-theme="midnight"] {
  --bg:             #060608;
  --surface:        #0c0c14;
  --panel:          #080810;
  --brass:          #a878e8;
  --brass-light:    #c8a0ff;
  --brass-dark:     #603890;
  --text-primary:   #d8c8f0;
  --text-muted:     #504868;
  --border:         rgba(160,110,220,.2);

  --board-light:    #c8b8e8;
  --board-dark:     #2a1848;
  --board-frame:    #160c28;
  --board-accent:   rgba(160,110,220,.5);

  --piece-c0: #9060d0;
  --piece-c1: #6030a0;
  --piece-c2: #280c48;
  --piece-s0: #f0e8ff;
  --piece-s1: #d8c0f0;
  --piece-s2: #a080c8;
  --score-active-glow: rgba(160,110,220,.2);
}

/* ── Theme: Crimson ─────────────────────────────────────────── */
body[data-theme="crimson"] {
  --bg:             #0e0a0a;
  --surface:        #1a1010;
  --panel:          #140a0a;
  --brass:          #e05a5a;
  --brass-light:    #ff8888;
  --brass-dark:     #8a2020;
  --text-primary:   #f0d8d8;
  --text-muted:     #806060;
  --border:         rgba(220,80,80,.2);

  --board-light:    #f0d0d0;
  --board-dark:     #6a1818;
  --board-frame:    #3a0c0c;
  --board-accent:   rgba(220,80,80,.5);

  --piece-c0: #e08080;
  --piece-c1: #a02828;
  --piece-c2: #480808;
  --piece-s0: #fff0f0;
  --piece-s1: #f0cece;
  --piece-s2: #c89898;
  --score-active-glow: rgba(220,80,80,.2);
}

/* ── Theme: Gold Marble ─────────────────────────────────────── */
body[data-theme="gold"] {
  --bg:             #f5f0e8;
  --surface:        #ede4d4;
  --panel:          #f0e8d8;
  --brass:          #8a6820;
  --brass-light:    #c09840;
  --brass-dark:     #5a4010;
  --text-primary:   #2a1e0a;
  --text-muted:     #8a7850;
  --border:         rgba(138,104,32,.22);

  --board-light:    #f8f0e0;
  --board-dark:     #8a5820;
  --board-frame:    #c09840;
  --board-accent:   rgba(138,104,32,.6);

  --piece-c0: #d4a020;
  --piece-c1: #9a6810;
  --piece-c2: #503008;
  --piece-s0: #2a1e0a;
  --piece-s1: #1a1208;
  --piece-s2: #080400;
  --score-active-glow: rgba(138,104,32,.2);
}

/* ── Theme: Neon Cyberpunk ──────────────────────────────────── */
body[data-theme="neon"] {
  --bg:             #060810;
  --surface:        #0a0d1a;
  --panel:          #060812;
  --brass:          #00ffe0;
  --brass-light:    #80fff0;
  --brass-dark:     #008870;
  --text-primary:   #c0f0e8;
  --text-muted:     #2a6060;
  --border:         rgba(0,255,200,.18);

  --board-light:    #102028;
  --board-dark:     #040810;
  --board-frame:    #020408;
  --board-accent:   rgba(0,255,200,.7);

  --piece-c0: #ff6040;
  --piece-c1: #c02808;
  --piece-c2: #500808;
  --piece-s0: #e0fffa;
  --piece-s1: #00ffe0;
  --piece-s2: #008878;
  --score-active-glow: rgba(0,255,200,.25);
}

/* ══════════════════════════════════════════════════════════════
   RESET + BASE
══════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Round 35 — form controls (<button> here; no <input>/<select>/<textarea>
   in this project) get their own UA-default line-height ("normal") that
   does NOT inherit from the page the way every other element's does, even
   though every button class in this project already explicitly overrides
   font-family/font-size (undoing the same UA default for those two) —
   line-height was the one property nobody had to touch yet, since
   "normal" only becomes visibly wrong for CJK text (see the html/body
   comment below for why). Deliberately narrower than the common `font:
   inherit` reset: that would ALSO make an unset button font-size inherit
   properly, which .lobby-icon-btn/.icon-btn currently do NOT do on
   purpose (their own em-based sizing math was tuned around the UA
   default staying fixed — see CLAUDE.md round 30) — changing that here
   would silently break that math. line-height alone carries no such
   assumption anywhere in the project. */
button { line-height: inherit; }

/* All type in this stylesheet is sized in rem, so bumping the root here
   scales every label/button/title uniformly (~+20%) in one place. */
html { font-size: 144%; }

html, body {
  width: 100%; height: 100%;
  /* Round 46 (iOS Safari bug report — "game scrolls, bottom buttons not
     visible", reproduced specifically on iPhone with the address bar
     docked at the bottom, not seen on Android/desktop): percentage height
     here resolves against the viewport in the same way the old `100vh`
     unit did — historically (and still, on iOS Safari) that means the
     LARGE viewport, i.e. as if the browser's own address-bar chrome were
     already hidden, even while it's actually still on screen and eating
     into the real visible area. #app (the game scene) stretches to fill
     this height (align-self:stretch, round 19), so it ends up genuinely
     taller than what's actually visible whenever that chrome is showing —
     #app's own overflow-y:auto safety net (also round 19, meant for a
     rare short/wide desktop window) then honestly kicks in, except here
     the "missing" room isn't real, it's the address bar, so the game
     reads as needing a scroll to reach its own bottom buttons instead of
     them just fitting like they do everywhere else. Fix: `100dvh` — the
     dynamic viewport height unit tracks the browser chrome live, so this
     resolves to the ACTUAL visible height at all times. Layered as a
     second declaration after the 100% one (not a replacement) so browsers
     that don't understand `dvh` simply ignore this line and keep the
     100% fallback — no @supports needed. */
  height: 100dvh;
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-weight: 600;
  /* Explicit default (round 35) — "normal" (the UA default, unset before)
     resolves against whichever font actually renders a given run of text,
     and Arial (the only font in --font-ui) has no CJK glyphs at all: for
     Japanese, every browser silently substitutes a system Japanese font,
     and that font's own "normal" line metrics run much taller than
     Arial's — several small labels across the lobby/game scene (score
     panel, header title, status text, lobby buttons/labels) were
     rendering visibly taller in Japanese than in any other language
     purely from this, not from the text itself needing more room, and it
     was enough extra height in aggregate to reintroduce real scrolling in
     both scenes. A fixed value here is inherited everywhere that doesn't
     set its own line-height, closing the gap at the root instead of
     patching each affected selector individually. Elements that
     deliberately need a different value (e.g. .score-num, always Latin
     digits) still set their own and override this. */
  line-height: 1.2;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background .4s, color .4s;
}

/* Round 55 (perf fix — see the long comment on ThemeSystem.apply() in
   script.js for the full diagnosis): suppresses every transition, project-
   wide, for the one instant a theme switch is landing — turns what would
   otherwise be dozens of elements animating border-color/background-color/
   box-shadow simultaneously over .2-.4s (a real, continuous repaint load
   that stutters on weak Android GPUs) into a single, instant repaint
   instead. `*` and `*::before`/`*::after` (not just the class itself) are
   needed since several of the actual slow elements are the ones with
   `transition: all` set on themselves, and pseudo-elements like
   .primary-btn::before/.lobby-play-btn::before carry their own separate
   transitions too. Toggled on/off around the data-theme attribute change
   only (script.js) — ordinary hover/selection transitions on buttons are
   completely unaffected the rest of the time. */
.theme-switching, .theme-switching *, .theme-switching *::before, .theme-switching *::after {
  transition: none !important;
}

/* This is a game, not a page — no selecting text/pieces, no dragging
   images or the board out of the window (a stray drag/select from a fast
   click or swipe shouldn't ever highlight text or pick up an image mid-
   move). Mirrors the existing contextmenu-disable in script.js bootstrap.
   -webkit-tap-highlight-color (round 38): mobile WebKit/Blink browsers
   flash a translucent blue overlay over whatever was tapped, by default —
   same "not a page" reasoning applies to that as to selection/callout.
   touch-action:manipulation (round 54, direct request — "double-tap zooms
   the whole page in on iPhone, kill it everywhere"): the viewport meta's
   user-scalable=no (index.html) alone doesn't reliably stop this on modern
   iOS Safari — Safari has ignored user-scalable=no for pinch-zoom since
   iOS 10 (an Apple accessibility decision, not a bug), and double-tap-to-
   zoom rides on the same "zoom is always allowed" behavior, meta tag or
   not. touch-action:manipulation is the actual fix: it tells the browser
   this element only needs pan/scroll gestures, not double-tap-zoom or
   pinch-zoom, and is honored by every current mobile browser regardless
   of the meta viewport. Deliberately NOT touch-action:none — that would
   also kill plain one-finger scrolling/panning, which #lobby-screen and
   #app both still rely on as their overflow-y:auto safety net (rounds 6/
   19) on the rare cramped screen; manipulation keeps that path open and
   only removes the double-tap/pinch-zoom gestures specifically. */
* {
  -webkit-user-select: none; -moz-user-select: none; user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
img, svg, canvas {
  -webkit-user-drag: none; user-drag: none;
}

body::before {
  content: '';
  position: fixed; inset: 0;
  background:
    radial-gradient(ellipse 70% 50% at 50% 0%, rgba(180,90,20,.15) 0%, transparent 70%),
    radial-gradient(ellipse 50% 40% at 20% 100%, rgba(200,117,51,.08) 0%, transparent 60%);
  pointer-events: none;
  transition: background .4s;
}

/* ══════════════════════════════════════════════════════════════
   BROWSER COMPATIBILITY SCREEN
══════════════════════════════════════════════════════════════ */
/* display:none by DEFAULT (round 36) — previously this screen was visible
   by default and JS (BrowserCompat.run(), first line of bootstrap) added
   `.hidden` to hide it on any browser that passes every check (the
   overwhelming majority of real visits). Since bootstrap only runs once
   the script has loaded/parsed, there was a real window — worse on a slow
   connection/device — where the raw page had already painted with this
   screen (and its "Browser Incompatible" copy) visible, before the script
   caught up and hid it: a startup flash with no failure to report, purely
   a race between paint and script execution. Inverting the default (only
   `.show`, added when a REAL failure is found, ever reveals it) makes the
   passing case — nearly everyone — never paint this screen at all, at any
   connection speed, instead of trying to win a timing race. */
#compat-screen {
  display: none;
  position: fixed; inset: 0;
  /* Round 52: see the #lobby-screen rule further down for why every
     position:fixed;inset:0 full-screen container in this file needs its
     own explicit height:100dvh, not just html/body's (round 46). */
  height: 100dvh;
  background: var(--bg);
  align-items: center;
  justify-content: center;
  z-index: 9999;
  flex-direction: column;
  text-align: center;
  padding: 40px 24px;
}
#compat-screen.show { display: flex; }
.compat-inner { max-width: 420px; width: 100%; }
.compat-icon {
  width: 64px; height: 64px;
  margin: 0 auto 20px;
  border: 2px solid var(--brass);
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--brass);
  font-size: 1.6rem;
  box-shadow: 0 0 30px rgba(212,168,67,.2);
}
.compat-title {
  font-family: var(--font-ui); font-size: 1.5rem; font-weight: 700;
  letter-spacing: .1em; color: var(--brass); margin-bottom: 8px;
  text-shadow: 0 0 30px rgba(212,168,67,.4);
}
.compat-subtitle {
  font-size: .85rem; font-style: italic;
  color: var(--text-muted); letter-spacing: .15em; margin-bottom: 24px;
}
.compat-message { font-size: .95rem; color: var(--text-primary); line-height: 1.7; margin-bottom: 24px; }
.compat-browsers { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; margin-bottom: 28px; }
.compat-browser-chip {
  padding: 8px 16px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); color: var(--text-muted);
  font-family: var(--font-ui); font-size: .6rem;
  letter-spacing: .15em; text-transform: uppercase;
}
.compat-divider { display: flex; align-items: center; gap: 12px; margin: 0 0 20px; }
.compat-divider-line { flex: 1; height: 1px; background: var(--border); }
.compat-divider-gem  { width: 5px; height: 5px; background: var(--brass-dark); transform: rotate(45deg); }
.compat-detail {
  font-size: .78rem; color: var(--text-muted); line-height: 1.6;
  text-align: left; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 12px 16px;
}
.compat-detail-title {
  font-family: var(--font-ui); font-size: .55rem;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted); margin-bottom: 6px;
}

/* ══════════════════════════════════════════════════════════════
   SPLASH SCREEN
   Shown once before the lobby — see the Splash module in script.js for
   the fade-in/tap-to-continue sequence (same interaction pattern as the
   sibling project "Kids Coloring World": bg+logo fade in on load, then a
   pulsing "tap" prompt appears after a short delay and gates the click-
   through). One deliberate difference from that reference, per explicit
   request: there the splash spans the full physical viewport (100vw/vh);
   here it's capped to the game's own width (.splash-inner max-width),
   matching #lobby-screen/.lobby-inner — same idea as "the game has its
   own box, not the physical screen" used everywhere else in this project.
══════════════════════════════════════════════════════════════ */
#splash-screen {
  position: fixed; inset: 0;
  /* Round 52: see the #lobby-screen rule further down for why every
     position:fixed;inset:0 full-screen container in this file needs its
     own explicit height:100dvh, not just html/body's (round 46). */
  height: 100dvh;
  background: var(--bg);
  z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  opacity: 1; transition: opacity .4s;
}
#splash-screen.hidden { opacity: 0; pointer-events: none; }
.splash-inner {
  position: relative;
  width: 100%; max-width: 460px; height: 100%;
  overflow: hidden;
}
.splash-bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1.5s ease;
}
.splash-bg.show { opacity: 1; }
.splash-logo {
  position: absolute; left: 50%;
  /* Same guaranteed top clearance used everywhere else (#lobby-screen,
     #app) — a bit more buffer here since the logo itself has visual
     weight and shouldn't hug a notch/status-bar. +7% of the container's
     own height on top of that guarantee (round 30: +15% to move it lower;
     round 31: raised back up by 8 points, to +7%, per direct request). */
  top: calc(max(28px, env(safe-area-inset-top) + 16px) + 7%);
  /* Originally "screen width -15% each side" = 70% of the container's own
     width (the container being .splash-inner (game width), not the
     physical viewport, per the same width convention as the rest of this
     screen) — enlarged +10% (round 33): 70% * 1.1 = 77%; +15% (round 38):
     77% * 1.15 = 88.55%; +15% again (round 39, direct request): 88.55% *
     1.15 = 101.83%. This now exceeds .splash-inner's own width — since
     that container has overflow:hidden (see .splash-inner), the logo's
     left/right edges clip by a small, symmetric amount (the image is
     centered via left:50%/translateX(-50%)) rather than spilling out past
     the game's own box. Checked by eye — the clipped sliver is small
     enough not to lose any part of the wordmark that matters. -5% (round
     40, direct request): 101.83% * .95 = 96.7385%. */
  width: 96.7385%;
  transform: translateX(-50%) translateY(10px) scale(.98);
  filter: drop-shadow(0 8px 24px rgba(0,0,0,.6));
  opacity: 0;
  transition: opacity 1.5s ease, transform 1.5s ease;
}
.splash-logo.show { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
.splash-tap {
  position: absolute; left: 50%; bottom: 8%;
  width: 80%;
  transform: translateX(-50%);
  text-align: center;
  font-family: var(--font-ui); font-weight: 700;
  font-size: 1rem; letter-spacing: .15em; text-transform: uppercase;
  color: #fff;
  /* Crisp 1px black outline around the letters themselves (not a
     rectangular box around the whole element — that's what plain CSS
     `outline` would draw) — paint-order keeps the stroke from eating into
     the white fill. Kept the soft drop-shadow too, for depth/legibility
     against the busier parts of the splash artwork. */
  -webkit-text-stroke: 1px #000;
  paint-order: stroke fill;
  text-shadow: 0 2px 14px rgba(0,0,0,.85);
  opacity: 0;
  transition: opacity .5s ease;
}
.splash-tap.show { opacity: 1; animation: splash-tap-pulse 1.4s ease-in-out infinite; }
@keyframes splash-tap-pulse {
  0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); }
  50%      { opacity: .55; transform: translateX(-50%) scale(1.06); }
}

/* ══════════════════════════════════════════════════════════════
   ORIENTATION LOCK SCREEN
   Pure CSS media-query toggle (no JS class) — held in landscape only
   on touch/mobile devices; desktop windows are never affected.
   Gated on (hover:none) and (pointer:coarse) rather than a max-width
   px threshold — a real phone in landscape (e.g. Galaxy S20 Ultra,
   ~915px CSS width) can be WIDER than any width cutoff we'd pick, so
   a width-based query let real phones slip through undetected (round
   15 bug). pointer/hover reflect the device's actual input capability
   instead, so it fires at any resolution — matches how sibling games
   (Kids Coloring World, drift-boss) gate this on touch capability too,
   just via CSS media features here instead of a JS UA/touch-points check.
   script.js mirrors this exact query via matchMedia() to pause music
   while it's showing (see OrientationGuard) — keep the two in sync.
══════════════════════════════════════════════════════════════ */
#orientation-screen {
  display: none;
  position: fixed; inset: 0;
  /* Round 52: see the #lobby-screen rule further down for why every
     position:fixed;inset:0 full-screen container in this file needs its
     own explicit height:100dvh, not just html/body's (round 46). */
  height: 100dvh;
  /* Matches the flat black background baked into Orientation.png itself,
     intentionally not var(--bg) — this screen is theme-agnostic. */
  background: #000;
  z-index: 9500;
  align-items: center; justify-content: center;
}
#orientation-screen img {
  max-width: 90vw; max-height: 90vh;
  width: auto; height: auto;
}
@media (orientation: landscape) and (hover: none) and (pointer: coarse) {
  #orientation-screen { display: flex; }
}

/* ══════════════════════════════════════════════════════════════
   APP LAYOUT
══════════════════════════════════════════════════════════════ */
#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  width: 100%;
  max-width: 520px;
  /* Fill the full viewport height instead of being centered as one block
     by body's align-items:center (round 19 — same move as the lobby's
     round 7 fix). Centering left a big dead gap above the header on tall
     phones; stretching + space-between (below) pulls the header up near
     the guaranteed top clearance and lets any leftover height distribute
     between sections instead of piling up on one side. */
  align-self: stretch;
  justify-content: space-between;
  /* Safety net if content still doesn't fit even at the smallest end of
     the fluid clamp below (mirrors #lobby-screen's overflow-y:auto) — a
     scrollbar beats silently clipping the board or status bar. */
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Same guaranteed top clearance as the lobby (see #lobby-screen) — the
     header's icon row must never sit closer than 25px to the true top
     edge on any device, notch or not. The 25px is intentionally NOT em —
     it's a physical safe-area guarantee, independent of the fluid scale
     below. Side/bottom padding are em so they shrink with everything else.
     Round 40: side padding floored at 6px (max(.434em, 6px)) — .btn-restart
     shares this same .434em for its own left offset, but being a <button>
     with no explicit font-size of its own, that em resolves against the
     browser's UA-default button font-size (~13.33px, see the round 30/36
     write-ups on this bug), not against #app's clamp here — so its offset
     is effectively a fixed ~5.8px, regardless of viewport. At the clamp's
     low end (narrow/short mobile screens) this side padding could shrink
     to LESS than that fixed 5.8px, letting the button poke a few px past
     #app's own content edge — real, if small, horizontal overflow on
     mobile. Flooring this padding at 6px keeps it always at least as big
     as the button's fixed need, independent of the clamp, without
     touching the button itself (which would also resize its fixed ~52px
     width/height, a much bigger, unintended change — see those same round
     30/36 notes on why that's deliberately left alone). */
  padding: max(25px, env(safe-area-inset-top)) max(.434em, 6px) .5208em;
  position: relative;
  /* Round 57 (direct request — status text must always stay Y-aligned with
     .btn-restart): --chrome-e mirrors this element's own font-size clamp as
     a custom property. Custom properties (unlike font-size) ARE inherited
     by <button> descendants even though UA stylesheets reset a button's own
     font — so .btn-restart can read this exact scale via calc(var(--chrome-e)
     * ...) instead of its own em (which silently resolves against the
     button's fixed UA-default font-size, not this clamp — see the round
     30/36/38/40 write-ups on why). This was the root cause measured here:
     #app's own bottom padding (.5208em, scales with this clamp) and
     .btn-restart's old bottom offset (.5208em, effectively FIXED px because
     buttons don't inherit font-size) drifted apart from each other as the
     clamp shrank on shorter/narrower desktop windows, pulling the button
     and the status text (which sits in normal flow, positioned by that same
     scaling padding) out of alignment by a different amount at every size —
     not by a constant, which is why a single fixed nudge (the approach taken
     in the Reversi Collection reference project, where the equivalent gap
     WAS constant) wouldn't have worked here on its own. */
  --chrome-e: clamp(9px, min(4.35vw, 3.3vh), 23.04px);
  /* Fluid local type-scale for the header/score-panel/status-bar "chrome"
     around the board — same technique as .lobby-inner (round 6), applied
     here because the header row (title + 3 icon buttons) has a fixed
     min-content width that doesn't shrink on its own and can overflow off
     the right edge of narrow phones (round 17 bug report — icons partially
     or fully off-screen at ~240-320px CSS width). The board itself already
     scales independently via #board-wrap's width:100%/aspect-ratio, so it
     isn't part of this scale — only the em-sized chrome below is.
     min(vw-term, vh-term): the vw-term is what actually saves narrow
     phones (portrait phones always have height > 1.454×width, so the
     vw-term is the smaller/binding one there); the vh-term reproduces the
     old max-height:700px breakpoint's shrink for short, wide desktop
     windows, continuously instead of as a step. 23.04px max matches the
     lobby's reference point (1rem at the global 144% scale), so nothing
     shrinks on generously sized screens vs. before this round. */
  font-size: clamp(9px, min(4.35vw, 3.3vh), 23.04px);
}
/* Without this, #app's default flex-shrink:1 lets the browser silently
   compress a child (most likely #board-wrap, which has overflow rules of
   its own) below its natural size instead of triggering #app's scrollbar
   above — the exact bug that hit the lobby in round 11. */
#app > * { flex-shrink: 0; }

/* ══════════════════════════════════════════════════════════════
   HEADER
══════════════════════════════════════════════════════════════ */
#header {
  width: 100%;
  display: flex; align-items: center; justify-content: flex-end;
  padding: 0 .1736em .434em;
  border-bottom: 1px solid var(--border);
  margin-bottom: .6076em;
}
/* Round 42 (direct request): the header logo (rounds 36/39/40/41 — text
   title -> logo image -> repeatedly enlarged) is removed entirely, and
   Fullscreen moved out of .header-controls to sit alone on the left in its
   old place — full-removal pattern (see CLAUDE.md section 3/11), not just
   hidden: the wrapping div, the <img>, and all of their CSS (previously
   .header-title-wrap/.header-logo, sized/repositioned across 4 rounds) are
   gone, not dead-code-kept.
   Round 46 (iOS bug report): #header used to be justify-content:
   space-between with exactly two flex children (this button, then
   .header-controls) — fine while both are visible, but FullscreenManager
   hides this button outright (display:none) on iOS (see its isSupported()/
   isIOS() check, section 3), and space-between with a SINGLE remaining
   flex child places that lone child at the flex start (left), not where
   it visually was — so on iOS specifically, Undo/Pause visibly jumped to
   the left edge instead of staying pinned to the right where every other
   platform (Fullscreen visible) shows them. Fixed structurally: #header is
   now justify-content:flex-end (so .header-controls always hugs the right
   edge, with or without a sibling), and this button gets its own
   margin-right:auto instead — when present, that auto margin eats all the
   space between it and .header-controls (visually identical to the old
   space-between); when display:none, a hidden element contributes no
   margin at all, so .header-controls is simply the only flex child left,
   flush right via flex-end. */
.header-left-btn { flex-shrink: 0; margin-right: auto; }
.header-controls { display: flex; gap: 1.6493em; align-items: center; flex-shrink: 0; }
.icon-btn {
  /* +50% in round 18 (2.0833em -> 3.125em), +25% more in round 19 -> 3.9063em. */
  width: 3.9063em; height: 3.9063em;
  background: transparent;
  border: 1px solid var(--border); border-radius: var(--radius);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted); transition: all .2s;
  flex-shrink: 0;
}
.icon-btn:hover { border-color: var(--brass); color: var(--brass); background: rgba(212,168,67,.06); }
.icon-btn svg { width: 1.7905em; height: 1.7905em; }

/* ══════════════════════════════════════════════════════════════
   SCORE PANEL
══════════════════════════════════════════════════════════════ */
#score-panel {
  width: 100%;
  display: grid; grid-template-columns: 1fr auto 1fr;
  gap: .3472em; margin-bottom: .4em; align-items: center;
}
/* Round 40: padding/disc/number shrunk (see .score-disc/.score-num below)
   specifically to reclaim vertical space in the header/score-panel "chrome"
   above the board — on a short/wide desktop window the fixed-height board
   (up to 480px, doesn't shrink with viewport height) plus this chrome could
   together push the board low enough that the corner button(s) — at the
   time, .btn-restart and .btn-cheat-win, both pinned to a fixed spot near
   #app's own bottom edge; .btn-cheat-win was later removed entirely in
   round 46, see CLAUDE.md — ended up overlapping the board's bottom edge —
   direct bug report, "at least 5px" gap requested. Permission was
   explicitly given to shrink this component specifically for that room.
   Round 41: this was meant to be PC-only from the start but had been left
   as an unscoped base rule, so it also (unintentionally) shrank the mobile
   score cards — reverted on mobile via the @media(max-width:480px) block
   below (see its own comment). Separately, on PC, #board-frame's decorative
   overhang (round 40's -.434em inset around #board-wrap, see #board-frame)
   turned out to reach up far enough to overlap #score-panel's own bottom
   edge at several common short-window sizes (1280x720 included) — #app's
   overflow-y:auto safety net doesn't catch this because #board-wrap itself
   never actually overlapped anything, only its decorative frame sibling
   did. margin-bottom bumped .28em -> .4em to clear it, combined with
   #board-wrap's own new vh-based shrink (see #board-wrap) so this doesn't
   simultaneously eat back into the buttons' clearance below the board —
   the two gaps (panel-to-frame above the board, board-to-buttons below it)
   pull against each other through the board's own position, so both had to
   be re-tuned together rather than raising this margin alone. */
.score-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius);
  /* Round 45, direct request: block HEIGHT +15% on PC only (mobile keeps
     its own separate padding value in the @media(max-width:480px) override
     below, untouched) — only the vertical padding component moved
     (.2em -> .23em), horizontal padding/disc/number sizes left exactly as
     they were, since the request was specifically about height, not the
     block's overall size. */
  padding: .65em .4em;
  display: flex; align-items: center; justify-content: center; gap: .434em;
  transition: border-color .3s, box-shadow .3s;
  position: relative; overflow: hidden;
}
.score-card.active {
  border-color: var(--brass);
  box-shadow: 0 0 16px var(--score-active-glow), inset 0 0 20px rgba(212,168,67,.04);
}
.score-disc {
  width: .68em; height: .68em; border-radius: 50%; flex-shrink: 0;
}
.score-disc.copper {
  background: radial-gradient(circle at 35% 30%, var(--piece-c0), var(--piece-c2));
  box-shadow: 0 3px 8px rgba(0,0,0,.6), inset 0 1px 3px rgba(255,200,100,.3);
}
.score-disc.slate {
  background: radial-gradient(circle at 35% 30%, var(--piece-s0), var(--piece-s2));
  box-shadow: 0 3px 8px rgba(0,0,0,.6), inset 0 1px 3px rgba(150,180,220,.25);
}
/* Round 37: was flex:1 (grows to fill the card), which combined with
   text-align defaulting to left made the disc+number+label group hug the
   card's left edge instead of sitting centered in it. Sized to its own
   content now — .score-card's new justify-content:center centers the
   whole dot+disc+info group as a unit. min-width:0 kept so it can still
   shrink instead of overflowing if a translated label is ever too wide
   for the card at a narrow viewport. */
/* Round 43, direct request: the "pieces"/"шашек" label under the number
   removed entirely (full-removal pattern — see CLAUDE.md section 3/11),
   not just hidden; .score-info now wraps only .score-num. text-align:center
   added here since .score-info no longer has a wider sibling to visually
   anchor to (min-width:0 kept — a translated/long number in some edge case
   still shouldn't force the card wider than the card's own layout allows). */
.score-info { min-width: 0; text-align: center; }
.score-num  { font-family: var(--font-ui); font-size: 1em; font-weight: 700; color: var(--text-primary); line-height: normal; }
.score-sep  { font-family: var(--font-ui); font-size: .7em; color: var(--text-muted); text-align: center; }

/* ══════════════════════════════════════════════════════════════
   BOARD
══════════════════════════════════════════════════════════════ */
#board-wrap {
  position: relative; width: 100%; aspect-ratio: 1;
  /* Round 41 (PC bug report — score-panel getting overlapped by the board):
     was a flat 480px, which never shrinks on a short/wide desktop window no
     matter how little vertical room #app actually has — see the vh term
     added here. Doesn't change anything on typical/tall windows: 63vh only
     drops below 480px once viewport height is under ~762px. */
  max-width: min(480px, 58vh);
}
.board-corner {
  position: absolute; width: 18px; height: 18px;
  border-color: var(--brass-dark); border-style: solid;
  opacity: .7; z-index: 3;
}
.board-corner.tl { top: -2px; left: -2px;   border-width: 2px 0 0 2px; }
.board-corner.tr { top: -2px; right: -2px;   border-width: 2px 2px 0 0; }
.board-corner.bl { bottom: -2px; left: -2px; border-width: 0 0 2px 2px; }
.board-corner.br { bottom: -2px; right: -2px;border-width: 0 2px 2px 0; }

/* Wood frame */
#board-frame {
  /* Round 40: was a flat -10px — on narrow mobile widths #board-wrap sits
     close enough to #app's own edge that this fixed overhang poked past
     the viewport (horizontal scroll). -10px == -.434em at #app's clamp
     max (23.04px), so switching to em reproduces the exact old look at
     full size and shrinks safely in lockstep with #app's own padding on
     narrow screens instead of overflowing it. #board-frame is a plain div
     (not a <button>), so em here correctly resolves against #app's own
     fluid font-size clamp — unlike .btn-restart, which is a button and
     doesn't inherit it (see round 30/36/38 write-ups). */
  position: absolute; inset: -.434em;
  background: linear-gradient(135deg, var(--board-frame) 0%, color-mix(in srgb, var(--board-frame) 60%, #000) 40%, color-mix(in srgb, var(--board-frame) 80%, #fff 10%) 60%, color-mix(in srgb, var(--board-frame) 60%, #000) 100%);
  border-radius: 6px;
  /* Round 35: dropped the outer drop-shadow (var(--shadow-deep), a
     downward-offset shadow shared with .dialog elsewhere) per direct
     request to remove the visible shadow under the board — the inset
     stays, that's the frame's own inner depth shading, not a cast shadow. */
  box-shadow: inset 0 0 30px rgba(0,0,0,.4);
  z-index: 0;
  transition: background .4s;
}
#board-frame::before {
  content: '';
  position: absolute; inset: 0;
  background: repeating-linear-gradient(88deg, transparent 0px, transparent 3px, rgba(0,0,0,.04) 3px, rgba(0,0,0,.04) 4px);
  border-radius: 6px;
}

#board-wrap::before {
  content: '';
  position: absolute; inset: -4px;
  border-radius: 4px;
  box-shadow: 0 0 40px rgba(0,0,0,.8), 0 0 0 1px rgba(212,168,67,.08), inset 0 0 60px rgba(0,0,0,.4);
  pointer-events: none; z-index: 2;
}

#board-canvas {
  position: relative; z-index: 1;
  display: block; width: 100%; height: 100%;
  cursor: pointer; border-radius: 2px;
  /* Round 35: same request as #board-frame just above — dropped this
     element's own downward-offset shadows too (0 8px / 0 2px), which
     stacked with the frame's to make the shadow under the board visible
     even after removing the frame's. Inset highlight kept. */
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.04);
}

/* ══════════════════════════════════════════════════════════════
   STATUS BAR
══════════════════════════════════════════════════════════════ */
#status-bar {
  width: 100%; display: flex; align-items: center; justify-content: center;
  gap: .3472em; margin-top: .6076em; min-height: 1.2153em;
}
/* Round 38 (mobile only at first): #status-bar sits in normal flex flow
   (part of #app's column, spaced out by justify-content:space-between),
   while .btn-restart (and, until round 46 removed it, .btn-cheat-win too)
   is pinned to a fixed distance from #app's own bottom edge — the two end
   up with roughly matching BOTTOM edges, but the button is a fixed ~52px
   tall (.icon-btn is stuck at a
   UA-default font-size that never scales with #app's clamp, see the round
   30/36 write-ups) while the status bar is only about one line tall, so
   their vertical CENTERS don't line up: the status text visibly sits
   below the buttons' center. Matching #status-bar's own min-height to
   that same fixed ~52px (rather than an em value, which would track
   #app's clamp and drift back out of alignment), plus 5px, — combined with
   its existing align-items:center — centers the text at the same Y as the
   buttons instead of hugging the bottom of a shorter box.
   Round 40: bumped from mobile-only 52px to 57px (+5px) and applied
   everywhere (no longer scoped to the mobile breakpoint) purely to keep
   this Y-alignment correct at every screen size -- this does NOT fix
   board/button overlap (that turned out to need a different fix, see the
   .score-card/.score-disc/.score-num shrink in the SCORE PANEL section,
   round 40 CLAUDE.md entry for why). Verified empirically that
   .btn-restart (position:absolute within #app, which is #app's own
   overflow-y:auto scrolling container) and #board-wrap (normal
   flow) shift by the exact same amount when #app scrolls -- their
   relative gap is constant regardless of scroll position, so making
   anything AFTER them in flow (like #status-bar) taller cannot ever
   convert an overlap between the board and the buttons into a scrollbar;
   it can only affect what comes after them. */
#status-bar { min-height: 57px; }
/* Round 57: after fixing .btn-restart's own clamp-dependent drift (see the
   --chrome-e comment on #app above), a small CONSTANT ~2.47px gap remained
   at every single size measured (desktop and mobile alike) — the button's
   real rendered height is a few px short of this box's 57px min-height
   (that 57px was deliberately rounded up "plus 5px" in round 40, not
   measured to the pixel), so align-items:center still lands the text's
   center slightly above the button's. A plain transform (not a margin/
   padding/height change) corrects it without touching layout at all — it
   can't affect #app's scrollHeight or create scroll, only repaints this
   box a couple px lower. Verified via getBoundingClientRect() center
   comparison across 12 sizes (1920x1080 down to 320x568): diff was a
   near-constant -2.46/-2.47px everywhere before this nudge, ~0 after. */
#status-text { transform: translateY(2.47px); }
#status-text {
  font-family: var(--font-ui);
  font-size: .95em; font-style: italic;
  color: var(--text-muted); letter-spacing: .05em;
  text-align: center; transition: color .3s;
}
#status-text.highlight { color: var(--brass); }

/* ══════════════════════════════════════════════════════════════
   TOAST — transient top-of-screen notification (round 39)
   Pattern researched from the reference project Reversi Collection's
   "not enough coins" toast: fixed, centered, fades/slides in via a toggled
   class + CSS transition, auto-dismissed by a JS timer (see Toast.show in
   script.js) that resets on every call rather than queuing multiple toasts.
   Capped to the game's own box width (like every other overlay in this
   project — .lobby-inner/.splash-inner/.dialog all do the same), not the
   physical viewport; centers correctly via plain left:50% because body
   already centers #app in the viewport the same way, so the two centers
   coincide without any extra containment math.
   z-index:150 is deliberately modest — below #lobby-screen (200) and
   .overlay (300), same "never outrank the lobby" rule as .btn-restart
   (round 27) — a toast can only ever be triggered from
   inside active gameplay (Game.onCellClick bails out while paused), so it
   never needs to compete with the lobby or a popup for visibility; if one
   happens to still be showing when Pause opens, the popup should win, not
   the transient toast underneath it. ══════════════════════════════════════════════════════════════ */
.toast {
  position: fixed; left: 50%;
  top: max(10px, env(safe-area-inset-top));
  transform: translate(-50%, -12px);
  z-index: 150;
  max-width: min(92vw, 460px);
  /* Round 44 (direct bug report — unreadable on the "gold" theme): was a
     flat rgba(10,8,6,.95), a near-black that never changes with the theme,
     while the text color IS theme-aware (var(--text-primary)) — on every
     theme but "gold" that text is a light color, so it read fine against
     this fixed dark background purely by accident; "gold" is the one
     light-background theme (see its --text-primary further up), so its
     dark text landed on this same near-black box and became almost
     invisible. Switched to var(--surface) — the same theme-aware surface
     .dialog itself sits on, so this box's contrast against its own text
     tracks the theme exactly like every popup already does, on all 8. */
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-deep);
  padding: .6em .9em;
  color: var(--text-primary); font-family: var(--font-ui); font-size: .85rem;
  text-align: center; line-height: 1.3;
  opacity: 0; pointer-events: none;
  transition: opacity .25s ease, transform .25s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ══════════════════════════════════════════════════════════════
   RESTART BUTTON — bottom-left corner of the game scene
   Reuses .icon-btn for identical size/style to the header icons, per
   direct request. Positioned absolute within #app (already
   position:relative) rather than fixed to the viewport, and kept to a
   low z-index (#app's own children top out around 3, e.g. .board-corner)
   — nowhere near #lobby-screen's (200) or .overlay's (300). A reference
   project's equivalent button used position:fixed with z-index:500 on
   its mobile breakpoint, which is HIGHER than its own menu screen's
   z-index — since a fixed-position element's stacking is compared against
   the whole document rather than contained by its non-stacking-context
   parent, that let the button visibly float over the main menu even
   though the game screen itself stayed correctly hidden underneath. Only
   ever raise this z-index if you also re-check it against every overlay
   z-index in the project (see CLAUDE.md round 27).
══════════════════════════════════════════════════════════════ */
.btn-restart {
  position: absolute;
  /* Round 57: left/bottom switched from .434em/.5208em (resolved against
     this <button>'s own fixed UA-default font-size, NOT #app's clamp — see
     the --chrome-e comment on #app) to calc() against that custom property
     instead — now tracks #app's own padding scale exactly, the fix this
     project's round 40 write-up already anticipated for this exact bug
     class ("raise a CSS variable with the clamp value on #app, compute the
     button offset via calc(), bypassing button font-size entirely").
     Visually identical to the old value at #app's clamp max (23.04px) —
     only the narrower/shorter end of the clamp range actually changes. */
  left: calc(var(--chrome-e) * .434); bottom: calc(var(--chrome-e) * .5208);
  z-index: 5;
}

/* ══════════════════════════════════════════════════════════════
   OVERLAYS — shared base
══════════════════════════════════════════════════════════════ */
.overlay {
  position: fixed; inset: 0;
  /* Round 52: see the #lobby-screen rule further down for why every
     position:fixed;inset:0 full-screen container in this file needs its
     own explicit height:100dvh, not just html/body's (round 46) — a tall
     popup (e.g. Pause with all its rows) could otherwise render taller
     than the actually-visible iOS Safari viewport and have its lower
     buttons sit behind the browser's own docked-bottom toolbar. */
  height: 100dvh;
  background: rgba(8,6,4,.88);
  display: flex; align-items: center; justify-content: center;
  z-index: 300;
  backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none;
  transition: opacity .4s;
  overflow-y: auto;
  padding: 16px 0;
}
.overlay.show { opacity: 1; pointer-events: all; }

.dialog {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 40px 44px;
  text-align: center;
  max-width: 432px; width: 92%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
  box-shadow: var(--shadow-deep);
  transform: translateY(10px);
  transition: transform .4s;
  flex-shrink: 0;
  margin: auto;
}
.overlay.show .dialog { transform: translateY(0); }

/* dialog corner ornaments */
.dialog::before, .dialog::after,
.dialog .d-corner-br, .dialog .d-corner-bl {
  content: ''; position: absolute;
  width: 14px; height: 14px;
  border-color: var(--brass-dark); border-style: solid;
}
.dialog::before  { top:8px; left:8px;   border-width:1px 0 0 1px; }
.dialog::after   { top:8px; right:8px;  border-width:1px 1px 0 0; }
.dialog .d-corner-br { bottom:8px; right:8px; border-width:0 1px 1px 0; }
.dialog .d-corner-bl { bottom:8px; left:8px;  border-width:0 0 1px 1px; }

.dialog-title {
  font-family: var(--font-ui); font-size: 1.3rem; font-weight: 700;
  color: var(--brass); letter-spacing: .08em;
  margin-bottom: 6px;
  text-shadow: 0 0 20px rgba(212,168,67,.5);
}
/* ── Game Over result discs ── */
/* No .dialog-subtitle and no "VS" separator here (round 35 — removed, see
   CLAUDE.md: matched the Reversi Collection reference popup, which shows
   neither — the title alone already says who won, and the two big score
   numbers already say by how much, so a subtitle restating "X — Y pieces
   remaining" was pure duplication). Title gets its margin back in its
   place, same value/technique the reference uses for the same reason. */
#overlay-gameover .dialog-title { margin-bottom: 30px; }
.result-discs { display: flex; justify-content: center; gap: 87.36px; margin-bottom: 24px; align-items: center; } /* 20px -> 26px (+30%, round 36) -> 35px (+35%, round 38) -> 45.5px (+30%, round 41) -> 87.36px (round 42, direct request: match Reversi Collection's gap exactly, see its own style.css comment — "+20% over 72.8px (which itself was +30% over the original 56px)") */
/* padding-bottom reserves room for .winner-badge below, which is taken out
   of flow entirely (see .winner-badge) — same technique as the reference:
   the badge text is much wider than the loser's empty one, and if it took
   part in flow sizing it'd make the two .result-item columns different
   WIDTHS (align-items:center gives each column only its own content's
   width), pushing the discs out of horizontal symmetry depending on which
   side happens to be winning. */
.result-item  { display: flex; flex-direction: column; align-items: center; gap: 8px; position: relative; padding-bottom: 1.3em; }
.result-disc-large { width: 44px; height: 44px; border-radius: 50%; }
.result-disc-large.copper {
  background: radial-gradient(circle at 35% 30%, var(--piece-c0), var(--piece-c2));
  box-shadow: 0 6px 20px rgba(0,0,0,.7), inset 0 2px 4px rgba(255,200,100,.3);
}
.result-disc-large.slate {
  background: radial-gradient(circle at 35% 30%, var(--piece-s0), var(--piece-s2));
  box-shadow: 0 6px 20px rgba(0,0,0,.7), inset 0 2px 4px rgba(150,180,220,.3);
}
.result-score-num { font-family: var(--font-ui); font-size: 1.6rem; font-weight: 700; color: var(--text-primary); }
.winner-badge {
  font-size: .65rem; letter-spacing: .08em; color: var(--brass); text-transform: uppercase; font-family: var(--font-ui);
  position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  white-space: nowrap;
}

/* ── Buttons ── */
.primary-btn {
  width: 100%; padding: 11px;
  background: transparent; border: 1px solid var(--brass); border-radius: var(--radius);
  color: var(--brass); font-family: var(--font-ui); font-size: .75rem;
  letter-spacing: .08em; text-transform: uppercase;
  cursor: pointer; margin-top: 6px;
  transition: all .2s; position: relative; overflow: hidden;
}
.primary-btn::before { content: ''; position: absolute; inset: 0; background: var(--brass); opacity: 0; transition: opacity .2s; }
.primary-btn:hover { color: var(--bg); }
.primary-btn:hover::before { opacity: 1; }
.primary-btn span { position: relative; z-index: 1; }

.go-lobby-btn { border-color: var(--border) !important; color: var(--text-muted) !important; }
.go-lobby-btn:hover { color: var(--text-primary) !important; border-color: var(--brass) !important; }

.quit-danger-btn  { border-color: rgba(180,60,60,.4) !important; color: rgba(180,80,80,.7) !important; }
.quit-danger-btn:hover { color: #c05050 !important; border-color: rgba(180,60,60,.7) !important; }

.quit-dialog-title { color: #b85050 !important; text-shadow: 0 0 20px rgba(180,60,60,.4) !important; }
.quit-confirm-btn  { border-color: rgba(180,60,60,.7) !important; color: #c05050 !important; }
.quit-confirm-btn:hover { color: #fff !important; }

/* ── Dialog close icon ── */
.dialog-close-icon {
  position: absolute; top: 12px; right: 12px;
  width: 38px; height: 38px;
  background: rgba(255,255,255,.06);
  border: 1px solid var(--border); border-radius: 50%;
  color: var(--text-muted); font-size: .9rem;
  cursor: pointer; transition: all .2s;
  display: flex; align-items: center; justify-content: center;
  /* Round 41: the ✕ glyph (text content, not an SVG — see index.html) was
     rendering visibly above center even though flex is centering it
     correctly on paper. Root cause: <button> gets its own line-height
     (inherit, from round 35's global `button{line-height:inherit}` reset)
     of 1.2, but the glyph itself sits near the top of that inflated line
     box rather than centered within it, so flex ends up centering a box
     that's taller than the glyph actually needs — line-height:1 shrinks
     the line box down to the glyph's own metrics, so the same flex
     centering now centers the glyph itself, not empty line-box padding. */
  line-height: 1;
}
.dialog-close-icon:hover { background: rgba(255,255,255,.12); color: var(--text-primary); border-color: var(--brass); }

/* ══════════════════════════════════════════════════════════════
   LOBBY SCREEN
══════════════════════════════════════════════════════════════ */
#lobby-screen {
  position: fixed; inset: 0;
  /* Round 52 (follow-up on the same iOS bug report — after shipping the
     .is-ios padding-top override just below, the developer pointed out a
     screenshot still showing Safari's own docked-bottom address bar
     overlapping the lobby's own bottom content, forcing a scroll to reach
     it). Round 46 already fixed this exact class of bug for the GAME
     scene by giving html/body an explicit height:100dvh — but that fix
     only reaches elements sized through normal flow/percentage/flex
     layout against html/body's own box, which is exactly how #app is
     sized (align-self:stretch, round 19). #lobby-screen (and every other
     position:fixed;inset:0 full-screen container in this file — see
     #compat-screen/#splash-screen/#orientation-screen/.overlay above) is
     NOT sized that way: a fixed-position element establishes its own
     containing block directly off the viewport, independent of html/
     body's CSS height property entirely, so inset:0 (== 0 on all four
     sides, meaning "100% of the containing block") kept resolving
     against iOS Safari's LARGE viewport (address bar assumed hidden) —
     the exact same historical quirk `dvh` exists to solve, just on a
     different element than round 46 patched. The fix has to be the same
     unit applied directly to the fixed element's OWN height, not
     inherited from an ancestor: see height:100dvh below. Impossible to
     reproduce the exact before/after in a desktop dev tool or in
     Playwright (no real engine here exhibits a collapsing/docked browser
     toolbar to create the large-vs-small viewport gap in the first
     place) — this is reasoned from how `position:fixed` containing
     blocks and `dvh` are documented to behave, not verified pixel-for-
     pixel against a real device; re-check on a real iPhone after this. */
  height: 100dvh;
  background: var(--bg);
  display: flex; align-items: stretch; justify-content: center;
  z-index: 200; opacity: 1; transition: opacity .5s;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Guaranteed clearance from the true top edge on every device (25px
     minimum, more on notched phones) — applies to every scene the same
     way; see the equivalent rule on #app for the in-game header. */
  padding-top: max(25px, env(safe-area-inset-top));
}
/* Round 52, direct request (iOS bug report — a few px of leftover scroll
   remained in the lobby specifically on iPhone, even after round 46's
   100dvh fix): round 46 only accounts for Safari's dynamic browser-chrome
   height, a SEPARATE budget line from the notch/Dynamic Island reservation
   handled just above via env(safe-area-inset-top) — every iPhone still sold
   (bar the small-bezel SE) has had a notch or Dynamic Island since the
   iPhone X (2017), reporting ~44-59px there, well past this project's 25px
   floor used everywhere else; Android's own display-cutout allowance is
   typically much smaller or reports 0 in mobile browsers, so this asymmetry
   is iOS-specific by nature, not something a generic touch-device query
   would isolate. That extra 20-35px eats directly into .lobby-inner's
   already tightly-tuned height budget (rounds 6/11/30/47) on exactly the
   devices QA tests with, tipping a couple of sections a few px past it and
   onto #lobby-screen's own overflow-y:auto safety net above. Halves the
   safe-area allowance specifically on iOS (detected via the same isIOS()
   check FullscreenManager already uses, round 23 — tagged onto <html> as
   .is-ios at bootstrap, see script.js) rather than dropping it to the
   25px floor outright — still keeps real clearance under a notch/Dynamic
   Island, just less of it; non-notch iPhones (env is 0) are unaffected,
   the 25px floor still wins there exactly as before. */
html.is-ios #lobby-screen { padding-top: max(25px, calc(env(safe-area-inset-top) * .5)); }
/* Round 53, direct follow-up (real-device screenshot: "Начать" still sat
   right at/under the bottom edge, needing a scroll to fully reveal it) —
   round 52's halving above still wasn't enough. Two more iOS-only levers,
   both additive to round 52's, neither touching Android/desktop:
   1. The .5 multiplier above only actually pulls the padding below this
      project's universal 25px floor once a device's own notch/Dynamic
      Island reports MORE than 50px (only the Pro/Pro Max Dynamic-Island
      tier, ~59px) — a plain notch (iPhone X/11/12/13/14 non-Pro, ~44px)
      already lands under the floor even after halving (44*.5=22 < 25), so
      for the exact phone in the bug report this earlier fix was already a
      no-op — there was no more room to give back there specifically.
   2. The real remaining budget problem is that .lobby-inner's overall
      content scale (rounds 6/11 — the local em clamp that every lobby
      element's size is expressed against) has no iOS-specific slack at
      all — it was tuned against ordinary browser chrome, not against the
      EXTRA space Safari's own docked toolbar can still eat even with
      round 52's height:100dvh fix on #lobby-screen in place (dvh tracks
      the chrome live, but only once it's settled — right at first paint,
      before any scroll/resize event, some Safari versions haven't
      necessarily reconciled it yet). Rather than chase that timing issue
      pixel-by-pixel with no way to reproduce it locally (neither Chromium
      nor Playwright's WebKit build has real collapsing browser chrome to
      test against), the robust fix is a flat, modest (~12%) shrink of the
      WHOLE lobby's local scale on iOS specifically — every em-sized
      element (logo, section gaps, mode/diff/theme buttons, the play
      button) gets a little smaller together, freeing real height as a
      buffer against whatever the chrome ends up actually costing, instead
      of gambling on one exact number. Deliberately does NOT touch
      .lobby-inner's own padding-top clamp (the topbar-to-logo clearance,
      rounds 30/47) — that one is calibrated against .lobby-icon-btn's
      REAL rendered size, which is a fixed ~54px regardless of any of this
      (buttons don't inherit font-size from their own ancestors at all —
      the same non-inheritance bug as everywhere else in this project, see
      round 30) — so shrinking the general content scale doesn't reduce
      that specific clearance's real requirement, and it was left alone on
      purpose rather than risking the topbar overlapping the logo. */
html.is-ios .lobby-inner { font-size: clamp(11px, 2.5vh, 20px); }
#lobby-screen.hidden { opacity: 0; pointer-events: none; }

.lobby-inner {
  width: 100%; max-width: 460px;
  /* Fluid local type-scale (1em here) so the whole lobby compresses
     together as viewport height shrinks — this is what keeps "Begin
     Match" on-screen without a scrollbar on short/wide desktop windows,
     independent of the global --font-ui / html{font-size} scale used
     everywhere else. Max (23.04px) matches the prior fixed rem look. */
  font-size: clamp(11px, 2.85vh, 23.04px);
  /* Top padding clears the (bigger, see .lobby-icon-btn) topbar buttons:
     .78em offset + 3.74em button height (desktop, the larger of the two
     breakpoints) + a small buffer. */
  padding: clamp(50px, 4.85em, 112px) 20px clamp(12px, 1.2em, 28px);
  display: flex; flex-direction: column; align-items: center;
  /* .lobby-inner is stretched to the full height of #lobby-screen (see
     align-items:stretch there); space-between lets any extra room on
     tall screens flow into the gaps between sections instead of sitting
     unused below "Begin Match". Short viewports are unaffected — there's
     no extra room to distribute, and the em-scale above already keeps
     things compact before it ever gets that far. */
  justify-content: space-between;
  position: relative; z-index: 1;
  margin: 0 auto;
}
/* Flex items default to flex-shrink:1 — combined with space-between above,
   that let the browser quietly compress a section (e.g. Difficulty) below
   its own content height whenever the lobby's total content was a hair
   taller than the stretched box, and since these sections use
   overflow:hidden for the show/hide slide transition, the compression
   clipped their bottom edge instead of ever showing a scrollbar (so the
   lobby-noscroll tests never caught it). Every direct child keeps its
   natural content height instead; the em-clamp above is what keeps the
   total within budget, with #lobby-screen's overflow-y:auto as the
   last-resort fallback for genuinely extreme sizes. */
.lobby-inner > * { flex-shrink: 0; }

/* Brand — the game-name text (.lobby-title/.lobby-subtitle) was replaced
   by the game-logo.png image (round 26, same asset as the splash screen).
   Sized in em, like everything else in .lobby-inner, so it shrinks in
   lockstep with the fluid vh-driven clamp on short screens instead of
   staying a fixed physical size (which risked reintroducing the lobby
   scroll bug rounds 6/7/11 fixed — see that section for why). */
.lobby-brand { text-align: center; margin-bottom: .35em; }
/* display:block strips the few px of inline baseline whitespace an <img>
   otherwise leaves below itself (round 30) — pure bug fix, same on every
   screen size, frees a little real vertical budget everywhere. */
.lobby-logo { display: block; width: 14.25em; max-width: 100%; height: auto; margin: 0 auto; } /* enlarged round 28 (from 11em) — note: the "+15%" this was requested as would only be 12.65em; 15em is what actually shipped, untested at the time per direct request, and is the number the round-30 fix below was measured against. Round 47: -5% (15em * .95 = 14.25em), direct request. */

/* Ornament (the small diamond + two fading side lines that used to sit
   between the logo and the Game Mode section) removed entirely across two
   rounds — round 47 dropped the two .lobby-ornament-line bars (direct
   request), round 48 removed the remaining .lobby-ornament/-diamond too
   (direct request) — full-removal both times (not display:none — see
   CLAUDE.md section 3/11), no CSS or markup left behind. */

/* Section labels */
.lobby-section { width: 100%; margin-bottom: .61em; }
.lobby-section-label { font-family: var(--font-ui); font-size: .55em; letter-spacing: .1em; color: var(--text-muted); text-transform: uppercase; margin-bottom: .35em; padding-left: 2px; }

/* Mode grid */
.lobby-mode-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .35em; }
.lobby-mode-btn {
  padding: .61em .43em;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text-muted); font-family: var(--font-ui); font-size: .65em;
  letter-spacing: .12em; cursor: pointer; text-transform: uppercase; transition: all .2s;
  display: flex; flex-direction: column; align-items: center; gap: .26em;
  position: relative; overflow: hidden;
}
.lobby-mode-btn:hover  { border-color: var(--brass); color: var(--brass); }
.lobby-mode-btn.selected {
  border-color: var(--brass); color: var(--brass);
  background: rgba(212,168,67,.07);
  box-shadow: 0 0 12px rgba(212,168,67,.15);
}
.lobby-mode-icon  { font-size: 1.4em; line-height: 1; }
.lobby-mode-label { position: relative; z-index: 1; }

/* Difficulty */
.lobby-diff-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: .26em; }
.lobby-diff-btn {
  padding: .35em .17em;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text-muted); font-family: var(--font-ui); font-size: .82em;
  letter-spacing: .08em; cursor: pointer; transition: all .2s; text-align: center;
}
.lobby-diff-btn.selected { border-color: rgba(100,180,100,.6); color: #90d890; background: rgba(100,180,100,.07); }
.lobby-diff-btn:hover:not(.selected) { border-color: rgba(100,180,100,.3); color: var(--text-primary); }
.lobby-diff-hard.selected { border-color: rgba(220,60,60,.7); color: #f07070; background: rgba(200,40,40,.09); }
.lobby-diff-hard:hover:not(.selected) { border-color: rgba(200,60,60,.35); color: var(--text-primary); }
/* Collapse/expand animates opacity + max-height only (2 properties) — NOT
   margin-bottom, even though .lobby-section normally sets one. A margin
   sits outside the box max-height/overflow:hidden clip, so animating it
   needs its own transition entry; adding a 3rd property that way is what
   caused a reproducible Chromium bug (confirmed against a sibling project
   that hit the same thing): two fast clicks (hide then immediately show
   again) could leave clientHeight reading 0 while scrollHeight still read
   full height, a transient false "clipped" state that tests/lobby-
   sections.spec.js could catch as a flake. Fix: move the section's
   trailing space from margin-bottom into padding-bottom instead — padding
   lives INSIDE the box (border-box), so it collapses for free as part of
   the max-height transition, with no 3rd transitioned property needed. */
.lobby-section.diff-section { transition: opacity .3s, max-height .3s; max-height: 6em; overflow: hidden; margin-bottom: 0; padding-bottom: .61em; }
.lobby-section.diff-section.hidden { opacity: 0; pointer-events: none; max-height: 0; }

/* Theme Grid — width capped in the same local em-unit so the square
   tiles (aspect-ratio: 1, otherwise driven by width alone) shrink
   together with everything else on short viewports, not just wide ones. */
.lobby-theme-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: .35em; max-width: min(100%, 18em); margin: 0 auto; }
.lobby-theme-btn {
  aspect-ratio: 1;
  border: 2px solid var(--border); border-radius: var(--radius);
  cursor: pointer; position: relative; overflow: hidden;
  transition: all .2s; background: transparent;
}
.lobby-theme-btn:hover { border-color: var(--brass); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,.5); }
.lobby-theme-btn.selected { border-color: var(--brass); box-shadow: 0 0 16px rgba(212,168,67,.3), inset 0 0 20px rgba(212,168,67,.05); }
.lobby-theme-btn.selected::after { content: '✦'; position: absolute; top:4px; right:5px; font-size: .5em; color: var(--brass); }

.theme-preview {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(4,1fr); grid-template-rows: repeat(4,1fr);
  gap: 1px; padding: .17em;
}
.theme-cell { border-radius: 1px; }

/* Lobby Actions */
.lobby-actions { width: 100%; margin-top: .17em; }
.lobby-play-btn {
  width: 100%; padding: .61em;
  background: transparent; border: 1px solid var(--brass); border-radius: var(--radius);
  color: var(--brass); font-family: var(--font-ui); font-size: .8em; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  cursor: pointer; position: relative; overflow: hidden; transition: color .2s;
  box-shadow: 0 0 20px rgba(212,168,67,.15);
}
.lobby-play-btn::before {
  content: ''; position: absolute; inset: 0;
  background: var(--brass);
  transform: scaleX(0); transform-origin: left; transition: transform .3s ease;
}
.lobby-play-btn:hover { color: var(--bg); }
.lobby-play-btn:hover::before { transform: scaleX(1); }
.lobby-play-btn span { position: relative; z-index: 1; }


/* ══════════════════════════════════════════════════════════════
   LOBBY TOP BAR — Fullscreen / Settings / Credits
══════════════════════════════════════════════════════════════ */
.lobby-topbar {
  position: absolute; top: .78em; right: .78em; z-index: 6;
  display: flex; gap: .78em;
}
.lobby-icon-btn {
  width: 3.74em; height: 3.74em; border-radius: 50%;
  background: var(--panel); border: 1px solid var(--border);
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: all .2s; flex-shrink: 0;
}
.lobby-icon-btn svg { width: 1.72em; height: 1.72em; }
.lobby-icon-btn:hover {
  border-color: var(--brass); color: var(--brass); background: rgba(212,168,67,.06);
  transform: translateY(-2px); box-shadow: 0 4px 16px rgba(212,168,67,.2);
}

/* In-game header reuses .icon-btn already defined above for #btn-fullscreen-game */

/* ══════════════════════════════════════════════════════════════
   PREFERENCE TOGGLES — Sound / Music / Language
   Shared identically between the Settings popup and the Pause popup —
   icon-only in both since round 22 (no more text labels anywhere).
══════════════════════════════════════════════════════════════ */
.pref-toggle-row { display: flex; gap: 12px; justify-content: center; margin: 4px 0 24px; flex-wrap: wrap; }
.pref-toggle-btn {
  flex: 1 1 0; min-width: 80px; max-width: 92px;
  padding: 14px 7px;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text-muted); cursor: pointer; transition: all .2s;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
}
.pref-toggle-btn svg { width: 29px; height: 29px; flex-shrink: 0; }
.pref-toggle-btn .pref-icon-on  { display: none; }
.pref-toggle-btn .pref-icon-off { display: block; }
.pref-toggle-btn.on .pref-icon-on  { display: block; }
.pref-toggle-btn.on .pref-icon-off { display: none; }
.pref-toggle-btn:hover { border-color: var(--brass); color: var(--brass); }
.pref-toggle-btn.on {
  border-color: var(--brass); color: var(--brass);
  background: rgba(212,168,67,.08); box-shadow: 0 0 10px rgba(212,168,67,.15);
}
.lang-toggle-btn img { width: 34px; height: 23px; border-radius: 2px; object-fit: cover; box-shadow: 0 1px 4px rgba(0,0,0,.5); }

.settings-dialog .dialog-title { margin-bottom: 26px; }
.settings-dialog .pref-toggle-row { margin-bottom: 4px; }

/* Pause popup has no subtitle/decorative icon above the toggles (removed —
   the title now leads straight into them), so the title gets its own
   breathing room in place of the old subtitle's margin. */
#overlay-pause .dialog-title { margin-bottom: 20px; }
/* Exit widened beyond .primary-btn's shared 6px margin-top — same
   deliberate "the danger action is the outlier, give it its own visual
   gap" spacing used for the equivalent button in Reversi Collection's
   Pause popup (round 34, ported to match 1:1). */
#overlay-pause #pause-exit-btn { margin-top: 16px; }

/* ══════════════════════════════════════════════════════════════
   CREDITS MODAL
══════════════════════════════════════════════════════════════ */
.credits-dialog { max-width: 528px; padding: 48px 38px 38px; }
/* Round 35 widened the right padding only (38px -> 42px) to add clearance
   between the close icon and the logo frame below it, back when the frame
   was still a visible bordered panel sitting fairly close to the dialog's
   own edge. Round 41: reverted (right padding back to 38px, matching left)
   — direct bug report that the logo now reads as off-center in the dialog,
   which this asymmetric padding was the actual cause of (.credits-logo-frame
   centers the logo within whatever box is left after the DIALOG's own
   padding, so unequal left/right padding there shifts the frame's box, and
   with it the centered logo, off the dialog's true center). Safe to drop:
   .credits-logo-frame has carried its own generous 34px padding since round
   25/39, which alone already keeps the logo well clear of the close icon
   regardless of the dialog's own padding being symmetric — the round-35
   asymmetry had stopped being necessary once that buffer existed, it just
   hadn't been revisited since. */
/* Round 36: same idea, vertical this time — +15% on top padding only
   (48px -> 55px) pushes the logo frame's top edge further down, away from
   the close button, without touching the dialog's bottom padding/height
   budget on the other three sides. Top asymmetry is NOT part of the
   off-center complaint (that's a left/right issue only) and is kept. */
.credits-dialog { padding-top: 55px; }
/* Round 39: dropped the panel background/border/radius this used to have
   (direct request — no boxed-frame look) — now purely a centering wrapper,
   the logo sits directly on .credits-dialog's own background instead of a
   separate panel inside it. padding kept, so the logo still keeps some
   breathing room from the dialog's edges even though there's no longer a
   visible box for that padding to sit inside. --surface (the dialog's own
   background) is dark on every theme except "gold", same as --panel was,
   so .credits-logo's filter:invert(1)/gold exception below still applies
   correctly without needing to change. */
.credits-logo-frame {
  display: flex; align-items: center; justify-content: center; padding: 34px;
}
/* logo.png is a plain black silhouette on a transparent background — invert
   it to white on the game's dark themes, but leave it black on "gold"
   (the one light-background theme) so it stays visible against a light
   panel. If a future theme adds another light background, add it here too. */
.credits-logo { max-width: 100%; height: auto; display: block; filter: invert(1); }
body[data-theme="gold"] .credits-logo { filter: none; }

/* ══════════════════════════════════════════════════════════════
   EXIT-CONFIRM DIALOG — checkmark / X (shared by Pause exit + header quit)
══════════════════════════════════════════════════════════════ */
/* Title carries the spacing that the (now-removed) subtitle used to give
   before the button row — subtitle's text moved INTO the title itself
   (round 20), so its old margin-bottom moves here too. */
.exit-confirm-dialog .dialog-title { margin-bottom: 34px; }
/* +75px per direct request (round 20): 26px -> 101px at the widths this
   dialog normally runs at (its 432px max-width leaves plenty of room).
   Fluid instead of a flat 101px so it doesn't force the two buttons wider
   than the dialog itself on a narrow phone (dialog width is 92vw there,
   not the full 432px) — clamps back down to the old 26px floor rather
   than overflowing. */
.exit-confirm-btn-row { display: flex; justify-content: center; gap: clamp(26px, 19vw, 101px); flex-wrap: wrap; }
.exit-confirm-icon-btn {
  width: 70px; height: 70px; border-radius: 50%;
  border: 1px solid var(--border); background: transparent;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: all .2s; flex-shrink: 0;
}
.exit-confirm-icon-btn svg { width: 29px; height: 29px; }
.exit-confirm-no { color: var(--text-muted); }
.exit-confirm-no:hover { border-color: rgba(200,60,60,.6); color: #c05050; background: rgba(180,60,60,.07); }
.exit-confirm-yes { border-color: rgba(100,180,100,.5); color: #90d890; }
.exit-confirm-yes:hover { border-color: #90d890; background: rgba(100,180,100,.1); box-shadow: 0 0 14px rgba(100,180,100,.25); }

/* ══════════════════════════════════════════════════════════════
   LOCALIZATION — RTL (Arabic) & CJK (Japanese)
   Scoped to text nodes only, per spec — layout/structure never flips.
══════════════════════════════════════════════════════════════ */
[data-lang="ar"] [data-i18n] {
  direction: rtl;
  unicode-bidi: plaintext;
  letter-spacing: normal !important;
}
[data-lang="ar"] .compat-message {
  line-height: 1.8;
}

/* ══════════════════════════════════════════════════════════════
   RESPONSIVE
══════════════════════════════════════════════════════════════ */
@media (max-width: 480px) {
  .lobby-theme-grid { grid-template-columns: repeat(4,1fr); }
  /* Round 49, direct request, mobile only: tighten the gaps between the
     Game Mode / Difficulty / Board & Pieces sections. Two separate
     selectors on purpose, not the shared .lobby-section rule — that rule
     also governs the gap AFTER "Board & Pieces" (before the play button),
     which wasn't part of this request and stays untouched.
     #lobby-mode-section's margin-bottom is the Game Mode -> Difficulty
     gap (roughly halved, .61em -> .3em). .diff-section's padding-bottom
     (not margin — see its own rule below, round 22's collapse-animation
     fix) is the Difficulty -> Board & Pieces gap, also roughly halved. */
  #lobby-mode-section { margin-bottom: .3em; }
  .diff-section { padding-bottom: .3em; }
  /* Round 50, same request/scope as round 49 just above, direct follow-up:
     the gap AFTER "Board & Pieces", before the "Begin Match" button —
     this section's own margin-bottom (inherited from the shared
     .lobby-section rule, .61em) is what creates it, so it's overridden
     here by ID rather than touching the shared rule (which would also
     affect the OTHER two gaps already handled by their own selectors
     above, round 49). Same roughly-halved value as those. */
  #lobby-theme-section { margin-bottom: .3em; }
  /* Round 51, direct request, mobile only: nudge "Begin Match" up by 5% —
     interpreted as 5% of the viewport height (matching how earlier %-based
     vertical nudges in this project were specified — e.g. the splash
     logo's "+7% of container height", round 30/31), not 5% of the
     button's own (tiny) height, which a plain CSS translateY(-5%) would
     actually resolve against per spec and would be visually
     imperceptible. A flat `-5vh` was tried first and OVERLAPPED the
     theme grid on short phones (320x568) — same root cause as round 48's
     lobby-brand nudge: below ~808px viewport height, .lobby-inner's own
     font-size clamp (clamp(11px,2.85vh,23.04px), maxes out at
     23.04/.0285≈808px) is still actively shrinking content to fit, so
     there's no free vertical room at all for a translate to borrow — any
     fixed upward shift there just drives the button straight into
     whatever's above it. Same self-limiting fix as round 48: `808px -
     100vh` is only negative (real surplus) above that 808px threshold,
     `min(0px, …)` zeroes it out below (no shift at all on short phones —
     safe by construction), and the outer clamp caps the maximum lift so
     very tall/unusual windows don't send it flying too far up. The `1.17`
     multiplier is picked so the common ~844px-tall phone lands close to
     the originally-intended ~5vh (~42px) worth of lift before the -40px
     cap even kicks in. Verified visually at 320x568 (no shift, no
     overlap — confirmed a plain -5vh DID overlap here before this fix)
     and 390x844 (visibly raised, clear of the grid above). */
  .lobby-play-btn { transform: translateY(clamp(-10px, calc(min(0px, 808px - 100vh) * 1.17), 0px)); }
  .lobby-topbar { top: .52em; right: .52em; gap: .59em; }
  /* Round 47, direct request, mobile only: +20% (3.39em -> 4.068em, svg
     1.6em -> 1.92em). .lobby-icon-btn is a <button> and doesn't inherit
     font-size (round 30) — its real rendered size is fixed regardless of
     .lobby-inner's clamp, so growing it here also grows the topbar's real
     height by the same ~20%, which eats into the clearance
     .lobby-inner's padding-top reserves above it (see round 30's writeup
     right below this block) — that clearance was re-measured and bumped
     to match, see its own comment. */
  .lobby-icon-btn { width: 4.068em; height: 4.068em; }
  .lobby-icon-btn svg { width: 1.92em; height: 1.92em; }
  .credits-dialog { padding: 34px 22px 24px; padding-top: 58px; } /* right/left both 22px (round 41 — dropped the round-35 24px right override, see the desktop rule's comment, same off-center reason applies here); top was 34->39 (round 36), now 58px (round 38) — .dialog-close-icon is a FIXED 38x38px at top:12px/right:12px on every screen size (no mobile override of its own), so its bottom edge always sits at 50px regardless of the dialog's own padding; 39px left only ~11px of overlap with the logo frame below it, worse than intended since mobile shrinks the dialog's overall width/padding proportionally but the button never shrinks to match. 58px reopens roughly the same ~8px clearance the desktop padding-top (55px) already has against that same fixed 50px. */
  .pref-toggle-btn { padding: 11px 5px; }
  /* Round 30 — see the desktop override below for the root cause writeup;
     same fix, just using this breakpoint's own (smaller) topbar offset/size.
     Confirmed this same round-28 regression on 390x700/360x640/320x568 too
     (13-16px of real scroll) once the desktop fix surfaced it in testing.
     Round 47: .lobby-icon-btn's mobile size grew +20% just above (its real
     rendered height is fixed regardless of this clamp, round 30) — bumped
     every term of this clamp by roughly that same growth (+9px min/max,
     ×1.2 the em term) to keep the topbar's now-taller real footprint
     clear of the logo; re-verified against tests/lobby-noscroll.spec.js
     and tests/lobby-sections.spec.js after, see CLAUDE.md round 47. */
  .lobby-inner { padding-top: clamp(49px, 4.62em, 98px); }
  /* Round 48, direct request, mobile only: shrink the gap between the logo
     and the Game Mode section further. .lobby-brand's margin-bottom alone
     was already 0 (round 30) — the REST of that gap (24px on a roomy
     844px-tall phone, but only ~2-4px on a short one like 320x568) comes
     entirely from .lobby-inner's justify-content:space-between (round 7),
     which freely distributes any leftover height across every gap between
     sections once the .lobby-inner font-size clamp above hits its max
     (23.04px, reached once viewport height passes ~808px — 23.04/.0285) —
     below that height there's no real leftover to distribute, which is
     exactly why the gap is already small there. A flat negative margin
     here would be unsafe (it'd overlap the mode section on short phones,
     where the gap is already only a few px) — instead this only pulls
     when there's actual surplus past that ~808px threshold: `808px -
     100vh` is negative (a surplus) only above that height, `min(0px, …)`
     keeps it at exactly 0 below it (no pull, no risk of overlap), and the
     ×.6 takes back only part of the surplus rather than fighting
     space-between for all of it. Verified across 320x568/360x640/390x700/
     390x844 (and a couple more heights either side of the 808px
     threshold) — no overlap anywhere, meaningfully tighter on tall
     phones, unchanged on short ones. */
  .lobby-brand { margin-bottom: clamp(-10px, calc(min(0px, 808px - 100vh) * .6), 0px); }
  /* Round 39, direct request: credits logo +10% on mobile specifically —
     it otherwise just fills 100% of .credits-logo-frame's own content
     width, so this makes it spill slightly past that padding (now just
     empty space since round 39 removed the frame's visible background/
     border, see .credits-logo-frame) rather than needing to steal room
     from .credits-dialog's own padding. */
  .credits-logo { width: 110%; }
  /* Round 39, direct request: raise the bottom corner buttons (and with
     them #status-bar, see below) using the SAME guaranteed-clearance rule
     #app's own padding-top already uses at the top of the scene —
     max(25px, safe-area-inset) — instead of the small em-based bottom
     offset (~12px at max clamp) they used before. Longhand padding-bottom
     only (never the padding shorthand here) so the top guarantee/side
     padding aren't touched — see the #app padding comment above this
     block. Desktop keeps its original smaller offset; this wasn't asked
     for there. */
  #app { padding-bottom: max(25px, env(safe-area-inset-bottom)); }
  .btn-restart { bottom: max(25px, env(safe-area-inset-bottom)); }
  /* Round 41, direct request: the round-40 score-panel shrink (padding/
     disc/number, plus #score-panel's own margin-bottom) was meant to apply
     to the PC version only — it was a fix for a PC-only bug (buttons/board
     overlap on a short desktop window) but had been written as an
     unscoped base rule, so it silently shrank the mobile score cards too,
     which nobody had asked for there. Restores the exact pre-round-40
     values on mobile only; the desktop base rule (further tuned this same
     round for the board-frame/score-panel overlap, see #board-wrap and the
     base .score-card/.score-disc/.score-num/#score-panel rules) is
     untouched by this override. */
  /* Round 42, direct request: +10% again on top of the round-41 revert
     (which restored the exact pre-round-40 numbers) — "make the text bigger
     too", so padding/disc/number all scale together, same as they always
     have as one unit. .3472em*1.1=.38192em, .5208em*1.1=.57288em,
     .9549em*1.1=1.05039em, 1.4em*1.1=1.54em. #score-panel's own
     margin-bottom is NOT part of "the blocks" (it's the gap around them,
     not their own size) and is left at its round-41 value. */
  .score-card { padding: .38192em .57288em; }
  .score-disc { width: 1.05039em; height: 1.05039em; }
  .score-num  { font-size: 1.54em; }
  #score-panel { margin-bottom: .6076em; }
  /* Round 41, direct request: "Ваш ход" +20% on mobile only. */
  #status-text { font-size: 1.14em; }
  /* Round 41, direct request: the mandatory-capture toast should span (near)
     the full screen width on mobile, not stay capped at the game-box width
     like the rest of the mobile-scoped chrome — "full width minus 5%".
     Overrides the base min(92vw,460px) outright rather than composing with
     it, since on mobile 460px was never the binding term anyway (92vw
     already wins on any phone screen) — the only actual change here is
     92vw -> 95vw.
     Round 42: round 41's fix only touched max-width, but .toast has no
     explicit width of its own anywhere — it's a shrink-to-fit bubble
     (width = its own text content, capped by max-width), so on most
     messages the box was still nowhere near 95vw wide, just narrower than
     its old 92vw cap. Direct follow-up report: "make it actually full
     width" — added explicit `width` here (mobile only; the desktop bubble
     is deliberately shrink-to-fit, unchanged) so the box itself stretches
     to 95vw instead of merely being ALLOWED to. */
  .toast { width: 95vw; max-width: 95vw; }
  /* Round 42, direct request: nudge the credits logo up specifically on
     mobile — .credits-logo-frame centers it via flex, so a transform (not
     margin/padding, see the "shift inside a flex-centered box" lesson used
     elsewhere in this project) moves it without perturbing the frame's own
     centering math or its width:110% sibling rule just above. Desktop
     .credits-logo-frame is untouched. */
  .credits-logo { transform: translateY(-15px); }
}

/* Round 30: the round-28 lobby-logo enlargement (+15%, see above) pushed
   several common desktop window sizes (short/wide — e.g. 1366x768,
   1024x600) into a real vertical scrollbar on #lobby-screen. The topbar
   buttons (.lobby-icon-btn) are plain <button> elements without
   `font: inherit`, so despite living inside .lobby-inner they've never
   actually tracked its em-scale — their rendered size has always been a
   fixed ~50px (the browser's own default button font-size), not the
   3.74em the old padding-top comment assumed. That means .lobby-inner's
   padding-top had far more headroom above the topbar than it needed at
   most sizes; this reclaims most of that headroom, plus tightens the
   logo's own bottom margin. Same root cause hit the ≤480px breakpoint too
   (see its own override above) — fixed there as well once testing turned
   it up, even though the original request only named desktop. */
@media (min-width: 481px) {
  .lobby-inner { padding-top: clamp(41px, 3.95em, 91px); }
  .lobby-brand { margin-bottom: 0; }
}
/* Note: .lobby-inner's padding/font-size, and #app's own padding/font-size
   (header, score-panel, status-bar, icon buttons), no longer need a
   max-height breakpoint here — both are driven by their own fluid clamp()
   (see "LOBBY SCREEN" and "APP LAYOUT" above), which already shrink
   continuously with viewport width/height instead of jumping at a fixed
   breakpoint (round 17 — the #app side used to have max-width:480px and
   max-height:700px steps for exactly this; removed once everything in its
   subtree moved to em). */
