/* ═══════════════════════════════════════════════════════════════════════════
   ROUNDZ — Lobby (v3) redesign. DESIGNER-SAFE.
   Scoped entirely under `.lobbyv3` (added to #lobby) so its generic class names
   (.card, .divider, .section-title, .avatar, .balance …) and design tokens never
   collide with style.css or auth.css. The game's screen system (.screen/.page/
   .hidden) and every JS hook id are untouched — this only restyles inside #lobby.
   The content (tier cards, leaderboard rows, chat, ticker, stats) is emitted by
   client.js render fns, which are updated to emit these class names (stage L2).
   ═══════════════════════════════════════════════════════════════════════════ */
#lobby.lobbyv3 {
  --bg: #0b1220;
  --bg-deep: #07101b;
  --surface: rgba(15, 25, 42, 0.88);
  --surface-strong: rgba(17, 29, 48, 0.96);
  --text: #f4f8fc;
  --muted: #9aa8bc;
  --muted-2: #6f7f96;
  --line: rgba(205, 226, 248, 0.12);
  --line-strong: rgba(0, 229, 255, 0.38);
  --accent: #00e5ff;
  --accent-soft: #66efff;
  --success: #72efbd;
  --warning: #f1c75b;
  --danger: #ff7280;
  --radius-xl: 22px;
  --radius-lg: 17px;
  --radius-md: 13px;
  --shadow: 0 24px 70px rgba(0, 0, 0, 0.38);
  --font-heading: Sora, Inter, ui-sans-serif, system-ui, sans-serif;
  --font-body: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* #lobby is the app-shell grid (topbar / dashboard / ticker) */
  color: var(--text);
  font-family: var(--font-body);
}

/* Desktop: pin the whole lobby to the viewport, no page scroll (the game already
   locks this ≥1121px; here we own the internal 3-row grid). */
@media (min-width: 1121px) {
  #lobby.lobbyv3 {
    /* ÷ --uiz, not plain 100dvh: style.css puts `zoom: var(--uiz)` on .page at
       ≥1680px, and dvh units are NOT divided by zoom — so a plain 100dvh lobby
       rendered 12–55% taller than the viewport with overflow:hidden and no
       scroller, permanently clipping the ticker row (review 2026-07-31). The
       division cancels the zoom exactly; --uiz is unset (=1) below 1680px. */
    height: calc(100dvh / var(--uiz, 1));
    overflow: hidden;
    display: grid;
    grid-template-rows: 66px minmax(0, 1fr) 36px;
  }
  /* style.css's desktop lobby-lock declares `#lobby .pane { display:flex }` — a
     higher-specificity id selector (1,1,0) that beats `.lobbyv3 .arena-panel`
     (0,2,0) and collapsed the arena back into a flex column. That killed the
     arena's own `grid-template-rows: auto auto minmax(0,1fr)`, so the tier grid
     stopped absorbing the pane's free height and left a dead band below the cards
     on tall screens. Re-assert the intended grid (and its clip). */
  #lobby.lobbyv3 .arena-panel { display: grid; overflow: hidden; }
}

.lobbyv3 button, .lobbyv3 input { font: inherit; }
.lobbyv3 button { cursor: pointer; }
.lobbyv3 svg {
  width: 20px; height: 20px; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}

.lobbyv3 .lobby-topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  /* PWA (black-translucent) draws UNDER the iOS status bar: clear it with the
     top inset — the clock sat ON the logo and the battery ON the balance chip
     (owner PWA screenshots 2026-08-11). Right inset: in landscape the topbar's
     right edge is the viewport edge and the notch can be on THAT side (depends
     on rotation direction). Left inset is NOT added here — in landscape the
     left rail absorbs it (its own rule), and in portrait it is 0. Browsers
     resolve every env() to 0, so desktop/browser layouts are byte-identical. */
  padding: env(safe-area-inset-top, 0px)
           calc(clamp(18px, 2.6vw, 42px) + env(safe-area-inset-right, 0px))
           0 clamp(18px, 2.6vw, 42px);
  border-bottom: 1px solid var(--line);
  background: rgba(7, 13, 23, 0.74); backdrop-filter: blur(18px);
}
.lobbyv3 .brand {
  font-family: var(--font-heading); font-size: 27px; font-weight: 800;
  letter-spacing: -0.055em; color: var(--text); user-select: none;
}
.lobbyv3 .brand span { color: var(--accent); }
.lobbyv3 .help-button {
  width: 42px; height: 42px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 13px; color: var(--muted);
  background: rgba(12, 20, 34, 0.78);
  transition: border-color .15s ease, color .15s ease, transform .15s ease;
}
.lobbyv3 .help-button:hover { color: var(--text); border-color: rgba(0,229,255,.28); transform: translateY(-1px); }
.lobbyv3 .topbar-actions { display: flex; align-items: center; gap: 10px; }
/* The one-tap profile shortcut (variant A) — PHONES ONLY. It exists because the
   player card (and its Profile button) is hidden in every phone zone. On desktop
   that card is always on screen in the left rail, so the chip was a second door
   to the same room (owner 2026-08-04). Shown by the ≤820px block, like the
   balance chip beside it. */
.lobbyv3 .avatar-chip {
  width: 42px; height: 42px; padding: 0; border-radius: 50%;
  border: 1px solid rgba(0, 229, 255, .35);
  background: radial-gradient(circle at 42% 38%, #0b2230, #05131e 70%);
  overflow: hidden; display: none; place-items: center;
  transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.lobbyv3 .avatar-chip:hover { border-color: var(--accent); transform: translateY(-1px); box-shadow: 0 0 14px rgba(0, 229, 255, .25); }
.lobbyv3 .avatar-chip span, .lobbyv3 .avatar-chip svg { width: 100%; height: 100%; display: block; }

/* Topbar balance chip — PHONES ONLY (shown by the ≤820px block). On desktop the
   left-rail player card carries the balance and the wallet button, so a second
   copy up here would be noise. */
.lobbyv3 .balance-chip { display: none; }
.lobbyv3 .balance-chip .bc-unit { color: var(--accent); font-weight: 800; font-size: 11px; }
.lobbyv3 .balance-chip .bc-amount { color: var(--accent); font-weight: 700; font-size: 14px; letter-spacing: .01em; }
.lobbyv3 .balance-chip .bc-wallet { display: none; width: 15px; height: 15px; }
/* DEMO: a readout, nothing to tap. REAL: the wallet door. */
.lobbyv3 .balance-chip[data-real="1"] { border-color: rgba(0, 229, 255, .38); color: var(--text); }
.lobbyv3 .balance-chip[data-real="1"] .bc-wallet { display: block; color: var(--accent); }
.lobbyv3 .balance-chip[data-real="1"]:hover { border-color: var(--accent); box-shadow: 0 0 14px rgba(0, 229, 255, .2); }
.lobbyv3 .balance-chip:not([data-real="1"]) { cursor: default; }

.lobbyv3 .dashboard {
  min-height: 0; display: grid;
  grid-template-columns: clamp(250px, 19vw, 305px) minmax(520px, 1fr) clamp(292px, 21vw, 345px);
  gap: 16px; padding: 16px clamp(16px, 2.4vw, 38px);
}
.lobbyv3 .left-rail, .lobbyv3 .right-rail, .lobbyv3 .arena-panel { min-height: 0; }
.lobbyv3 .left-rail, .lobbyv3 .right-rail { display: grid; gap: 14px; }
.lobbyv3 .left-rail { grid-template-rows: auto minmax(0, 1fr); }
.lobbyv3 .right-rail { grid-template-rows: minmax(300px, 46%) minmax(0, 1fr); }
/* center column wraps the arena panel + the (game-owned) #matchWait theater — one
   visible at a time; showWait() toggles them. Keeps matchWait in grid column 2. */
.lobbyv3 .center-col { min-height: 0; display: grid; }
.lobbyv3 .center-col > * { min-height: 0; grid-area: 1 / 1; }
.lobbyv3 .center-col > .hidden { display: none !important; }

.lobbyv3 .card {
  min-height: 0; border: 1px solid var(--line); border-radius: var(--radius-xl);
  background: linear-gradient(180deg, rgba(255,255,255,.022), transparent 25%), var(--surface);
  box-shadow: var(--shadow); backdrop-filter: blur(18px); overflow: hidden;
}

/* ── Player card v2 (owner's Claude Design export, 2026-08-01) ──────────────
   Left-aligned: avatar+name row, hairline, realm pill stacked OVER the balance,
   full-width mono buttons. `.player-card.real` (paintRealmChrome) switches the
   avatar ring and the pill to cyan — DEMO stays deliberately muted. */
.lobbyv3 .player-card {
  padding: 20px; display: flex; flex-direction: column; gap: 16px; text-align: left;
  background: linear-gradient(180deg, #162035 0%, #111A2B 100%);
}
/* flex-start: the column beside the avatar is three lines tall now, so centring
   floated the avatar against the middle of the balance instead of the name. */
.lobbyv3 .pc-head { display: flex; align-items: flex-start; gap: 14px; min-width: 0; }
.lobbyv3 .avatar {
  width: 64px; height: 64px; flex: none; border-radius: 50%;
  border: 1px solid rgba(139, 152, 179, .34); padding: 3px;
  background: radial-gradient(circle at 34% 30%, #2b3a55 0%, #182238 55%, #0e1626 100%);
  display: grid; place-items: center; overflow: hidden;
}
.lobbyv3 .player-card.real .avatar { border-color: rgba(0, 229, 255, .55); box-shadow: 0 0 12px rgba(0, 229, 255, .16); }
.lobbyv3 .avatar svg { width: 100%; height: 100%; display: block; border-radius: 50%; }
/* The card's one data column: name → realm → balance, all on the same left x. */
.lobbyv3 .identity { min-width: 0; display: grid; gap: 6px; justify-items: start; }
.lobbyv3 .nickname {
  max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
  font-family: var(--font-body); font-size: 15px; font-weight: 600; letter-spacing: 0;
}
.lobbyv3 .nickname .who-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lobbyv3 .pc-rule { height: 1px; background: var(--line); }
/* No pill: the border was a box floating mid-column, and its 10px of padding
   pushed the label off the nickname's left edge — the visible "не ровно". As a
   plain dot + caption it lines up to the glyph and reads as what it is: a label
   on the number under it. REAL still recolours it (rule below). */
.lobbyv3 .realm-badge {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 7px;
  padding: 0; border: 0;
  color: var(--muted); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; white-space: nowrap; line-height: 1;
}
.lobbyv3 .realm-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--muted); flex: none; }
.lobbyv3 .player-card.real .realm-badge { color: var(--accent); }
.lobbyv3 .player-card.real .realm-dot { background: var(--accent); box-shadow: 0 0 8px rgba(0, 229, 255, .6); }
/* (.balance-row is gone — the balance is the third line of .identity now.) */
.lobbyv3 .balance {
  display: flex; align-items: baseline; gap: 7px; min-width: 0; white-space: nowrap;
  margin-top: 1px;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; line-height: 1;
}
.lobbyv3 .balance .currency { color: var(--muted); font-size: 11px; font-weight: 500; letter-spacing: .12em; flex: none; }
/* Brand cyan, both realms (owner 2026-08-04 — the v3 rebuild had put it back to
   white). The balance is THE number on this card; the accent is what makes it
   read as one instead of as another label. */
.lobbyv3 .balance .amount { color: var(--accent); font-size: 26px; font-weight: 600; letter-spacing: -.01em; margin: 0; overflow: hidden; text-overflow: ellipsis; }
/* Unit placement: $RZ (demo, .rz-unit) prefixes the number — DOM order; USDC
   (real) reads as a suffix. One order flip, mirrored from paintRealmChrome. */
.lobbyv3 .balance .currency:not(.rz-unit) { order: 2; }
.lobbyv3 .pc-actions { display: flex; flex-direction: column; gap: 8px; }

.lobbyv3 .quiet-button, .lobbyv3 .outline-button, .lobbyv3 .primary-button, .lobbyv3 .icon-button {
  border: 1px solid var(--line); border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center; gap: 9px; font-weight: 750;
  transition: transform .14s ease, border-color .14s ease, color .14s ease, background .14s ease;
}
.lobbyv3 .quiet-button:hover, .lobbyv3 .outline-button:hover, .lobbyv3 .primary-button:hover, .lobbyv3 .icon-button:hover { transform: translateY(-1px); }
.lobbyv3 .quiet-button {
  width: 100%; min-height: 44px; background: none; color: var(--text);
  font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  border-radius: 10px; padding: 0 14px; justify-content: space-between;
}
.lobbyv3 .quiet-button:hover { border-color: var(--muted); transform: none; }
.lobbyv3 .quiet-button svg { color: var(--muted); }
.lobbyv3 .quiet-button.wallet { justify-content: center; color: var(--accent); border-color: rgba(0, 229, 255, .42); }
.lobbyv3 .quiet-button.wallet:hover { border-color: var(--accent); background: rgba(0, 229, 255, .08); }
.lobbyv3 .quiet-button svg { width: 17px; height: 17px; }

/* Top padding trimmed below the side/bottom (owner 2026-07-31: "Player stats"
   floated with a void above it) — the lift + tighter row gap also buys the
   recent-runs list one-two extra visible rows. */
.lobbyv3 .stats-card { padding: 10px 16px 14px; display: grid; grid-template-rows: auto auto 1px minmax(0,1fr); gap: 8px; overflow: hidden; }
.lobbyv3 .section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 24px; }
.lobbyv3 .section-title h2 { margin: 0; font-family: var(--font-heading); font-size: 15px; font-weight: 700; letter-spacing: -.015em; }
.lobbyv3 .section-kicker { color: var(--muted-2); font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
/* Clickable kicker (e.g. "View all →") — a button reset back to the kicker look, in
   the accent colour so it reads as a link into the full history. */
.lobbyv3 .link-kicker { border: 0; background: transparent; padding: 0; color: var(--accent); font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; cursor: pointer; transition: color .14s ease; }
.lobbyv3 .link-kicker:hover { color: var(--accent-soft); }

.lobbyv3 .stats-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 7px; }
.lobbyv3 .stat-tile { min-width: 0; padding: 9px 10px; border: 1px solid var(--line); border-radius: 12px; background: rgba(9,17,29,.56); display: grid; gap: 4px; }
.lobbyv3 .stat-head { display: flex; align-items: center; gap: 7px; min-width: 0; color: var(--muted); font-size: 12px; }
.lobbyv3 .stat-head svg { width: 15px; height: 15px; color: var(--accent); flex: 0 0 auto; }
.lobbyv3 .stat-value { font-family: var(--font-mono); font-size: 16px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lobbyv3 .stat-value.positive { color: var(--success); }

.lobbyv3 .divider { height: 1px; background: var(--line); }

.lobbyv3 .recent-matches { min-height: 0; display: grid; grid-template-rows: auto minmax(0,1fr); gap: 8px; overflow: hidden; }
/* A flex column (not a grid) so each run keeps its natural height and the list
   SCROLLS when it outgrows the rail — the old grid crushed the rows into equal
   slivers on short screens and their content overlapped. */
.lobbyv3 .match-list { min-height: 0; display: flex; flex-direction: column; gap: 7px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: rgba(0,229,255,.25) transparent; padding-right: 2px; }
.lobbyv3 .match-list::-webkit-scrollbar { width: 5px; }
.lobbyv3 .match-list::-webkit-scrollbar-thumb { background: rgba(0,229,255,.2); border-radius: 3px; }
.lobbyv3 .match-row { flex: 0 0 auto; display: grid; grid-template-columns: 32px minmax(0,1fr) auto; gap: 9px; align-items: center; padding: 8px 9px; border: 1px solid rgba(205,226,248,.09); border-radius: 12px; background: rgba(8,15,26,.5); cursor: pointer; transition: border-color .14s ease, background .14s ease, transform .14s ease; }
.lobbyv3 .match-row:hover { border-color: rgba(0,229,255,.32); background: rgba(0,229,255,.05); transform: translateY(-1px); }
.lobbyv3 .match-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lobbyv3 .match-icon { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 10px; color: var(--accent); background: rgba(0,229,255,.06); border: 1px solid rgba(0,229,255,.18); }
.lobbyv3 .match-icon svg { width: 16px; height: 16px; }
.lobbyv3 .match-copy { min-width: 0; }
.lobbyv3 .match-name { font-size: 12px; font-weight: 700; color: #dce5ef; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lobbyv3 .match-meta { margin-top: 2px; color: var(--muted-2); font-family: var(--font-mono); font-size: 10px; white-space: nowrap; }
.lobbyv3 .match-result { font-family: var(--font-mono); font-size: 12px; font-weight: 700; color: var(--success); }
.lobbyv3 .match-result.loss { color: var(--danger); }
.lobbyv3 .match-empty { color: var(--muted-2); font-size: 12px; text-align: center; align-self: center; padding: 12px; }


/* REAL realm with a balance under the cheapest tier inserts #depositCta as a THIRD
   grid item, so it took the minmax(0,1fr) row meant for the tier grid: measured
   clientHeight 80 vs scrollHeight 139 at 1280x720, and since the panel is
   overflow:hidden the 59px surplus painted straight over the first row of cards.
   `.has-cta` (set in client.js next to the .hidden toggle — KEEP IN LOCKSTEP) adds a
   content-sized row for the CTA and keeps the flexible row on the tier grid. Demo,
   where the CTA is display:none, keeps the original 3-row template untouched — that
   template is what stops the tier grid leaving a dead band on tall screens. */
.lobbyv3 .arena-panel.has-cta { grid-template-rows: auto auto min-content minmax(0,1fr); }
/* …and with the CTA taking a row the cards drop to ~100px, which leaves the icon row
   ~8px. Drop the decorative glyph rather than render a clipped sliver of it — name,
   price and the button all stay. */
.lobbyv3 .arena-panel.has-cta .arena-icon { display: none; }
.lobbyv3 .arena-panel {
  padding: 14px; display: grid; grid-template-rows: auto auto minmax(0,1fr); gap: 10px;
  border: 1px solid var(--line); border-radius: var(--radius-xl);
  background: linear-gradient(180deg, rgba(255,255,255,.018), transparent 22%), rgba(15,25,42,.86);
  box-shadow: var(--shadow); overflow: hidden;
}
.lobbyv3 .mode-tabs { min-height: 52px; display: grid; grid-template-columns: repeat(3,1fr); padding: 5px; border: 1px solid var(--line); border-radius: 15px; background: rgba(8,15,27,.72); }
.lobbyv3 .mode-tab { min-width: 0; border: 0; border-radius: 11px; background: transparent; color: var(--muted); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 13px; font-weight: 720; white-space: nowrap; transition: background .15s ease, color .15s ease; }
.lobbyv3 .mode-tab svg { width: 16px; height: 16px; }
.lobbyv3 .mode-tab.active { color: #001218; background: linear-gradient(180deg, var(--accent-soft), var(--accent)); box-shadow: 0 10px 24px rgba(0,229,255,.15); }
/* Inactive mode tabs light up on hover like a tier card does (owner 2026-08-10) —
   same cyan family, deliberately WEAKER than .active's full fill so the selected
   state stays unambiguous. Tabs are borderless: tint background + text only (the
   tier recipe's border-color has nothing to paint here, and a lift would clip
   against the 5px pill grid). Guards skip the filled .active and "Team soon". */
.lobbyv3 .mode-tab:not(.active):not(:disabled):not(.tab-soon):hover { color: var(--text); background: rgba(0,229,255,.07); }
.lobbyv3 .mode-tab.tab-soon { cursor: not-allowed; color: #64748a; }
.lobbyv3 .mode-tab:disabled { cursor: not-allowed; color: #64748a; }
.lobbyv3 .soon { color: var(--accent); opacity: .68; font-family: var(--font-mono); font-size: 9px; letter-spacing: .07em; text-transform: uppercase; }

/* The mode hint states a RULE (the cash-out entry lock, the round length) and both
   halves are server-derived, so their length changes with tier/round config. It must
   therefore neither be clipped nor be allowed to blow out the column:
   · it used to be nowrap+ellipsis, which silently truncated the cash-out rule on
     phones ("…until eaten." — measured 572px of text in a 332px box);
   · and because a nowrap child defaults to min-width:auto, it refused to shrink and
     instead stretched the panel past its grid track (p 728px inside a 655px panel),
     rendering the tier cards clipped under the right rail.
   Wrap it, and keep min-width:0 on both boxes so it can never widen the column. */
.lobbyv3 .mode-copy { padding: 0 4px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; min-height: 24px; min-width: 0; }
/* min-height reserves exactly ONE line: both mode hints are written to fit it
   (client.js), so switching Open world ↔ Rounds can no longer shove the tier
   grid up and down. Keep the two in lockstep — a longer hint reintroduces the
   jump this reserves against. */
.lobbyv3 .mode-copy p { min-width: 0; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.35; min-height: 17px; }
.lobbyv3 .online-count { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; color: #c7d3df; font-family: var(--font-mono); font-size: 10px; }
.lobbyv3 .online-count::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--success); box-shadow: 0 0 10px rgba(114,239,189,.6); }

.lobbyv3 .tier-grid { min-height: 0; display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); grid-template-rows: repeat(3, minmax(0,1fr)); gap: 10px; }
/* The join-error line (style.css .error) reserves margin-top + min-height even when
   EMPTY — that was the dead strip under the tier grid (owner 2026-07-31 green-box
   screenshot). Collapse it until an error actually lands; the 1fr tier-grid row
   absorbs the freed height, so the cards stretch to the panel edge. */
.lobbyv3 #lobbyError { margin: 0; min-height: 0; }
.lobbyv3 #lobbyError:empty { display: none; }
.lobbyv3 .tier-card {
  min-height: 0; position: relative; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: linear-gradient(150deg, rgba(0,229,255,.025), transparent 44%), rgba(9,17,29,.68);
  display: grid; grid-template-rows: auto minmax(0,1fr) auto; gap: 9px; overflow: hidden;
  transition: border-color .15s ease, transform .15s ease, background .15s ease;
}
/* Hover lights up the tier the cursor is on (playable ones only). Replaces the old
   pinned-first-card highlight, which read as a permanent, unexplained selection. The
   pointed-at card lifts, brightens, and its Play button fills to draw the click. */
.lobbyv3 .tier-card:not(.is-disabled):hover,
.lobbyv3 .tier-card.is-selected {
  border-color: rgba(0,229,255,.68);
  background: linear-gradient(150deg, rgba(0,229,255,.09), transparent 46%), rgba(10,23,37,.9);
  box-shadow: 0 0 0 1px rgba(0,229,255,.06) inset, 0 14px 32px rgba(0,229,255,.08);
}
.lobbyv3 .tier-card:not(.is-disabled):hover { transform: translateY(-2px); }
.lobbyv3 .tier-card:not(.is-disabled):hover .arena-icon { color: rgba(0,229,255,.5); }
.lobbyv3 .tier-card:not(.is-disabled):hover .tier-play {
  color: #001218; border: 1px solid var(--accent);
  background: linear-gradient(180deg, var(--accent-soft), var(--accent));
  box-shadow: 0 10px 24px rgba(0,229,255,.16);
}
.lobbyv3 .tier-card.is-disabled { opacity: .58; }
.lobbyv3 .tier-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.lobbyv3 .tier-name { display: flex; align-items: center; gap: 9px; min-width: 0; font-family: var(--font-heading); font-size: 14px; font-weight: 720; letter-spacing: .14em; }
.lobbyv3 .tier-name svg { width: 19px; height: 19px; color: var(--accent); }
.lobbyv3 .tier-price { display: inline-flex; align-items: baseline; gap: 6px; font-family: var(--font-mono); color: var(--accent); white-space: nowrap; }
.lobbyv3 .tier-price span { font-size: 11px; }
.lobbyv3 .tier-price strong { font-size: 22px; letter-spacing: -.05em; }
/* `min-height: 0` lets this row be crushed by the card's grid whenever the panel is
   short (a REAL lobby with the deposit CTA, a laptop at 720px, a phone) — but the svg
   inside keeps its fixed px size and OVERFLOWS, landing on the "instant entry" line
   or straight on the Play button. The icon is decoration: cap it to its box and clip
   the remainder rather than let it paint over a control. */
.lobbyv3 .arena-icon { min-height: 0; display: grid; place-items: center; overflow: hidden; color: rgba(0,229,255,.32); }
.lobbyv3 .arena-icon svg { width: 52px; height: 52px; max-width: 100%; max-height: 100%; stroke-width: 1.5; }
.lobbyv3 .is-selected .arena-icon { color: rgba(0,229,255,.5); }
.lobbyv3 .tier-bottom { display: grid; gap: 8px; }
.lobbyv3 .tier-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 16px; color: var(--muted-2); font-family: var(--font-mono); font-size: 9px; text-transform: uppercase; letter-spacing: .06em; }
.lobbyv3 .tier-meta .live { color: var(--success); }
.lobbyv3 .primary-button, .lobbyv3 .tier-play { min-height: 40px; width: 100%; padding: 0 14px; border-radius: 11px; font-size: 13px; font-weight: 780; }
.lobbyv3 .primary-button, .lobbyv3 .tier-card.is-selected .tier-play {
  color: #001218; border: 1px solid var(--accent);
  background: linear-gradient(180deg, var(--accent-soft), var(--accent)); box-shadow: 0 10px 24px rgba(0,229,255,.16);
}
.lobbyv3 .tier-play { border: 1px solid rgba(0,229,255,.32); background: transparent; color: var(--accent); }
.lobbyv3 .tier-play:disabled { cursor: not-allowed; border-color: rgba(205,226,248,.13); background: rgba(205,226,248,.04); color: #718096; box-shadow: none; }

.lobbyv3 .leaderboard-card, .lobbyv3 .chat-card { padding: 16px; display: grid; min-height: 0; grid-template-rows: auto minmax(0,1fr) auto; gap: 10px; }
.lobbyv3 .card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.lobbyv3 .card-head h2 { margin: 0; display: inline-flex; align-items: center; gap: 9px; font-family: var(--font-heading); font-size: 16px; font-weight: 720; letter-spacing: -.025em; }
.lobbyv3 .card-head h2 svg { width: 18px; height: 18px; color: var(--accent); }
.lobbyv3 .head-link { border: 0; background: transparent; color: var(--accent); font-size: 11px; font-weight: 720; white-space: nowrap; }

.lobbyv3 .leader-list { min-height: 0; display: grid; grid-template-rows: repeat(10, minmax(0,1fr)); align-content: stretch; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.lobbyv3 .leader-row { position: relative; min-height: 0; display: grid; grid-template-columns: 27px minmax(0,1fr) auto; gap: 8px; align-items: center; border-bottom: 1px solid rgba(205,226,248,.045); font-size: 12px; }
/* Win rate given SHAPE: a 2px bar per row (the % itself, no new data). Top-3
   bars carry the accent; the rest fade so the eye lands on the podium. */
.lobbyv3 .leader-bar { position: absolute; left: 35px; bottom: -1px; height: 2px; max-width: calc(100% - 35px); background: var(--accent); opacity: .22; border-radius: 1px; pointer-events: none; }
.lobbyv3 .leader-row:nth-child(-n+3) .leader-bar { opacity: .45; }
.lobbyv3 .leader-row:nth-child(n+4) .leader-score { color: var(--text); }
.lobbyv3 .leaderboard-card { grid-template-rows: auto auto minmax(0, 1fr) auto; }
.lobbyv3 .lb-metric { margin: -4px 0 2px; color: var(--muted-2); font-family: var(--font-mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; }
.lobbyv3 .leader-row:last-child { border-bottom: 0; }
.lobbyv3 .leader-rank { color: var(--muted-2); font-family: var(--font-mono); font-size: 10px; }
/* Top-3 = gold / silver / bronze — the SAME palette as the full page's
   .lbrank.r1/.r2/.r3 (style.css), so the widget and the board tell one story.
   nth-child is safe here: every child of #miniLbList is a .leader-row. */
.lobbyv3 .leader-row:nth-child(1) .leader-rank { color: #ffd84d; text-shadow: 0 0 8px rgba(255, 216, 77, .4); }
.lobbyv3 .leader-row:nth-child(2) .leader-rank { color: #dbe4f2; text-shadow: 0 0 8px rgba(219, 228, 242, .35); }
.lobbyv3 .leader-row:nth-child(3) .leader-rank { color: #e08d4f; text-shadow: 0 0 8px rgba(224, 141, 79, .4); }
.lobbyv3 .leader-row.me .leader-name { color: var(--accent); font-weight: 700; }
.lobbyv3 .leader-name { color: #cbd6e2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lobbyv3 .leader-score { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 700; }
.lobbyv3 .you-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--accent); font-size: 11px; font-weight: 720; }
.lobbyv3 .you-row span:last-child { color: var(--muted-2); font-family: var(--font-mono); font-size: 9px; text-transform: uppercase; letter-spacing: .08em; }
.lobbyv3 .rank-pips { display: inline-flex; align-items: center; gap: 4px; }
.lobbyv3 .rank-pips b { width: 6px; height: 6px; border-radius: 50%; border: 1px solid rgba(139,152,179,.45); }
.lobbyv3 .rank-pips b.on { border-color: var(--accent); background: var(--accent); box-shadow: 0 0 6px rgba(0,229,255,.5); }
.lobbyv3 .rank-pips em { font-style: normal; margin-left: 4px; }

/* Bottom-anchored chat: flex column + `margin-top:auto` on the first message pins the
   log to the bottom when it isn't full yet (new lines arrive at the bottom and push the
   rest up), and collapses to 0 once it overflows so the whole history stays scrollable
   (no top-clip). Replaces `align-content:start`, which stacked messages from the top. */
.lobbyv3 .chat-log { min-height: 0; display: flex; flex-direction: column; gap: 9px; overflow-y: auto; overflow-x: hidden; padding-top: 3px; scrollbar-width: thin;
  /* soften the hard mid-line clip at the top of the scrollback */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 26px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 26px); }
.lobbyv3 .chat-log > :first-child { margin-top: auto; }
.lobbyv3 .chat-log::-webkit-scrollbar { width: 5px; }
.lobbyv3 .chat-log::-webkit-scrollbar-thumb { background: rgba(0,229,255,.2); border-radius: 3px; }
/* Inline flow (not a 2-col grid): the nickname sits inline before the text so a
   wrapped line returns to the LEFT edge (under the nick), not a hanging indent under
   the message body (owner report). */
/* Hanging indent: wrapped lines tuck under the message, not under the nick —
   the flat wrap was what made the log read as one solid text wall. */
.lobbyv3 .chat-message { font-size: 12px; line-height: 1.4; padding-left: 14px; text-indent: -14px; }
.lobbyv3 .chat-user { font-weight: 700; margin-right: 6px; }
.lobbyv3 .chat-message.me .chat-user { color: var(--text); }
.lobbyv3 .chat-user { color: var(--accent); font-weight: 720; white-space: nowrap; }
.lobbyv3 .chat-text { color: #c4d0dd; }
/* System rows (REAL realm): the arena feed's settlements rendered into the chat
   card — server events, not speakers, so they read dimmer + mono, no username. */
.lobbyv3 .chat-message.sys { padding-left: 0; text-indent: 0; }
.lobbyv3 .chat-message.sys .chat-text { color: #7f8ca6; font-family: var(--font-mono); font-size: 11px; }
.lobbyv3 .chat-message.sys b { color: var(--green); font-weight: 600; }
.lobbyv3 .chat-form { display: grid; grid-template-columns: minmax(0,1fr) 42px; gap: 8px; }
.lobbyv3 .chat-input { min-width: 0; height: 42px; padding: 0 13px; border: 1px solid var(--line); border-radius: 12px; background: rgba(7,14,24,.72); color: var(--text); font-family: var(--font-mono); font-size: 11px; }
.lobbyv3 .chat-input::placeholder { color: #617188; }
.lobbyv3 .chat-input:focus { outline: none; border-color: rgba(0,229,255,.45); }
.lobbyv3 .chat-send.ready { background: rgba(0,229,255,.14); border-color: rgba(0,229,255,.6); }
.lobbyv3 .icon-button { width: 42px; height: 42px; padding: 0; color: var(--accent); border-color: rgba(0,229,255,.34); background: transparent; }
.lobbyv3 .icon-button svg { width: 17px; height: 17px; }

/* Ticker is the game's fixed bottom strip → here it's the 3rd grid row. */
.lobbyv3 .lobby-ticker {
  min-width: 0; display: grid; grid-template-columns: auto minmax(0,1fr); align-items: center; gap: 16px;
  padding: 0 clamp(16px, 2.2vw, 34px); border-top: 1px solid var(--line); background: rgba(6,12,21,.88); overflow: hidden;
}
.lobbyv3 .ticker-label { position: relative; z-index: 2; display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; white-space: nowrap; background: rgba(6,12,21,.92); } /* opaque plate + on top: the scrolling tape can never show to its LEFT (owner) */
.lobbyv3 .ticker-label svg { width: 15px; height: 15px; }
/* CANONICAL MARQUEE (owner bug: tape ran LEFT of the label + emptied on the right).
   The VIEWPORT is the static clip box that lives in grid col 2; the TRACK animates
   INSIDE it. Previously overflow:hidden + the translateX animation were on the SAME
   element, so translating the track moved its clip box too — it slid left of the label
   and out of the cell. Split them: viewport clips (never moves), track scrolls. */
.lobbyv3 .ticker-viewport { min-width: 0; overflow: hidden; mask-image: linear-gradient(90deg, transparent 0, black 2%, black 96%, transparent 100%); }
.lobbyv3 .ticker-track { display: flex; align-items: center; gap: 22px; width: max-content; color: var(--muted); font-size: 11px; white-space: nowrap; }
.lobbyv3 .ticker-track > * { flex: 0 0 auto; }
.lobbyv3 .ticker-track { animation: lobby-ticker-scroll 20s linear infinite; will-change: transform; } /* duration here is a FALLBACK only — renderTicker() (client.js) overrides animation-duration inline (items × TICKER_SEC_PER_ITEM); change the speed THERE */
@keyframes lobby-ticker-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .lobbyv3 .ticker-track { animation: none; } }
.lobbyv3 .ticker-item { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; }
.lobbyv3 .ticker-item::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: .7; }
.lobbyv3 .ticker-item strong { color: var(--success); font-family: var(--font-mono); }
.lobbyv3 .ticker-item .tier { color: #708097; }

/* Help modal (its own overlay; the game's #infoBtn opens it) */
.lobbyv3 .help-modal { position: fixed; inset: 0; z-index: 20; display: none; place-items: center; padding: 20px; background: rgba(2,7,13,.72); backdrop-filter: blur(12px); }
.lobbyv3 .help-modal.is-open { display: grid; }
.lobbyv3 .help-panel { width: min(100%, 470px); padding: 22px; border: 1px solid var(--line-strong); border-radius: var(--radius-xl); background: var(--surface-strong); box-shadow: var(--shadow); }
.lobbyv3 .help-panel .card-head { margin-bottom: 16px; }
.lobbyv3 .help-steps { display: grid; gap: 10px; }
.lobbyv3 .help-step { display: grid; grid-template-columns: 34px 1fr; gap: 11px; align-items: center; color: var(--muted); font-size: 13px; line-height: 1.4; }
.lobbyv3 .help-step span:first-child { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid rgba(0,229,255,.22); border-radius: 10px; color: var(--accent); font-family: var(--font-mono); font-size: 11px; }

.lobbyv3 button:focus-visible, .lobbyv3 input:focus-visible, .lobbyv3 .tier-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

@media (max-width: 1240px) and (min-width: 1121px) {
  .lobbyv3 .dashboard { grid-template-columns: 242px minmax(470px,1fr) 280px; gap: 12px; padding-inline: 12px; }
  .lobbyv3 .player-card { padding: 16px; }
  .lobbyv3 .stats-card, .lobbyv3 .leaderboard-card, .lobbyv3 .chat-card { padding: 13px; }
  .lobbyv3 .arena-panel { padding: 11px; }
  .lobbyv3 .tier-card { padding: 11px; }
  .lobbyv3 .arena-icon svg { width: 43px; height: 43px; }
  .lobbyv3 .tier-price strong { font-size: 19px; }
  .lobbyv3 .chat-message, .lobbyv3 .leader-row { font-size: 11px; }
}
@media (max-height: 780px) and (min-width: 1121px) {
  #lobby.lobbyv3 { grid-template-rows: 58px minmax(0,1fr) 32px; }
  .lobbyv3 .dashboard { padding-block: 10px; gap: 10px; }
  .lobbyv3 .left-rail, .lobbyv3 .right-rail { gap: 10px; }
  .lobbyv3 .player-card { padding-block: 13px; gap: 8px; }
  .lobbyv3 .avatar { width: 60px; height: 60px; }
  .lobbyv3 .nickname { font-size: 17px; }
  .lobbyv3 .balance .amount { font-size: 21px; }
  .lobbyv3 .quiet-button { min-height: 38px; }
  .lobbyv3 .stats-card { padding: 8px 12px 11px; gap: 7px; }
  .lobbyv3 .stat-tile { padding: 8px; gap: 4px; }
  .lobbyv3 .stat-value { font-size: 14px; }
  .lobbyv3 .match-row { padding: 6px 7px; }
  .lobbyv3 .match-icon { width: 27px; height: 27px; }
  .lobbyv3 .arena-panel { gap: 7px; padding: 10px; }
  .lobbyv3 .mode-tabs { min-height: 44px; }
  .lobbyv3 .tier-grid { gap: 7px; }
  .lobbyv3 .tier-card { padding: 9px; gap: 5px; }
  .lobbyv3 .arena-icon svg { width: 36px; height: 36px; }
  /* .tier-meta used to be display:none here — the short-desktop compression
     sacrificed the LIVE/INSTANT ENTRY row. Owner 2026-08-12: mobile and PWA
     show per-room LIVE, desktop must too (it is honest server data, and the
     cards have empty middle to spare). Measured after restoring: ≤780-tall
     desktop keeps 0 panel overflow — the 1fr glyph track absorbs the 16px. */
  .lobbyv3 .primary-button, .lobbyv3 .tier-play { min-height: 34px; }
  .lobbyv3 .leaderboard-card, .lobbyv3 .chat-card { padding: 12px; gap: 7px; }
  .lobbyv3 .chat-log { gap: 6px; }
  .lobbyv3 .chat-input, .lobbyv3 .icon-button { height: 36px; }
  .lobbyv3 .lobby-ticker { gap: 10px; }
}

/* ── Phones (≤820px) — the REAL mobile lobby (owner 2026-08-01) ──────────────
   Until now the v3 dashboard kept its 3-column desktop grid at every width
   below 821px (style.css's phone rules target the pre-v3 .page-body/.sidebar
   markup, which no longer exists), so a phone rendered the desktop layout
   cropped, with horizontal panning. One column now, driven by the ZONE tabs
   (#lobby[data-zone] — set by setLobbyZone, default "play" in the markup):
     play  → the arena (mode tabs + tier grid); profile/stats live in Board
     board → player card + stats + recent runs + leaderboard, one scroll
     chat  → the chat card, input pinned to the bottom
   The tab bar is shown HERE, not via style.css's phone block — that block is
   aspect-gated (max-aspect-ratio: 13/9) and vanished on landscape phones,
   which would strand the zones with no way to switch.
   The second media term is LANDSCAPE PHONES (932x430 etc): by width they fall
   into the 821-1120 tablet band, which stacked the full desktop dashboard —
   a giant player card first and the arena two screens down (owner prod
   screenshot). Tiny height = it is a phone; give it the zone layout too. */
@media (max-width: 820px), ((max-width: 1120px) and (max-height: 500px)) {
  #lobby.lobbyv3 {
    height: 100dvh;
    display: flex; flex-direction: column;
    overflow: hidden;
  }
  /* min-height must RIDE the status-bar inset: border-box means padding-top
     counts INTO min-height, so a bare 54px would crush the row's content to
     ~7px under a 47px Dynamic-Island inset. */
  .lobbyv3 .lobby-topbar { flex: none; min-height: calc(54px + env(safe-area-inset-top, 0px)); }
  .lobbyv3 .brand { font-size: 22px; }
  .lobbyv3 .help-button { width: 38px; height: 38px; }
  .lobbyv3 .avatar-chip { width: 38px; height: 38px; }
  .lobbyv3 .dashboard {
    flex: 1; min-height: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: auto;
    gap: 10px; padding: 10px;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  /* Zone switching. display:none beats the rails' base display:grid on
     specificity (the id selector), and .waiting's own rules still outrank
     these where they overlap. */
  #lobby[data-zone="play"] .left-rail, #lobby[data-zone="play"] .right-rail { display: none; }
  #lobby[data-zone="board"] .center-col, #lobby[data-zone="board"] .chat-card { display: none; }
  /* Board = stats + recent runs + leaderboard. The player card (avatar, name,
     realm badge, balance, Profile button) used to sit on top of it — owner
     2026-08-04: the profile belongs behind the Profile button, not above the
     leaderboard. Its two jobs moved: identity → the topbar avatar chip
     (variant A), balance + wallet → the topbar balance chip beside it. */
  #lobby[data-zone="board"] .player-card { display: none; }
  .lobbyv3 .avatar-chip { display: grid; }
  .lobbyv3 .balance-chip {
    display: inline-flex; align-items: center; gap: 6px;
    height: 38px; padding: 0 12px;
    border: 1px solid var(--line); border-radius: 12px;
    background: rgba(12, 20, 34, .78);
    color: var(--muted); font-family: var(--font-mono);
    transition: border-color .15s ease, box-shadow .15s ease;
  }
  #lobby[data-zone="chat"] .center-col, #lobby[data-zone="chat"] .left-rail,
  #lobby[data-zone="chat"] .leaderboard-card { display: none; }
  /* Rails stack their cards; heights come from content, the dashboard scrolls. */
  .lobbyv3 .left-rail, .lobbyv3 .right-rail { grid-template-rows: none; grid-auto-rows: auto; min-width: 0; }
  /* Board block-flow rules live in their OWN media block right below — they
     must NOT fire in the ≤500px-tall landscape split (which keeps its two
     side-by-side rail scrollers and was healthy; block flow there stacked the
     rails and drowned the leaderboard under an overflow:hidden dashboard —
     caught live on the sim 2026-08-12). */
  .lobbyv3 .center-col { min-width: 0; }
  /* Play: the arena owns the zone height; the tier grid scrolls inside it if it
     must (short landscape), instead of stretching the page. */
  #lobby[data-zone="play"] .dashboard { grid-template-rows: minmax(0, 1fr); overflow: hidden; }
  #lobby[data-zone="play"] .arena-panel { overflow-y: auto; -webkit-overflow-scrolling: touch; }
  /* Chat: the card fills the zone so the input sits at the bottom, log scrolls. */
  #lobby[data-zone="chat"] .dashboard { grid-template-rows: minmax(0, 1fr); overflow: hidden; }
  #lobby[data-zone="chat"] .right-rail { grid-template-rows: minmax(0, 1fr); }
  /* Compact cards. */
  .lobbyv3 .player-card { padding: 14px; gap: 8px; }
  .lobbyv3 .avatar { width: 58px; height: 58px; border-width: 2px; }
  /* Tier cards at half a phone width: shrink the type so the price and the
     meta row fit instead of clipping at the card edge (seen at 390px). */
  /* Row FLOOR + STRETCH (2026-08-11, owner PWA screenshots): the floor stays —
     the base minmax(0,1fr) rows COMPRESSED on short Safari landscapes (~330px:
     rows crushed to ~10px, every overflow:hidden card self-clipped, nothing to
     scroll) and 96px turns crush into overflow that the arena-panel's
     overflow-y:auto actually scrolls. But the old `auto` MAX left a dead band
     on TALL viewports: the chrome-less PWA portrait is ~100px taller than the
     browser and the cards just stopped, leaving empty panel above the ticker.
     1fr with the same floor: short screens overflow+scroll exactly as before,
     tall screens stretch the cards to fill the panel. height:100% (not
     align-self stretch) is load-bearing: inside the scrollable arena-panel the
     grid item refuses to stretch into its 1fr track (measured 473px in a 573px
     track), while 100% resolves against the definite track and fills it —
     and on short screens 100% + the 96px floors still overflow into the
     panel's scroller exactly like before. */
  .lobbyv3 .tier-grid { height: 100%; grid-template-rows: repeat(3, minmax(96px, 1fr)); }
  .lobbyv3 .tier-card { padding: 10px; gap: 6px; }
  .lobbyv3 .tier-name { font-size: 12px; letter-spacing: .1em; gap: 6px; }
  .lobbyv3 .tier-name svg { width: 16px; height: 16px; }
  .lobbyv3 .tier-price strong { font-size: 16px; }
  .lobbyv3 .tier-price span { font-size: 9px; }
  /* Glyph vs the LIVE/entry line (two failed rounds, keep both lessons):
     round 1: `min-height: 0` + a bare svg height let the crushed box overflow
     onto .tier-meta → "reserved" the svg's height via min-height: 34px.
     Round 5 (owner iPhone screenshots 2026-08-11): that reserve is itself an
     overflow — the card grid's middle track is minmax(0,1fr), so a short card
     (the 812px small-viewport PWA, SE-class phones) shrinks the TRACK below
     the item's min-height and the 34/46px icon paints over the meta line
     anyway. Fix: no reserve; the svg's height:100% chain (set here, capped by
     max-height in the base rule) makes the glyph SHRINK with its track — small
     glyph on a tight card, never ink over text. */
  .lobbyv3 .arena-icon { min-height: 0; }
  .lobbyv3 .arena-icon svg { width: 34px; height: min(34px, 100%); }
  .lobbyv3 .tier-meta { font-size: 9px; letter-spacing: .08em; }
  /* Phone: the online-count chip sits beside the (now wrapping) hint, so stack them
     rather than squeezing the rule into a narrow column. */
  .lobbyv3 .mode-copy { flex-direction: column; gap: 4px; }
  .lobbyv3 .tier-play, .lobbyv3 .primary-button { min-height: 38px; }
  .lobbyv3 .lobby-ticker { flex: none; }
  /* The tab bar (style.css hides it globally; its phone block is aspect-gated). */
  #lobby.lobbyv3 .lobby-tabs {
    flex: none; display: flex;
    border-top: 1px solid var(--line);
    background: rgba(8, 16, 28, .94);
    /* Not the full inset (owner's green-pen: the 34px band read as dead
       space): the home indicator may overlap the bar's lower padding — the
       touch targets sit above it, which is standard native-app behavior. */
    padding-bottom: max(env(safe-area-inset-bottom, 0px) - 14px, 0px);
  }
  #lobby.lobbyv3 .lobby-tabs button {
    flex: 1; display: grid; justify-items: center; gap: 3px;
    /* Trimmed 2026-08-11 (owner PWA screenshot): with the home-indicator inset
       under it the old 8/9px read as a fat dead band at the screen bottom. */
    padding: 5px 0 6px; border: 0; background: none;
    color: var(--muted); font-size: 12px;
  }
  #lobby.lobbyv3 .lobby-tabs button svg { width: 18px; height: 18px; }
  #lobby.lobbyv3 .lobby-tabs button.active { color: var(--accent); }
  #lobby.lobbyv3.waiting .lobby-tabs { display: none; }
  /* The dashboard must be BOUNDED here, not content-sized: #lobby is
     height:100dvh + overflow:hidden on phones, so an unbounded row lets the
     wait pane grow past the screen and hard-clips its footer (the cancel /
     leave button). Bounded row + the pane's own overflow-y = a real scroller. */
  .lobbyv3.waiting .dashboard {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    flex: 1; min-height: 0;
    gap: 10px; padding: 10px;
  }
  .lobbyv3.waiting .left-rail, .lobbyv3.waiting .right-rail { display: none; }
  .lobbyv3.waiting .center-col { min-width: 0; }
  .lobbyv3.waiting .lobby-ticker { display: none; }
}

/* ═══ Board zone, PORTRAIT phones (2026-08-12). min-height:501 is the EXACT
   complement of the landscape split (≤1120 & ≤500h keeps two side-by-side rail
   scrollers — block flow there stacked the rails and drowned the leaderboard,
   caught on the sim) — keep the two gates in lockstep.
   BLOCK flow on purpose, not a tidier grid: the desktop cards are nested grids
   with minmax(0,1fr) + overflow:hidden tuned for the no-scroll desktop. Every
   such box is a scroll container whose automatic minimum size is 0, and WebKit
   (the real device) legally compressed the whole chain to viewport-fit — the
   Recent-runs list rendered as a half-row sliver no matter what floors the
   inner tracks were given (probe-traced on the simulator; Blink distributes
   free space differently and MASKS it — browser screenshots are not evidence
   for this bug class). Block children can never be compressed below content
   height: cards size to content, the dashboard's "one scroll" finally engages.
   Content is bounded: runs ≤10 (client.js slice), leaderboard ≤10 + You. The
   stat tiles / list rows inside keep their own grid/flex layouts. */
@media (max-width: 820px) and (min-height: 501px) {
  #lobby[data-zone="board"] .dashboard { display: block; }
  #lobby[data-zone="board"] .left-rail, #lobby[data-zone="board"] .right-rail { display: block; }
  #lobby[data-zone="board"] .right-rail { margin-top: 10px; } /* the grid gap the block flow dropped */
  #lobby[data-zone="board"] .stats-card { display: block; overflow: visible; }
  #lobby[data-zone="board"] .recent-matches { display: block; overflow: visible; margin-top: 8px; }
  #lobby[data-zone="board"] .match-list { overflow: visible; margin-top: 8px; }
  #lobby[data-zone="board"] .leaderboard-card { display: block; overflow: visible; }
}

/* Landscape phones (short viewport, owner screenshots 2026-08-10): the vertical
   chrome — topbar (~54) + ticker + bottom tab bar (~60) — ate most of ~390px and
   every zone clipped mid-card. Same-markup regrade, refining the mobile block
   above (this media is a strict subset of its second clause):
   the tab bar becomes a LEFT RAIL (a grid column, freeing ~60px of height), the
   topbar slims to one 44px line, tiers go 3-across × 2 rows (landscape is wide,
   not tall), the Board zone splits into two side-by-side scrolling halves.
   Page-level scroll stays OFF — lists scroll inside their own cards, exactly the
   portrait rule. The PWA gets the same layout with more height to spend. */
@media (max-width: 1120px) and (max-height: 500px) {
  #lobby.lobbyv3 {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
  }
  /* Tab bar → left rail. Overrides the phone block's flex row; .waiting still
     hides it entirely (display:none there wins by order + specificity). */
  /* Rail owns ONLY the middle row (2026-08-11 second pass, owner's green-pen
     screenshots): with grid-row 1/-1 the corners above and below the icon
     stack were dead black holes — the topbar and the ticker now span the FULL
     width instead, so the rail column is bounded by real chrome on both ends. */
  #lobby.lobbyv3 .lobby-tabs {
    grid-column: 1; grid-row: 2;
    flex-direction: column; justify-content: center; gap: 4px;
    /* Notch side in landscape: the inset must WIDEN the rail, not squeeze it —
       box-sizing is border-box, so a bare width:64px with the env padding left
       the icons ~17px and clipped them INTO the notch (owner PWA screenshot
       2026-08-11). Rotated the other way the inset is 0 and this is plain 64px. */
    width: calc(64px + env(safe-area-inset-left, 0px));
    border-top: 0; border-right: 1px solid var(--line);
    padding-bottom: 0;
    padding-left: env(safe-area-inset-left, 0px);
  }
  #lobby.lobbyv3 .lobby-tabs button { flex: 0 0 auto; padding: 10px 0; font-size: 10px; }
  .lobbyv3 .lobby-topbar {
    grid-column: 1 / -1; grid-row: 1;
    min-height: calc(44px + env(safe-area-inset-top, 0px));
    /* Full-width now → its LEFT edge is the viewport edge: take the notch
       inset (the base rule only carries the right one). */
    padding-left: calc(clamp(18px, 2.6vw, 42px) + env(safe-area-inset-left, 0px));
  }
  .lobbyv3 .brand { font-size: 18px; }
  .lobbyv3 .help-button, .lobbyv3 .avatar-chip { width: 32px; height: 32px; }
  .lobbyv3 .balance-chip { height: 32px; padding: 0 10px; font-size: 12px; }
  /* Right inset on the content column + ticker: rotated so the notch sits on
     the RIGHT, the dashboard/tape edge is the viewport edge (the left rail
     only covers the left rotation). The ticker spans the FULL width (kills
     the bottom-left corner hole) and deliberately takes NO bottom inset: the
     home indicator overlays the decorative tape harmlessly, while an inset
     here stole 21px straight out of the tier cards and clipped their Play
     buttons (owner's green-pen screenshot — browser env()=0 hid it). */
  .lobbyv3 .dashboard { grid-column: 2; grid-row: 2; gap: 8px; padding: 8px calc(8px + env(safe-area-inset-right, 0px)) 8px 8px; }
  .lobbyv3 .lobby-ticker {
    grid-column: 1 / -1; grid-row: 3;
    padding-left: calc(clamp(16px, 2.2vw, 34px) + env(safe-area-inset-left, 0px));
    padding-right: calc(clamp(16px, 2.2vw, 34px) + env(safe-area-inset-right, 0px));
  }
  /* Play: three tier cards per row, two rows. The decorative arena icon is
     dropped here — it costs ~34px of vertical per card, and on a short landscape
     that was the difference between a whole card and one that self-clipped its
     own Play button (.tier-card is overflow:hidden). Row floor sized to the
     REMAINING content (name + meta + button ≈ 96px) so nothing clips; the panel
     scrolls if a given height still can't hold both rows. */
  #lobby[data-zone="play"] .arena-icon { display: none; }
  .lobbyv3 .tier-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    /* Row floor = the tallest card's real content, not a magic number. The old
       96px was measured for the ≤820 phone paddings; at 821-1120 width the base
       paddings are fatter and every Play button clipped 4px (measured 900×360),
       and the 401-500 block below re-adds the glyph (+24px track) which the
       number also didn't know about. min-content tracks all of it by
       construction; any true shortfall overflows into the panel scroller. */
    grid-template-rows: repeat(2, minmax(min-content, 1fr));
    gap: 6px; /* part of the band-wide compact set below (2026-08-12) */
  }
  /* The overflow VALVE for this whole ≤500-tall band. The ≤820 phone block has
     the same rule, but 821–1120-wide landscape (the real PWA is 874×402) ran
     the base overflow:hidden — when the rows below can't fit, the second card
     row was cut at the panel edge with no way to reach its Play buttons
     (owner screenshot 2026-08-12). Cards may overflow; the panel scrolls. */
  #lobby[data-zone="play"] .arena-panel { overflow-y: auto; -webkit-overflow-scrolling: touch; }
  /* Compact vertical spend for the WHOLE ≤500 band (2026-08-12, owner sim
     874×402): with base paddings the icon-less card fit only barely — the
     second row's Play still dipped ~26px under the ticker and the panel
     scrolled ("открываются не полными"). These are the SAME values the
     ≥420 glyph block has always run (proven on the taller PWAs — identical
     there, so this changes nothing for them); band-wide they buy ~80px of
     slack, and both card rows sit fully above the ticker with real margin.
     The valve above stays as the safety net, not as the design. */
  .lobbyv3 .arena-panel { padding: 9px; gap: 6px; }
  .lobbyv3 .tier-card { padding: 7px 10px; gap: 4px; }
  .lobbyv3 .tier-play { min-height: 32px; }
  /* Portrait stacked these to survive a narrow column; landscape has the width. */
  .lobbyv3 .mode-copy { flex-direction: row; align-items: center; gap: 10px; }
  /* Board: stats/runs beside the leaderboard instead of under it — each half
     is its own scroller, the page still never scrolls. */
  #lobby[data-zone="board"] .dashboard {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    overflow: hidden;
  }
  #lobby[data-zone="board"] .left-rail,
  #lobby[data-zone="board"] .right-rail { overflow-y: auto; -webkit-overflow-scrolling: touch; min-height: 0; }
  /* The waiting pane keeps the phone block's single-column bounded layout; with
     the tabs hidden the rail column collapses to zero on its own (width:auto) —
     which ALSO collapses the safe-area cover the rail provided: the wait pane's
     content then starts at the raw viewport edge and the STAKE column sat
     behind the notch (owner PWA screenshots 2026-08-11). Give the waiting
     dashboard the insets directly; zero on notch-less devices/rotations. */
  .lobbyv3.waiting .dashboard {
    padding-left: calc(10px + env(safe-area-inset-left, 0px));
    padding-right: calc(10px + env(safe-area-inset-right, 0px));
  }
}

/* Landscape with HEIGHT to spend (the chrome-less PWA: ~430px vs a browser's
   ~330-360) — the icon-drop above was sized for the browser's worst case and
   left the taller PWA with bare squat cards (owner screenshots 2026-08-11).
   The glyph returns AND the card's vertical spend is compressed to leave a
   real slack margin: round 2 of the owner's screenshots proved a 3px margin
   dies to device font/metric drift — the Play label clipped at the card
   edge. Compression buys ~20px per card. Browser landscape (≤400px after
   its own chrome) keeps the compact icon-less card untouched.
   FLOOR MOVED 401 → 440 (2026-08-12, owner sim report "открываются не
   полными"). The 401 floor was a guess at "the PWA is ~430 tall"; MEASURED,
   the glyph track costs ~56px across the two rows, so it only fits with real
   margin from ~440 up:
     419 no glyph → 22px clearance   ·   420 WITH glyph → 2px OVER the panel
     440 with glyph → 18px clearance ·   956×440 (17 Pro Max) → 18px, no scroll
   A landscape PWA's height is the device's SHORT side, i.e. 375/390/393/402/
   414/428/430/440 — so a 420 floor would have re-clipped every 414-430 phone
   (the 2026-08-11 lesson: a sub-10px margin dies to device font drift). 440
   puts the boundary where the glyph genuinely fits; everything below keeps
   the PROVEN icon-less compact card (fits at 360, so 402-430 have slack).
   A missing decorative glyph is cosmetic; a clipped Play button is the bug. */
@media (max-width: 1120px) and (max-height: 500px) and (min-height: 440px) {
  #lobby[data-zone="play"] .arena-icon { display: grid; }
  .lobbyv3 .arena-panel { padding: 9px; gap: 6px; }
  .lobbyv3 .tier-grid { gap: 6px; }
  /* The glyph's guaranteed slot lives on the TRACK (minmax min), not on the
     item — an item min-height overflows a crushed track onto the LIVE/entry
     text (the portrait round-5 bug), while a track min makes the CARD the
     overflow unit, and the arena panel's scroller absorbs that. Round 5b:
     the first round-5 cut dropped the reserve entirely and the 1fr track
     collapsed to ~8px — «милипиздрические» glyphs (owner screenshot). */
  .lobbyv3 .tier-card { padding: 7px 10px; gap: 4px; grid-template-rows: auto minmax(24px, 1fr) auto; }
  .lobbyv3 .tier-play { min-height: 32px; }
  .lobbyv3 .arena-icon svg { width: 24px; height: min(24px, 100%); }
  /* Row floor: none needed here — the landscape block's minmax(min-content, 1fr)
     already counts the glyph track this block re-adds. (A 96px floor sized for
     the icon-less card once lived there and clipped every Play button 11px
     through the card's overflow:hidden — owner screenshot 874×402.) */
}

/* Portrait with height to spend (the same chrome-less PWA, ≥700px tall): the
   stretched cards left a small 34px glyph adrift in empty middle space
   (owner round-3 screenshots) — scale the glyph to the room it now has.
   Short/old portrait phones (<700px) keep the compact 34px.
   min(46px, 100%), no min-height reserve — same round-5 rule as the phone
   block above: the glyph shrinks with a tight card instead of overflowing
   onto the LIVE/entry line.
   FLOOR MOVED 700 → 780 (2026-08-12d, owner's roundz.gg Safari screenshot).
   The 700 floor was the SAME family bug as the landscape 401: "≥700 tall"
   was meant to mean "the chrome-less PWA", but Safari-with-chrome on a
   874pt phone reports ~700-724 and fell into the block. Its 46px glyph +
   min-content rows force 163px cards; MEASURED at width 390-402:
     715 → 52px overflow, last Play 40px past the panel (the owner's shot;
           and BEFORE the 08-12 sweep this exact height silently clipped
           10px of the Play button INSIDE the card — it was never right)
     780 → fits, 24px clearance  ·  812 (mini/X PWA) → fits, 24px clearance
   Real heights near the line: browsers ≤724 (→ base compact, which fits
   with 11-24px clearance), PWA portraits 812/844/852/874+ (→ this block).
   Nothing ships between 725 and 811 except desktop windows. */
@media (max-width: 820px) and (min-height: 780px) {
  /* Track min, not item min — see the landscape block right above. Tall
     portrait has the budget (measured: 24px clearance at 780/812, 48px of
     slack on the 874pt PWA); short phones stay on the base minmax(0,1fr) +
     shrinking svg, where a guaranteed slot would clip the Play button. */
  .lobbyv3 .tier-card { grid-template-rows: auto minmax(46px, 1fr) auto; }
  .lobbyv3 .arena-icon svg { width: 46px; height: min(46px, 100%); }
  /* Same floor-vs-content arithmetic as the landscape fix: the phone block's
     96px row floor (line ~591) predates this block's 46px glyph track, so the
     card min (~163px) could exceed the row on 700-760px-tall portraits (silent
     clip) and made card heights FLIP between engine layout states when a
     deposit-CTA row or an error line ate the margin (agent audit 2026-08-12).
     min-content keys the floor to the real card; deficits go to the panel
     scroller. Short portraits <700 keep the base shrink-only behavior. */
  .lobbyv3 .tier-grid { grid-template-rows: repeat(3, minmax(min-content, 1fr)); }
}

/* Narrow-desktop / tablet (821–1120px) — a real layout gap: the desktop viewport
   grid-lock is ≥1121px only and the mobile zone-switcher is ≤820px only, so this band
   was left running the BASE 3-column .dashboard (min ~1094px), which overflowed and
   CLIPPED the right rail (owner report: "что за нахуй когда сжимаешь"). Stack it into
   one scrolling column and release the fixed-height panes so nothing is cut off. */
@media (max-width: 1120px) and (min-width: 821px) and (min-height: 501px) {
  .lobbyv3 .dashboard {
    grid-template-columns: minmax(0, 1fr);
    gap: 14px; padding: 14px clamp(14px, 3vw, 30px);
    align-content: start;
  }
  /* Rails/cards were fixed-height for the no-scroll desktop; let content set the height
     so the PAGE scrolls instead of crushing or clipping each pane. */
  .lobbyv3 .left-rail, .lobbyv3 .right-rail { grid-template-rows: auto; }
  .lobbyv3 .stats-card, .lobbyv3 .arena-panel,
  .lobbyv3 .leaderboard-card, .lobbyv3 .chat-card,
  .lobbyv3 .recent-matches { overflow: visible; min-height: 0; }
  .lobbyv3 .arena-panel { grid-template-rows: auto auto auto; }
  .lobbyv3 .tier-grid { grid-template-rows: repeat(3, minmax(116px, auto)); }
  /* Cap the two internal scrollers so a long list doesn't balloon the stack. */
  .lobbyv3 .match-list { max-height: 240px; overflow-y: auto; }
  .lobbyv3 .chat-log { max-height: 260px; }
  /* The wait screen takes over this band too (review 2026-07-31). The takeover
     used to stop at 820px while the dashboard stays a STACK up to 1120px, so a
     tablet, a landscape phone, or a desktop at 150% zoom rendered the screen
     shown right before money moves as one block buried in the full lobby —
     countdown and leave button hundreds of px down the page. Same shape as the
     phone rule: rails and ticker out, the wait pane owns the viewport. */
  .lobbyv3.waiting .dashboard {
    grid-template-rows: minmax(0, 1fr);
    min-height: calc(100dvh - 28px);
    align-content: stretch;
  }
  .lobbyv3.waiting .left-rail, .lobbyv3.waiting .right-rail { display: none; }
  .lobbyv3.waiting .lobby-ticker { display: none; }
  .lobbyv3.waiting .center-col { min-width: 0; }
}

/* ═══ PWA standalone shells: dvh → lvh (round 5, 2026-08-11) ═══
   iOS 26 hands an installed web-app the SMALL viewport in dvh/svh/innerHeight:
   measured on an iPhone 17 Pro, dvh/innerHeight = 812 while lvh = 874 — the
   missing 62px is exactly env(safe-area-inset-top), i.e. iOS treats the
   status-bar band as retractable UA chrome even though black-translucent
   draws under it edge-to-edge. Every 100dvh app shell therefore stopped 62px
   short of the real screen: a black void under the lobby tab bar, and tier
   cards compressed until the glyph collided with the LIVE/entry line (owner
   screenshots). lvh is the full screen; the dvh line above it is the exact
   old behavior and stays as the fallback for engines without lvh units.
   Scope is body.pwa (client.js:4055 sets it from pwa.standalone at boot) —
   browser tabs KEEP dvh on purpose: there the URL-bar collapse is real and
   lvh would put the tab bar under Safari's chrome.
   ⚠️ Deliberately NOT ported: the game canvas (#game style.css + the
   client.js sizeCanvas/input-mapping pair on window.innerHeight) still lives
   on the 812 small viewport — portrait-only cosmetic band, landscape hides
   the status bar; touching it means touching input math (rule 7).
   ⚠️ Keyboard: the viewport meta opts into interactive-widget=resizes-content;
   dvh follows the keyboard, lvh does NOT — verified on the simulator that the
   lobby chat input still clears the keyboard (iOS pans the standalone webview
   itself when the focused field would be covered). If a future report says an
   input hides behind the keyboard in the PWA, this rule is the suspect. */
/* The body floor is the anti-freeze: iOS sizes the web-app viewport at FIRST
   layout, and if the visible content fits the small 812px launch viewport
   exactly, it freezes there (~half of cold launches) and the taller shells
   below get their bottom cut. body.pwa is set by the inline script at the top
   of <body> (index.html) precisely so this floor exists before that first
   layout — the document always overflows the small viewport and iOS expands
   it to the full screen. Verified by repeated cold relaunches on the
   simulator. */
body.pwa { min-height: 100lvh; min-width: 100lvw; }
/* ^ min-width is a DEFENSIVE floor only. A landscape probe measured the
   width already full (inner 874, lvw = svw = 874, symmetric L/R insets of
   62px — right-anchored HUD sitting 68px in from the glass is the DESIGNED
   safe area, not a width freeze). No width short-changing was ever confirmed,
   so this line is a verified no-op that only exists to mirror the height
   floor should a future iOS pull the same trick horizontally. */
body.pwa #lobby.lobbyv3 {
  height: calc(100dvh / var(--uiz, 1));
  height: calc(100lvh / var(--uiz, 1));
}
/* The account workspace runs the same 100dvh lock in style.css — mirror its
   two media guards exactly (desktop ≥1121 / portrait phone ≤820 + 13/9). */
@media (min-width: 1121px) {
  body.pwa #account.page { height: 100dvh; height: 100lvh; }
}
@media (max-width: 820px) and (max-aspect-ratio: 13/9) {
  body.pwa #account.page { height: 100dvh; height: 100lvh; }
}
