/* public/style.css — the arcade's shared skin.
   Premium dark theme: layered surfaces, soft depth, crisp card shadows,
   per-game accent identity in the lobby. Every game imports this file, so
   a change here lifts all 25 games at once. */
:root {
  --bg: #0d1117;
  --bg2: #161d27;
  --bg3: #1c2531;
  --line: #2a3542;
  --line-soft: rgba(255, 255, 255, .07);
  --card: #f7f3e8;
  --ink: #1c2026;
  --red: #d64545;
  --green: #3ecf8e;
  --accent: #4aa3ff;
  --accent2: #7c5cff;
  --gold: #ffd166;
  --dim: #8b98a6;   /* muted text — many game HUDs already use var(--dim) */
  --fg: #eef2f6;    /* bright text — same */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-2: 0 4px 14px rgba(0, 0, 0, .35);
  --shadow-3: 0 14px 36px rgba(0, 0, 0, .5);
  /* Gutters are 3px and the side padding 6px: every pixel not spent on air goes
     into the cards themselves. */
  /* Fallback sizing for a page with no #board. NOTE: these are derived here AND
     on #board — see the comment there before "simplifying" one of them away. */
  --card-w: clamp(38px, calc((100vw - var(--sal) - var(--sar) - 10px - 6 * 3px) / 7), 150px);
  --card-h: calc(var(--card-w) * 1.4);
  /* fan steps: the exposed band of a stacked card. 0.15/0.26 (was 0.17/0.30)
     keeps a 12px floor of every card visible while shortening a 13-card column
     by ~15%, which is what lets the cards be bigger on a short screen. */
  --step-down: max(8px, calc(var(--card-w) * 0.15));
  /* 0.46 matches the renderer's own face-up fan step (klondike render.js:
     upStep = w * 0.46), which is what makes a 0.46 index fully visible on a
     stacked card. This var is the fallback for .col heights; 14px is the floor. */
  --step-up: max(14px, calc(var(--card-w) * 0.46));
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* iOS standalone runs the web view UNDER the status bar / Dynamic Island
     (apple-mobile-web-app-status-bar-style: black-translucent), so every bar of
     chrome has to reserve that strip or the island eats the top of the app.
     Kept as vars (not raw env()) so the inset can be simulated in a test. */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; overscroll-behavior: none; }
html {
  /* The canvas colour. A gradient painted on the BODY only covers the body's box;
     whatever it does not cover falls through to the browser's default WHITE canvas,
     which is exactly the white band Rick saw at the bottom of Bejeweled. Giving the
     html element the base colour means nothing can ever fall through to white. */
  background: var(--bg);
}
body {
  margin: 0; color: #dfe5ec; font-family: var(--font);
  overscroll-behavior: none; touch-action: manipulation; user-select: none;
  -webkit-user-select: none;
  /* depth: a cool glow above, a violet ember below — never flat black.
     NO background-attachment: fixed — iOS Safari can leave a fixed background
     unpainted (the other half of that white band) and it buys nothing here. */
  background:
    radial-gradient(1100px 620px at 50% -10%, rgba(74, 163, 255, .10), transparent 62%),
    radial-gradient(900px 520px at 88% 112%, rgba(124, 92, 255, .09), transparent 58%),
    var(--bg);
}

#app {
  /* `svh`, NOT the dynamic viewport height. Rick: "Tetris starts shaking vertically when i open the page up."
     The dynamic viewport height changes continuously on iOS while Safari's toolbar settles
     after a page opens, and the board is sized from it — so the play area itself was being
     resized frame by frame during load. It compounded with the pad: the board's chrome
     includes var(--pad-h), the pad's own measured height, so a moving viewport made the
     board resize, the pad re-measure, and the board resize again. The small viewport height
     is a constant — the toolbar cannot move it — so nothing downstream can oscillate.
     The `vh` first line is the fallback for a browser without svh support.
     The flex column is NOT optional: the board is `flex: 1` and the controls sit AFTER it.
     a507b26 dropped `display: flex; flex-direction: column` when it swapped the app shell
     to 100svh, and the board stopped stretching — controls climbed the page ("the button row
     which used to be on the bottom has moved all the way upto the cards" — Rick, 2026-09-24). */
  display: flex; flex-direction: column;
  height: 100vh;
  height: 100svh;
  max-width: 1180px; margin: 0 auto;
  /* size container (not just inline-size) so games can query cqh: a board sized
     from width alone is ~800px tall in a 390px landscape viewport and the page
     scrolls (it did, on 18 of 27 games). */
  container-type: size;
}

/* Card width derives from the app container width (cqw), so it is correct at any
   screen size and inside any wrapper. 7 columns + 6 gaps + side padding must fit.
   Big ceiling (150px) so desktop cards fill the play area instead of leaving margins.
   The height term is what keeps a tall tableau on screen: with the tighter fan
   above, a 13-card column is ~4.75 card-widths tall, so cards must fit --play-h. */
#board {
  /* Height term: a column of 13 fanned cards is 1.4 + 12*0.26 card-widths tall and
     the row of piles above it is another 1.4, so ~5.9 card-widths has to fit
     --play-h. Measured, not guessed: at 4.75 the landscape board asked for 33px
     more than the viewport had. */
  /* 7 cards + 6 gutters + the board's own side padding + the landscape insets
     (the island sits on one side once the phone is rotated) must fit 100cqw.
     Getting this wrong is not subtle: subtracting 24px when the row spends 40
     made the row 16px too wide, and a centred board spills half of that off each
     edge — "the cards are running off the page to the left". */
  --card-w: clamp(38px, min(calc((100cqw - var(--sal) - var(--sar) - 10px - 6 * 3px) / 7), calc(var(--play-h, 100cqh) / 5.9)), 150px);
  /* Anything DERIVED from --card-w has to be re-declared in every scope that
     overrides it. var() inside a custom property is substituted where the
     property is DECLARED, so `--card-h: calc(var(--card-w) * 1.4)` at :root is
     frozen against the ROOT --card-w (a 100vw formula). Here the board's card
     width comes from the container, and landscape is where it shows: a 49px-wide
     card was being given a 164px height (1:3.35 instead of 1:1.4), which pushed
     the tableau off screen and scrolled the page. */
  --card-h: calc(var(--card-w) * 1.4);
  --step-down: max(8px, calc(var(--card-w) * 0.15));
  /* 0.46 matches the renderer's own face-up fan step (klondike render.js:
     upStep = w * 0.46), which is what makes a 0.46 index fully visible on a
     stacked card. This var is the fallback for .col heights; 14px is the floor. */
  --step-up: max(14px, calc(var(--card-w) * 0.46));
  /* the height actually available to the board, once the page's own chrome
     (header + HUD + controls) is subtracted. Games cap their cell size with it. */
  --play-h: calc(100cqh - var(--chrome, 158px) - var(--sat));
  --fit: min(100cqw, var(--play-h));
}

/* A board grid must shrink-wrap its cells and centre them. As a block-level grid
   (or a stretched flex child) it fills the container width, so on a wide, short
   screen — landscape phone — the playfield sits pinned to the left edge with
   dead space beside it. Auto margins centre the shrunk box in either parent. */
#grid { width: fit-content; margin-left: auto; margin-right: auto; }

/* ---------- header ---------- */
#header {
  display: flex; align-items: center; gap: 10px;
  padding: calc(8px + var(--sat)) calc(12px + var(--sar)) 4px calc(12px + var(--sal));
  border-bottom: 1px solid var(--line-soft); min-height: 44px;
  background: linear-gradient(180deg, rgba(255, 255, 255, .035), transparent);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .35);
}
.brand {
  font-weight: 800; font-size: clamp(11px, 3.5vw, 15px); letter-spacing: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.hud { flex: 1; display: flex; align-items: center; gap: 6px; font-size: 13px; color: #9aa7b5; min-width: 0; }
.hud b { color: #eef2f6; }
.hud-name { color: #eef2f6; font-weight: 600; border-bottom: 1px dashed var(--line); max-width: 45vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.iconbtn {
  background: linear-gradient(180deg, #242f3d, #1a222d);
  border: 1px solid var(--line); color: #dfe5ec; border-radius: 10px;
  font-size: 16px; width: 38px; height: 38px; cursor: pointer;
  box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255, 255, 255, .06);
  transition: transform .1s ease, border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.iconbtn:hover { border-color: #3d8bfd; transform: translateY(-1px); }
.iconbtn:active { transform: translateY(0) scale(.94); }

/* ---------- board ---------- */
/* THE BOARD OWNS ITS GESTURES. A drag on the play surface is a game move, never
   a page scroll — without this the browser claims the gesture and the page slides
   under the player's finger (worst on iOS Safari, which keeps the momentum going
   after the move lands). Bejeweled proved it; every game gets it. Nav, headers,
   controls and the leaderboard panels are outside these selectors, so they still
   scroll normally. */
#board, #grid, #tableau, .tableau, .board, canvas, .gamepad {
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
#board { flex: 1; display: flex; flex-direction: column; padding: 10px calc(5px + var(--sar)) 0 calc(5px + var(--sal)); gap: 4px; min-height: 0; }
#toprow { display: flex; justify-content: space-between; align-items: flex-start; gap: 6px; }
.zone { display: flex; gap: 8px; }
/* A top-row slot may shrink but never exceed a card's width, so a narrow phone
   squashes the row instead of overflowing it. Where the row sits on the tableau's
   grid (below), it cannot overflow at all — this is the belt to that brace. */
#toprow .slot, #toprow .fslot { flex: 0 1 var(--card-w); min-width: 0; }

/* THE SKELETON BOXES SIT ON THE TABLEAU'S GRID.
   Spreading the foundation group to fill the leftover width gave the top row its
   own pitch (74px where the tableau below uses 54.7px), so the empty slots no
   longer lined up with the piles under them (Rick: "the skeleton boxes have
   different spacing than the rest of the game"). These rows are now laid out by
   the SAME rule as the tableau — `repeat(N, var(--card-w))` with space-between —
   which produces the identical column positions at every width, because both
   divide the same free space the same way. Opt in per game with a class, since
   each solitaire's top row has a different number of slots. */
#toprow.grid-7 {
  display: grid; grid-template-columns: repeat(7, var(--card-w));
  justify-content: space-between; align-items: start;
  gap: 3px;
}
#toprow.grid-7 > .zone:first-child { grid-column: 1 / span 2; display: flex; justify-content: space-between; }
#toprow.grid-7 > .zone:last-child { grid-column: 4 / span 4; display: flex; justify-content: space-between; }
#toprow.grid-8 {
  display: grid; grid-template-columns: repeat(8, var(--card-w));
  justify-content: space-between; align-items: start;
  gap: 3px;
}
#toprow.grid-8 > .zone:first-child { grid-column: 1 / span 4; display: flex; justify-content: space-between; }
#toprow.grid-8 > .zone:last-child { grid-column: 5 / span 4; display: flex; justify-content: space-between; }
#toprow.grid-10 {
  display: grid; grid-template-columns: repeat(10, var(--card-w));
  justify-content: space-between; align-items: start;
  gap: 3px;
}
#toprow.grid-10 > .zone:first-child { grid-column: 1 / span 1; display: flex; justify-content: space-between; }
#toprow.grid-10 > .zone:last-child { grid-column: 3 / span 8; display: flex; justify-content: space-between; }
/* and the stock/waste pair keeps the tableau's column pitch rather than an
   arbitrary 8px, so the second slot lands on column 2 */
#toprow.grid-7 > .zone, #toprow.grid-8 > .zone { gap: 0; }
.frow { gap: 3px; } /* tight: keeps 4 foundations + draw fan on-screen even at 390px */

/* card + slot */
.slot, .fslot {
  width: var(--card-w); height: var(--card-h); border-radius: calc(var(--card-w) * 0.08);
  background: linear-gradient(180deg, #131a23, #10161e);
  border: 1px dashed rgba(122, 140, 160, .38); position: relative;
  box-shadow: inset 0 2px 8px rgba(0, 0, 0, .45), inset 0 0 0 1px rgba(0, 0, 0, .2);
}
/* Playing card. Width is driven by --card-w (the slot/column size),
   never by a global width rule — keep this class exclusive to actual cards. */
.card {
  position: absolute; left: 0; top: 0; width: var(--card-w); height: var(--card-h);
  border-radius: calc(var(--card-w) * 0.08);
  background: linear-gradient(160deg, #fbf8ef 0%, #f5f0e3 55%, #ede6d4 100%);
  color: var(--ink);
  border: 1px solid rgba(58, 48, 30, .38); /* crisp edge in stacked piles */
  box-shadow:
    0 1px 1px rgba(0, 0, 0, .38),
    0 2px 5px rgba(0, 0, 0, .32),
    0 6px 14px rgba(0, 0, 0, .18);
  cursor: grab; z-index: 1;
  display: flex; flex-direction: column; justify-content: space-between; padding: 4px 5px;
  font-weight: 700; line-height: 1; touch-action: none;
  transition: box-shadow .18s ease, filter .18s ease;
}
.card.dragging { transition: none; } /* drag must track the pointer 1:1 */
.wcue {
  position: absolute; left: -2px; top: 2px; height: 100%;
  display: flex; align-items: flex-start; justify-content: flex-end; padding: 2px 6px 0 0;
  color: #b6c2cf; font-size: 12px; font-weight: 800; pointer-events: none;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
}
/* ---------- card face: one deck, every game ----------
   Sizes derive from --face-w: klondike/spider/freecell size cards with
   --card-w, golf/pyramid/tripeaks with --cw. Either way the face scales with
   the card, so a 24px tripeaks card and a 150px desktop card are the same
   design at two zooms (no per-game face code). */
.card { --face-w: var(--cw, var(--card-w, 48px)); }
/* Rank+suit on ONE line so a stacked card's exposed sliver shows BOTH (a two-line
   corner hid the suit under the overlapping card). The suit glyph (.cs) renders
   ~20% larger than the rank so the suit reads at a glance, and the mirrored
   bottom-right index is what makes a run of cards read as a real deck. */
/* The index is what a player actually reads: on a phone the card is ~52px wide, so
   it gets 0.42 of the card and sits right in the corner — a tight top offset is
   what lets the fan band show the whole glyph on stacked cards. */
.card .corner { position: absolute; top: 2.5%; left: 3.5%; display: flex; flex-direction: row; align-items: center; gap: 0.03em; font-size: calc(var(--face-w) * 0.44); line-height: 0.86; }
/* the suit glyph rides ~15% larger than the rank: the index has to read as "9♥",
   not as two similar shapes of the same weight */
.card .corner .cs { font-size: 0.92em; line-height: 0.86; }
.card .corner.b { top: auto; bottom: 2.5%; left: auto; right: 3.5%; transform: rotate(180deg); }
/* pips: the traditional grid. Lower-half pips are inverted (see .flip) so the
   card reads from either end, exactly like the printed deck. */
/* The pip cluster sits BELOW the corner indices (a real deck's pips never touch
   the index; ours keeps the index big enough to read in a 0.3-card stacked band,
   so the grid yields instead: 26% top/bottom clearance, columns pulled in). */
/* ONE suit, centred, as big as the card will carry. The grid of counted pips
   this replaced rendered 6px glyphs on a 40px phone card — unreadable, and the
   count is on the index anyway. 0.62 of the card width keeps the glyph clear of
   both corner indices. */
/* The suit is centred in the card, not in a shifted box: with a 0.42 index in both
   corners the middle band is what is left, and shifting the box down (tried it)
   put the pip straight into the mirrored index. */
.card .pips { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.card .pip { font-size: calc(var(--face-w) * 0.42); line-height: 1; }
/* court cards: a mirrored two-headed figure — no pips, like the real thing */
/* The court art is an SVG, so `height: auto` comes from its viewBox ratio (1.4) —
   not from the bottom inset. Inside a card whose padding box is 1.4167:1 the
   ratio-derived box is a fraction of a px short, the box is over-constrained and
   the art anchors to the TOP (measured: a 3% vertical offset at 420px, and the
   gold frame visibly pinched at the top). Pinning the width and letting auto
   margins split the leftover keeps the ratio AND centres the art exactly. */
.card .court { position: absolute; inset: 1.5%; margin: auto; width: 97%; height: auto; --court-gold: #c9a227; }
.card .court .crt-rule { fill: none; stroke: currentColor; stroke-width: 1.4; opacity: .45; }
.card .court .crt-rule.thin { stroke: var(--court-gold); stroke-width: 1; opacity: .85; }
.card .court path.crt-rule { fill: none; }
.card .court .crt-corners { fill: var(--court-gold); stroke: none; }
.card .court .crt-cape { fill: currentColor; fill-opacity: .38; stroke: currentColor; stroke-width: .8; }
.card .court .crt-sleeve { fill: currentColor; fill-opacity: .55; stroke: currentColor; stroke-width: .8; }
.card .court .crt-brocade path { fill: none; stroke: var(--court-gold); stroke-width: .9; opacity: .8; }
.card .court .crt-ermine { fill: #fdf6e8; stroke: currentColor; stroke-width: .9; }
.card .court .crt-metal { fill: #b6c4d2; stroke: #738799; stroke-width: 1.7; stroke-linejoin: round; stroke-linecap: round; }
.card .court .crt-leaf { fill: #6f9f63; stroke: #46703d; stroke-width: .7; }
.card .court .crt-line { fill: none; stroke: currentColor; stroke-width: 1; stroke-linecap: round; }
.card .court .crt-skin { fill: #fdf6e8; stroke: currentColor; stroke-width: 1; }
.card .court .crt-hair { fill: currentColor; opacity: .62; }
.card .court .crt-detail { fill: currentColor; }
.card .court .crt-gold { fill: var(--court-gold); stroke: #8a6a12; stroke-width: .5; }
.card .court .crt-goldline { fill: none; stroke: var(--court-gold); stroke-width: 2.2; stroke-linecap: round; }
.card .court .crt-cap { fill: #4a6b8a; stroke: #2c4560; stroke-width: .6; }
.card .court .crt-feather { fill: #cfe3f2; stroke: #7c93a8; stroke-width: .6; }
.card .court .crt-featherline { fill: none; stroke: #8fa6b8; stroke-width: .7; }
.card .court .crt-jewel { fill: var(--red); stroke: rgba(0, 0, 0, .35); stroke-width: .4; }
.card .court .crt-band { fill: currentColor; fill-opacity: .14; }
.card .court .crt-suit { fill: currentColor; }
.card .court text { font-family: inherit; font-weight: 800; font-size: 15px; text-anchor: middle; }
.card .court .crt-bandpip { font-size: 12px; }
.card .court .crt-tunic { font-size: 7px; opacity: .75; }
.card.red { color: var(--red); }
.card.black { color: var(--ink); }
.card .down { height: 100%; }
/* Card back: deep navy weave + a centered diamond emblem — reads as "real deck". */
.card.back::before {
  content: ""; position: absolute; inset: 12%; border-radius: calc(var(--card-w) * .05);
  background:
    radial-gradient(circle at 50% 50%, rgba(255, 255, 255, .08) 0 16%, transparent 17%),
    repeating-linear-gradient(45deg, #2c3e57 0 7px, #24344a 7px 14px);
  border: 2px solid #1a2838;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .07), inset 0 2px 6px rgba(0, 0, 0, .45);
}
.card.back::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 30%; height: 30%; border-radius: 24%;
  background: linear-gradient(135deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .03));
  border: 1px solid rgba(255, 255, 255, .14);
  transform: rotate(45deg);
  box-shadow: 0 0 8px rgba(74, 163, 255, .18);
}
.card.selected { outline: 2px solid var(--accent); outline-offset: 1px; box-shadow: 0 0 12px rgba(74, 163, 255, .45), 0 2px 5px rgba(0, 0, 0, .32); }
.card.dragging { z-index: 1000; box-shadow: 0 14px 30px rgba(0, 0, 0, .6); transform-origin: 0 0; }
/* A hint has to be findable at a glance on a phone: the old pulse was a 1s
   brightness lift on one card, which players reported as "the hint does nothing".
   This is a ring, it lasts longer, and it sits above its neighbours. */
.card.glow { animation: glow 1.3s ease-in-out 4; z-index: 40; }
@keyframes glow {
  0%, 100% { filter: brightness(1.05); box-shadow: 0 0 0 0 rgba(255, 209, 102, 0); }
  50% { filter: brightness(1.45) saturate(1.2); box-shadow: 0 0 0 3px var(--accent), 0 0 20px 5px rgba(255, 209, 102, .75); }
}
.card.flash { animation: flash .5s ease; }
@keyframes flash { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1); } }
/* stock count badge + recycle marker (klondike uses these; spider overrides locally) */
.cnt {
  position: absolute; right: 2px; bottom: 1px; z-index: 2;
  font-size: 10px; font-weight: 800; color: var(--dim);
  background: rgba(0, 0, 0, .4); border-radius: 6px; padding: 1px 4px;
}
.recycle-card .down { display: flex; align-items: center; justify-content: center; font-size: calc(var(--card-w) * .38); color: #9fb3c8; }
.hud-sep { opacity: .4; }

.col { position: relative; width: var(--card-w); height: calc(var(--card-h) + 12 * var(--step-up)); flex: 0 0 auto; }
.col .base, .fslot.filled { background: var(--bg2); opacity: .55; }
#tableau { display: flex; justify-content: space-between; gap: 3px; overflow: visible; flex: 1; padding-bottom: 6px; }

/* feedback strip for hints/errors */
/* Hidden until it has something to say. The games toggle a `show` class for
   1.8s; without a hidden default the element was visible forever once created
   — Nana: "it won't go away after appearing, it is blocking me from seeing the
   board" (sudoku, on a rejected digit).
   Opacity comes from the CLASS, never from a keyframe: an animation that has
   not run yet (backgrounded tab, throttled phone) pins its from-value, which
   would leave the toast invisible. Transitions only, so the state is always
   whatever the class says. */
#toast {
  position: fixed; left: 50%; top: 60px;
  transform: translateX(-50%) translateY(-6px);
  background: rgba(10, 14, 19, .92); color: #fff; padding: 9px 16px;
  border-radius: 20px; font-size: 13px; z-index: 2000; pointer-events: none;
  border: 1px solid var(--line-soft); box-shadow: var(--shadow-2);
  opacity: 0; visibility: hidden;
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
#toast.show {
  opacity: 1; visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition: opacity .18s ease, transform .18s ease, visibility 0s;
}

/* ---------- controls ---------- */
#controls { display: flex; gap: 8px; padding: 8px 10px calc(10px + var(--sab)); justify-content: center; }
.ctl {
  flex: 1; max-width: 160px; padding: 13px 8px; border-radius: 12px;
  border: 1px solid var(--line);
  background: linear-gradient(180deg, #242f3d, #1a222d);
  color: #e8edf2; font-size: 15px; font-weight: 600; cursor: pointer; min-height: 48px;
  box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255, 255, 255, .06);
  transition: transform .1s ease, border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.ctl:hover { border-color: #3d8bfd; }
.ctl:active { transform: translateY(1px); }
.ctl.primary {
  background: linear-gradient(180deg, #5cb0ff, #3d8bfd);
  border-color: #5cb0ff; color: #06182e;
  box-shadow: 0 4px 14px rgba(61, 139, 253, .35), inset 0 1px 0 rgba(255, 255, 255, .35);
}
.ctl.active { border-color: var(--accent); color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
/* A secondary action under a primary row (the win panel's "Board": inspects the
   finished board; it never was the leaderboard, which is what "Board" read as).
   Full width, quiet, still a 44px target. */
.ctl.ghost {
  flex: none; display: block; width: 100%; max-width: none;
  margin: 8px 0 0; padding: 11px 8px; min-height: 44px;
  background: transparent; border-color: transparent; box-shadow: none;
  color: var(--dim); font-size: 13.5px; font-weight: 600;
}
.ctl.ghost:hover { color: var(--fg); border-color: var(--line); }
.hidden { display: none !important; }

/* ---------- overlays ---------- */
.overlay {
  position: fixed; inset: 0; background: rgba(6, 9, 13, .78);
  display: flex; align-items: center; justify-content: center; z-index: 500;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  animation: fadeIn .18s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* Leaderboard draw-variant tabs (1-card vs 3-card are separate boards). */
/* The win panel's Leaderboard row is a tappable shortcut (header trophy is
   under the overlay at z-index 500), so it reads and acts like a button. */
.stat.tappable { cursor: pointer; }
.stat.tappable:active b { color: var(--accent); }

/* ---------- the game header on a phone: ONE top-right dropdown ----
   Four controls (🔊 ⇄ 🏆 + avatar) beside a three-part HUD is more than a 375px
   header can hold; Rick's report was "header is crowded and icons overlap".
   Below 560px the three game icons step into the avatar menu (account.js mounts
   Sound / Leaderboard / Switch player there, driving these same buttons), so the
   game keeps its controls and the header keeps its room. The ids are named
   explicitly: duel-race's Invite/Leave are multiplayer actions with no menu
   equivalent and must stay put. */
@media (max-width: 560px) {
  #header #btn-sound, #header #btn-switch, #header #btn-lb { display: none; }
  #header .hud { flex: 1; min-width: 0; }
}

/* the divider between the game's actions and the player's, in the menu */
.acct-sep { height: 1px; margin: 6px 8px; background: var(--line-soft); }

/* ---------- narrow phones: the header has more in it than it has room for ----
   At 320px (the narrowest phone this arcade runs on) the header's four controls
   plus the HUD pushed the account button 9px past the right edge — the whole page
   scrolled sideways because of it. The HUD yields first: the player's name and
   the clock are what you need mid-hand, while "best" lives on the win panel and
   the trophy is one tap away. The wordmark goes too — you are already in the game. */
@media (max-width: 380px) {
  #header { gap: 5px; }
  #header .hud { flex: 1; min-width: 0; }
  #header .hud > *:not(.hud-name):not(#hud-time) { display: none; }
  #header .hud-name { max-width: 40vw; }
  #header .iconbtn, #header .acct-btn { width: 34px; height: 34px; font-size: 15px; }
}
/* Narrower still (320px): the wordmark is the next thing to go — you are inside
   the game, and the space belongs to the controls. */
@media (max-width: 350px) {
  #header .brand { display: none; }
}

/* ---------- leaderboard rows ---------- */
/* ONE row language for both surfaces: the win panel's standings and the trophy
   overlay list. Rick, 2026-09-19: "primitive bootstrap like. chunky." What was
   there: a grey rank number, a border under every row, and a cramped grey line
   with an ISO date in it. What a leaderboard owes a phone, in order: who is
   winning (the rank reads as a MEDAL), the score big enough to compare down the
   column, where YOU are without hunting, and the supporting numbers present but
   quiet. The markup comes from public/board.js for every game. */
.mini-list, #lb-list { list-style: none; margin: 0; padding: 0; }
.mini-list li, #lb-list li {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 10px; border-radius: 12px; font-size: 15px;
}
/* a hairline BETWEEN rows only — never a box per row */
.mini-list li + li, #lb-list li + li { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .045); }
.mini-list .rank, #lb-list .rank {
  flex: none; width: 30px; height: 30px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 800; font-variant-numeric: tabular-nums;
  background: rgba(255, 255, 255, .06); color: var(--dim);
}
.mini-list .rank.r1, #lb-list .rank.r1 { background: linear-gradient(180deg, #f7d98a, #d9a72c); color: #3a2a05; box-shadow: 0 2px 10px rgba(217, 167, 44, .3); }
.mini-list .rank.r2, #lb-list .rank.r2 { background: linear-gradient(180deg, #e3eaf2, #a8b4c2); color: #262d36; }
.mini-list .rank.r3, #lb-list .rank.r3 { background: linear-gradient(180deg, #e8bd90, #b97f45); color: #33200c; }
.mini-list .lg-col, #lb-list .lg-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.mini-list .nm, #lb-list .nm { font-weight: 600; letter-spacing: .1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-list .sub, #lb-list .sub { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; }
.mini-list .sc, #lb-list .sc {
  flex: none; min-width: 54px; text-align: right; font-size: 18px; font-weight: 800;
  font-variant-numeric: tabular-nums; color: var(--accent);
}
.mini-list .you, #lb-list .you, .you {
  margin-left: 6px; padding: 2px 6px; border-radius: 999px; font-size: 10px; letter-spacing: .8px;
  text-transform: uppercase; background: rgba(74, 163, 255, .16); color: #cfe4ff; vertical-align: 1px;
}
.mini-list .badge1, #lb-list .badge1 { flex: none; margin-left: 6px; padding: 2px 6px; border-radius: 999px; font-size: 10px; background: rgba(255, 255, 255, .07); color: #cfd8e0; }
/* you are here: one tinted row, a bar on the left, gold score */
.mini-list li.me, #lb-list li.me {
  background: linear-gradient(90deg, rgba(74, 163, 255, .16), rgba(74, 163, 255, .04));
  box-shadow: inset 3px 0 0 var(--accent);
}
.mini-list li.me .sc, #lb-list li.me .sc { color: var(--gold); }
.mini-list li.rank-foot, #lb-list li.rank-foot { margin-top: 8px; }
.mini-list li.empty, #lb-list li.empty, #lb-list li.nm { justify-content: center; color: var(--dim); font-size: 13px; padding: 18px 10px; }
.mini-loading, .mini-empty { color: var(--dim); font-size: 12.5px; padding: 6px 0; }
.mini-head { font-size: 11.5px; letter-spacing: 1.2px; text-transform: uppercase; color: #9fb0c0; margin-bottom: 7px; }

/* WIN-PANEL POLISH (measured, not guessed: the action buttons came in at 50px
   with 15px labels next to 35px stat rows, and the tertiary action at 41px with
   13.5px text and no affordance). A modal's buttons confirm a result — they are
   not the game's controls, so they get a quieter, better-proportioned size. */
#modal .row { gap: 10px; }
#modal .row .ctl { min-height: 46px; padding: 11px 10px; font-size: 15.5px; }
#modal .ctl.ghost {
  min-height: 44px; margin-top: 10px; font-size: 14.5px;
  background: rgba(255, 255, 255, .035); border: 1px solid var(--line-soft);
  color: #c2cdd8;
}
#modal .ctl.ghost:active { background: rgba(255, 255, 255, .08); }
#modal h2 { font-size: 21px; margin-bottom: 10px; }
#modal .stat { padding: 9px 2px; }
.mini-board { margin: 14px 0 2px; }

/* ── no double-tap zoom on controls ──────────────────────────────────────────
   Rick: "in tetris when i press a button twice it zooms in and ruins the game play."
   The body sets touch-action: manipulation, but touch-action is NOT inherited, so every
   button was still zoomable — measured: btn-pause, btn-new, Play, howto-ok, btn-rules
   and the account button ALL computed to `auto`. `manipulation` keeps panning and
   pinching but kills double-tap-to-zoom. Element-level, so the pad's own stricter
   `touch-action: none` (a class selector) still wins. */
button, a, [role="button"], .ctl, .iconbtn, .pad-btn { touch-action: manipulation; }

/* Panel = modal/dialog surface. Deliberately NOT ".card" — that class belongs to playing cards. */
.panel {
  background: linear-gradient(180deg, #1c2531, #141b24);
  border: 1px solid var(--line); border-radius: 18px; padding: 22px;
  width: min(94vw, 400px);
  box-shadow: var(--shadow-3), inset 0 1px 0 rgba(255, 255, 255, .06);
  animation: popIn .22s cubic-bezier(.2, .9, .3, 1.15);
}
@keyframes popIn { from { opacity: 0; transform: scale(.94) translateY(10px); } to { opacity: 1; transform: none; } }
form h1 { margin: 0 0 4px; font-size: 26px; letter-spacing: 1px; color: var(--accent); }
.sub { margin: 0 0 16px; color: #9aa7b5; font-size: 13px; }
input[type=email], input[type=text] {
  width: 100%; padding: 13px 12px; margin-bottom: 12px; border-radius: 12px;
  border: 1px solid var(--line);
  background: #0d1116; color: #fff; font-size: 16px;
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .4);
}
input[type=email]:focus, input[type=text]:focus { outline: none; border-color: var(--accent); box-shadow: inset 0 2px 6px rgba(0, 0, 0, .4), 0 0 0 3px rgba(74, 163, 255, .18); }
button[type=submit] {
  width: 100%; padding: 14px; border-radius: 12px; border: none;
  background: linear-gradient(180deg, #5cb0ff, #3d8bfd); color: #06182e;
  font-size: 16px; font-weight: 700; cursor: pointer;
  box-shadow: 0 4px 14px rgba(61, 139, 253, .35), inset 0 1px 0 rgba(255, 255, 255, .35);
  transition: transform .1s ease, box-shadow .15s ease;
}
button[type=submit]:active { transform: translateY(1px); }
.err { color: #ff8080; font-size: 13px; min-height: 18px; margin: 8px 0 0; }

.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--line-soft); font-size: 15px; }
.stat b { color: #fff; font-size: 17px; }
.record { text-align: center; color: var(--green); font-weight: 700; padding: 8px 0; }
.row { display: flex; gap: 8px; margin-top: 14px; justify-content: center; }
/* The win/result panel's buttons. Rick: "the winner popup at the end of the game has too
   big of buttons and the buttons are left aligned. i want the buttons to be centered."
   Measured before: 150x46 each with 20-22px of slack and `justify: normal` (= flex-start),
   so the pair sat hard left on every game. Centred now, and trimmed to a size that suits
   two actions rather than one hero button. A full-width ghost (close) still spans the row. */
#modal .row { justify-content: center; }
/* 132px made "Next level" wrap to two lines (60px tall — worse, not better), so the
   cap is generous enough for the longest label on ONE line. */
#modal .row .ctl { max-width: 140px; min-height: 40px; padding: 10px 10px; font-size: 14.5px; white-space: nowrap; }

.lc { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
#lb-list { max-height: 62vh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }

/* tabs as a segmented control (they were two bordered buttons = the "bootstrap" read) */
.lbtabs {
  display: flex; gap: 4px; padding: 4px; margin: 0 0 12px;
  background: rgba(255, 255, 255, .05); border: 1px solid var(--line); border-radius: 12px;
}
.lb-tab {
  flex: 1; min-height: 42px; border: 0; border-radius: 9px; background: transparent;
  color: #cfd8e0; font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: background .15s ease, color .15s ease, box-shadow .15s ease;
}
.lb-tab:hover { color: #fff; }
.lb-tab.active { background: linear-gradient(180deg, #5cb0ff, #3d8bfd); color: #06182e; box-shadow: 0 2px 10px rgba(61, 139, 253, .3); }

/* ── the leaderboard on a phone (26 game pages share this overlay) ──────────
   Measured at 390px before this block: the draw-mode tabs were 32px tall (under
   a thumb), the panel's 22px padding ate 44px of a 345px panel, the overlay
   ignored the island/home-indicator insets, and the detail line was the first
   thing to get clipped. Everything here is a phone-only override. */
@media (max-width: 560px) {
  .overlay { padding: calc(10px + var(--sat)) 10px calc(10px + var(--sab)); }
  .panel { padding: 16px; }
  .lc { margin-bottom: 10px; }
  .lbtabs { gap: 8px; }
  /* id-qualified on purpose: the generic `button:not(.iconbtn)...` touch rule
     later in this file also claims plain buttons, and at 390px the tabs
     measured 43px — just under a comfortable thumb */
  #lbpanel .lb-tab { min-height: 48px; font-size: 14px; }
  #lb-list {
    max-height: 56vh;
    overscroll-behavior: contain;      /* scrolling the list must not scroll the game */
    -webkit-overflow-scrolling: touch;
  }
  .mini-list li, #lb-list li { padding: 12px 10px; gap: 11px; }
  .mini-list .rank, #lb-list .rank { width: 32px; height: 32px; font-size: 14px; }
  .mini-list .sc, #lb-list .sc { font-size: 19px; }
  .mini-list .nm, #lb-list .nm { font-size: 15.5px; }
}

/* ── arcade lobby ────────────────────────────────────────────── */
.lobby-body {
  display: block;
  background:
    radial-gradient(1100px 420px at 50% -160px, rgba(74, 163, 255, .10), transparent 70%),
    linear-gradient(180deg, #0e141c, var(--bg) 340px);
}
#lobby { max-width: 1080px; margin: 0 auto; padding: 0 16px 28px; }

/* header: a brand block on the left, "who is playing" on the right. Nothing
   jammed together — the name is a pill you can tap to log in. */
#lobby-header {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: calc(16px + var(--sat)) 2px 12px;
}
#brandmark { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
#brandmark .brand { font-size: 15px; font-weight: 900; letter-spacing: .34em; }
#brandmark .brand-sub { font-size: 11.5px; color: var(--dim); letter-spacing: .04em; }
#who { display: flex; align-items: center; gap: 6px; }

/* toolbar: just the library stats — the search box is gone (it is a shelf of
   29 games; the chips and the keyboard cover browsing) */
#toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 2px 2px 10px; }
#stats { font-size: 12.5px; color: var(--dim); }
#stats b { font-size: 13.5px; color: var(--fg); }
#stats .dot { padding: 0 2px; opacity: .45; }
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- the account menu: one icon, everything about the PLAYER ---------- */
.acct { position: relative; display: flex; align-items: center; }
.acct-btn {
  position: relative; /* anchor for #acct-dot, which must not widen the header */
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; border-radius: 999px; cursor: pointer;
  background: linear-gradient(180deg, #1b2430, #151c26); border: 1px solid var(--line);
  color: var(--fg); font-size: 15px; font-weight: 700; line-height: 1;
}
.acct-btn:hover { border-color: #3d8bfd; }
.acct-btn:active { transform: scale(.94); }
.acct-btn[aria-expanded="true"] { border-color: #3d8bfd; box-shadow: 0 0 0 2px rgba(61,139,253,.25); }
.acct-face { pointer-events: none; }
/* The reply count on the avatar. The avatar is the ONE control present on every
   game's header, so it is the only place a player can be told "there is an
   answer" from inside a game — but it must not grow the header: the dot is
   absolutely positioned inside the button box, decoration only
   (pointer-events:none, aria-hidden), and the count also lives in the menu row
   and in the button's own aria-label. At the ≤380px breakpoint the button is
   34px, so anything that reflowed it would re-break the header that block
   exists to fix. */
.acct-dot {
  position: absolute; top: -6px; right: -6px; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px; background: #3d8bfd; color: #06182e;
  font: 800 10px/16px -apple-system, system-ui, sans-serif; text-align: center;
  pointer-events: none; box-shadow: 0 0 0 2px #0d1117;
}
.acct-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  width: min(84vw, 268px); padding: 8px; border-radius: 14px;
  background: #131a24; border: 1px solid var(--line); box-shadow: 0 18px 44px rgba(0,0,0,.55);
}
.acct-head { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
.acct-face.lg {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 999px; flex: 0 0 auto;
  background: rgba(255,255,255,.06); border: 1px solid var(--line); font-size: 16px;
}
.acct-id { display: flex; flex-direction: column; min-width: 0; }
.acct-id b { font-size: 14px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-id em { font-size: 11px; color: var(--dim); font-style: normal; overflow: hidden; text-overflow: ellipsis; }
.acct-stats { display: flex; gap: 6px; flex-wrap: wrap; padding: 0 8px 8px; font-size: 11px; color: var(--dim); }
.acct-stat {
  display: flex; flex-direction: column; align-items: center; min-width: 56px;
  padding: 6px 8px; border-radius: 10px; background: rgba(255,255,255,.04); border: 1px solid var(--line);
}
.acct-stat b { font-size: 14px; color: var(--fg); }
.acct-stat em { font-style: normal; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; }
.acct-item {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px;
  border-radius: 10px; border: 0; background: transparent; cursor: pointer;
  color: var(--fg); font-size: 13.5px; text-align: left; text-decoration: none;
}
.acct-item:hover, .acct-item:focus-visible { background: rgba(255,255,255,.06); }
.acct-item .acct-ic { width: 18px; text-align: center; flex: 0 0 auto; }
.acct-item .acct-state { margin-left: auto; font-size: 11px; font-style: normal; color: var(--dim); }
.acct-item.danger { color: #ffb4b4; }
.acct-menu hr { border: 0; border-top: 1px solid var(--line); margin: 6px 4px; }
@media (max-width: 520px) { .acct-btn { width: 40px; height: 40px; } }
#search {
  width: 220px; padding: 7px 11px; font-size: 13px; color: var(--fg);
  background: var(--bg2); border: 1px solid var(--line); border-radius: 10px;
}
#search:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(74, 163, 255, .12); }
#search::placeholder { color: var(--dim); }

/* category chips — the shelf filter. The count reads as a badge, not glued text. */
#chips {
  display: flex; gap: 7px; flex-wrap: wrap;
  padding: 0 0 12px; margin-bottom: 5px; border-bottom: 1px solid var(--line-soft);
}
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; border-radius: 999px; cursor: pointer;
  background: var(--bg2); border: 1px solid var(--line); color: var(--dim);
  font-size: 12.5px; font-weight: 600; font-family: inherit;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.chip:hover { color: var(--fg); border-color: #3d5a80; }
.chip.on {
  color: #fff; border-color: var(--accent); background: #1b2c46;
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.chip-count {
  font-size: 10.5px; font-weight: 700; padding: 1px 6px;
  border-radius: 999px; background: rgba(255, 255, 255, .07); color: var(--dim);
}
.chip.on .chip-count { background: rgba(74, 163, 255, .28); color: #dceaff; }

/* sections: title — hairline — count, so each category reads as a shelf */
.cat { margin: 0 0 14px; }
.cat-head {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 8px; font-size: 11px; font-weight: 800;
  letter-spacing: 1.6px; text-transform: uppercase; color: var(--dim);
}
.cat-rule { flex: 1 1 auto; height: 1px; background: linear-gradient(90deg, var(--line), transparent); }
.cat-n { font-size: 10.5px; font-weight: 700; letter-spacing: 0; opacity: .7; }

.game-grid { display: block; }
.shelf { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); }
.game-card {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 11px;
  padding: 9px 12px; border-radius: 13px; text-decoration: none;
  background: linear-gradient(180deg, #1b2430, #151c26);
  border: 1px solid var(--line); color: inherit;
  box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255, 255, 255, .04);
  transition: transform .12s ease, border-color .12s ease, box-shadow .18s ease, background .12s ease;
  animation: cardIn .25s ease backwards;
  animation-delay: calc(var(--i, 0) * 14ms);
}
@keyframes cardIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* accent identity strip + hover glow, driven by --gc-accent set in lobby.js */
.game-card::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--gc-accent, var(--accent)); opacity: .85;
}
.game-card:hover, .game-card:focus-visible {
  transform: translateY(-2px);
  border-color: var(--gc-accent, #3d8bfd);
  background: linear-gradient(180deg, #1f2a38, #171f2b);
  box-shadow:
    0 6px 18px rgba(0, 0, 0, .45),
    0 0 0 1px var(--gc-accent, #3d8bfd),
    0 0 16px var(--gc-glow, rgba(74, 163, 255, .25));
  outline: none;
}
.gc-icon {
  flex: none; width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; line-height: 1; border-radius: 10px;
  background: linear-gradient(160deg, #26313f, #171f2a);
  border: 1px solid var(--line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 0 10px var(--gc-glow, rgba(74, 163, 255, .15));
}
.gc-body { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
.gc-top { display: flex; align-items: center; gap: 7px; min-width: 0; }
.gc-name {
  font-weight: 650; font-size: 13.5px; color: var(--fg);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gc-blurb {
  font-size: 11.5px; color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gc-badge {
  margin-left: auto; flex: none; font-size: 9.5px; font-weight: 800;
  padding: 1px 5px; border-radius: 6px; letter-spacing: .05em;
  background: #22304a; color: #9fc4ff; border: 1px solid #33507d;
}
.loading { opacity: .6; padding: 24px 4px; }
.lobby-foot {
  margin-top: 18px; padding-top: 12px; border-top: 1px solid var(--line-soft);
  text-align: center; font-size: 11.5px; opacity: .38;
}

/* the community strip — one line of news above the shelf */
#community-strip {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 10px; padding: 8px 12px; border-radius: 11px;
  border: 1px solid var(--line); text-decoration: none; color: var(--dim);
  background: linear-gradient(90deg, rgba(74, 163, 255, .07), rgba(74, 163, 255, 0));
  font-size: 13px;
}
#community-strip:hover { border-color: var(--accent); color: var(--fg); }
#community-strip #community-ic { flex: none; font-size: 15px; }
#community-strip #community-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#community-strip #community-text b { color: var(--fg); }
#community-strip .community-go { flex: none; color: var(--accent); font-weight: 650; font-size: 12.5px; }

/* the login popover — compact, anchored, and it never covers the shelf */
#login.mini { position: fixed; top: 58px; right: 18px; z-index: 300; width: min(340px, calc(100vw - 36px)); }
#login.mini .panel { padding: 14px 15px; border-radius: 14px; box-shadow: var(--shadow-3); }
#login.mini h2 { margin: 0 0 2px; font-size: 16px; }
#login.mini .sub { font-size: 12.5px; margin: 0 0 10px; }
#login.mini input { width: 100%; }
#login.mini .row { display: flex; gap: 8px; margin-top: 10px; }
#login.mini .row > * { flex: 1 1 0; }

/* phones: two tiles per row, blurb dropped (the name + icon carry it), and the
   stat line trimmed so the header does not wrap into a paragraph */
@media (max-width: 560px) {
  #lobby { padding: 0 10px 20px; }
  #brandmark .brand-sub { display: none; }
  #stats .dim-extra { display: none; } /* keep the dots: "27 games · 5 categories" */
  .shelf { grid-template-columns: repeat(auto-fill, minmax(146px, 1fr)); gap: 8px; }
  .game-card { padding: 7px 9px; gap: 9px; border-radius: 11px; }
  .gc-icon { width: 28px; height: 28px; font-size: 16px; border-radius: 9px; }
  .gc-name { font-size: 12.5px; }
  .gc-blurb { display: none; }
  .cat { margin: 0 0 10px; }
  .chip { padding: 4px 9px; font-size: 12px; }
}
.backlink {
  color: inherit; text-decoration: none; font-size: 18px;
  opacity: .7; padding: 0 8px;
}
.backlink:hover { opacity: 1; }

/* Persistent "loss" notice — shown after the player chooses Inspect on a dead deal. */
#lossbanner {
  position: fixed; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 600;
  background: rgba(24, 28, 34, .96); border: 1px solid rgba(255, 140, 140, .5); color: #ffd9d9;
  padding: 10px 16px; border-radius: 20px; font-size: 14px; font-weight: 600;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .5); pointer-events: none; max-width: 92vw; text-align: center;
}

/* accessibility: respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

/* ---------- touch + small-screen ergonomics (appended last so it wins) ---------- */

/* A finger is ~9mm wide: anything tappable gets a 44px box on a coarse pointer.
   Measured before this: the game back link was 24x21 and every HUD button 38x38.
   Padding (not transform) grows the box, so the icon stays where it was. */
@media (any-pointer: coarse) {
  .backlink {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 0 6px; margin-left: -6px;
  }
  .iconbtn { width: 44px; height: 44px; font-size: 17px; }
  /* #who .iconbtn (30px) and the tetris/boxhead .ctl arrows (22px) are more
     specific / narrower than the base rules, so name them explicitly here */
  #who .iconbtn { width: 44px; height: 44px; }
  .ctl { min-height: 48px; min-width: 44px; }
  /* plain <button>s in the games (Roll / Keep / Bank / Flip) measured 41px tall */
  button:not(.iconbtn):not(.chip):not(.ctl):not(.wkey):not(.gem) { min-height: 44px; }
  #header { gap: 6px; }
  /* iOS zooms the page when a focused input is under 16px */
  input, select, textarea { font-size: max(16px, 1em); min-height: 44px; }
  /* table links (the community roster) get a real row to hit */
  td a.who-link, td a { display: inline-block; padding: 14px 0; }
  #community-strip { padding: 11px 12px; }
  #login.overlay input[type=email], #login.overlay input[type=text],
  #login.overlay button[type=submit] { min-height: 46px; }
  #login.overlay .panel { padding: 16px 16px 17px; }
  .chip { min-height: 44px; padding: 8px 12px; }
  #search { min-height: 44px; }
}

/* landscape phone: the chrome must yield to the board. Measured: header 44 +
   HUD 33 + controls 66 = 143px of a 390px viewport before any game was drawn. */
@media (max-height: 520px) {
  #header { min-height: 34px; padding: calc(3px + var(--sat)) calc(10px + var(--sar)) 2px calc(10px + var(--sal)); }
  #header .brand { font-size: 13px; }
  .backlink { font-size: 16px; }
  .iconbtn { width: 34px; height: 34px; font-size: 15px; }
  #controls { gap: 6px; padding: 4px 8px calc(6px + var(--sab)); }
  .ctl { min-height: 38px; padding: 8px 6px; font-size: 13.5px; border-radius: 10px; }
  #hudrow, #hud, .hud { font-size: 12px; }
  #lossbanner { bottom: 48px; padding: 7px 12px; font-size: 13px; }
}

/* ---------- opening a game ---------- */
/* A Home-Screen launch has NO browser progress bar: tapping a tile used to
   look like nothing happened until the next document painted. Answer the tap
   immediately with a card + indeterminate bar (the navigation runs underneath,
   never blocked). */
#arcade-opening {
  position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center;
  padding: var(--sat) 16px var(--sab);
  background: rgba(6, 9, 13, .74);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
#arcade-opening .op-card {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px 12px;
  width: min(92vw, 360px); padding: 16px 18px 13px; border-radius: 16px;
  background: var(--bg2); border: 1px solid var(--line); box-shadow: var(--shadow-3);
}
#arcade-opening .op-icon { font-size: 30px; line-height: 1; }
#arcade-opening .op-text { display: flex; flex-direction: column; min-width: 0; }
#arcade-opening .op-name { font-weight: 700; font-size: 15px; color: var(--fg); }
#arcade-opening .op-hint { font-size: 12px; color: var(--dim); }
#arcade-opening .op-bar { flex-basis: 100%; height: 4px; border-radius: 999px; background: rgba(255, 255, 255, .08); overflow: hidden; }
#arcade-opening .op-bar i {
  display: block; height: 100%; width: 38%; border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  animation: op-slide 1.05s ease-in-out infinite;
}
@keyframes op-slide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
.game-card.opening { opacity: .45; }
@media (prefers-reduced-motion: reduce) {
  #arcade-opening .op-bar i { animation: none; width: 100%; opacity: .6; }
}

/* ---------- game login: a card, not a takeover ---------- */
/* The game pages opened with a full-screen blur + centred modal, so a phone
   showed NO board at all until you logged in (measured: overlay 390x844 on a
   390x844 screen, backdrop-filter blur(6px) over everything). It is still the
   login gate — it just stops hiding the game and stops yanking the layout. */
#login.overlay {
  align-items: flex-start; justify-content: center;
  padding: max(10px, var(--sat)) 12px 0;
  background: linear-gradient(180deg, rgba(6, 9, 13, .72), rgba(6, 9, 13, .34) 62%, rgba(6, 9, 13, .16));
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
#login.overlay .panel { padding: 14px 15px 15px; border-radius: 14px; width: min(96vw, 400px); }
#login.overlay h1 { font-size: 18px; letter-spacing: .4px; margin: 0 0 2px; }
#login.overlay .sub { font-size: 12.5px; margin: 0 0 10px; }
#login.overlay input[type=email], #login.overlay input[type=text] { padding: 11px 12px; margin-bottom: 9px; }
#login.overlay button[type=submit] { padding: 12px; }
#login.overlay .err { margin: 6px 0 0; min-height: 14px; font-size: 12.5px; }
@media (max-height: 520px) {
  #login.overlay h1 { font-size: 16px; }
  #login.overlay .sub { display: none; }
  #login.overlay .panel { padding: 10px 12px; width: min(96vw, 360px); }
}

/* phones: nothing below 11px — 10.5px badge text was unreadable next to a 15px label */
@media (max-width: 560px) {
  .chip-count, .cat-n { font-size: 11.5px; }
  .gc-badge { font-size: 11px; }
  .chip { padding: 8px 11px; font-size: 12.5px; }
}

/* ── win celebration (fired by sound.tada() — every game calls it on a win) ── */
.confetti-host { position: fixed; inset: 0; pointer-events: none; z-index: 3000; overflow: hidden; }
.confetti-bit {
  position: absolute; top: -14px; width: 8px; height: 14px; border-radius: 2px;
  opacity: .95; animation: confettiFall linear forwards;
}
@keyframes confettiFall {
  to { transform: translateY(106vh) rotate(720deg); opacity: .65; }
}

/* ---------- the standard gamepad (public/pad.js) ----------
   NES shape: D-pad on the left, B then A on the right, Start/Select between them.
   Same layout on every game that declares one, so the hand is never retaught.
   The container is pointer-events:none and only the buttons take pointers — a
   transparent full-width bar must never eat a tap that belonged to the board
   (see the toast bug: an invisible box that still hit-tests). */
.gamepad {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 620;
  display: none; align-items: flex-end; justify-content: space-between;
  padding: 6px calc(10px + var(--sar)) calc(8px + var(--sab)) calc(10px + var(--sal));
  pointer-events: none; user-select: none; -webkit-user-select: none;
  /* the D-pad and the A/B cluster sit on ONE centre line, as they do on the
     handheld — bottom-aligning them left the arrows riding higher than the buttons */
  align-items: center;
}
body.pad-on .gamepad, body.touch .gamepad.has-keys { display: flex; }
@media (any-pointer: coarse) { .gamepad.has-keys { display: flex; } }
.gamepad .side { display: grid; gap: 6px; pointer-events: none; }
/* Rick: "tetris is great but i need the arrows buttons to be bigger so that its easier to
   use". Measured: the arrows were 46x46 at a 15px glyph while the A/B rounds were 62x62 at
   17px — the buttons a player presses most were the smallest targets in the pad, and the
   STASH chip was 58x26, half a thumb. The arrows now match the action buttons and the glyph
   grows with them. Landscape keeps the tighter sizing on purpose: the board there is only
   ~106px tall, and a bigger pad would eat what is left of the playfield. */
.gamepad .side-left { display: flex; flex-direction: column; gap: 6px; }
/* Each row is a flex line, so an arrow's place is its ORDER — no rule names it and nothing
   can displace it. (The old 3x3 grid also wasted five empty cells and 60px of width.) */
.gamepad .drow { display: flex; gap: 6px; justify-content: center; }
/* The handheld layout: D-pad left, A/B right, and B sits LOWER-LEFT of A on a
   diagonal, the way a Game Boy reads. Rows are shorter than the buttons so the
   offset is a partial one (46px against a 62px button) rather than a full step. */
.gamepad .side-right { grid-template-columns: repeat(2, 62px); grid-template-rows: 40px 40px; align-items: center; justify-items: center; }
.gamepad .side-mid { grid-template-columns: repeat(2, auto); gap: 8px; align-items: end; }
.gamepad .pad-btn {
  pointer-events: auto; touch-action: none;
  display: flex; align-items: center; justify-content: center;
  min-width: 58px; min-height: 58px; border-radius: 14px;
  border: 1px solid rgba(255,255,255,.14);
  background: rgba(18,24,32,.72); color: #dbe6f2;
  font: 700 20px/1 -apple-system, system-ui, sans-serif;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 3px 12px rgba(0,0,0,.35);
}
/* `.pressed`, never a direction name: `.down` is the ▼ ARROW's position rule, and
   naming the pressed state after it displaced the ▼ into cell 1/1. */
.gamepad .pad-btn.pressed { background: rgba(74,163,255,.35); border-color: rgba(120,190,255,.6); transform: translateY(1px); }
.gamepad .dir { border-radius: 10px; }
.gamepad .round {
  width: 62px; height: 62px; min-width: 62px; min-height: 62px; border-radius: 999px;
  font-size: 17px; background: rgba(24,32,44,.8);
}
.gamepad .round.b { grid-area: 2 / 1; }
.gamepad .round.a { grid-area: 1 / 2; background: rgba(38,54,78,.82); }
.gamepad .meta { min-width: 58px; min-height: 26px; border-radius: 999px; font-size: 10px; letter-spacing: .8px; }
.gamepad .side-mid { grid-template-columns: auto auto; align-content: end; }
/* The pad owns a BAND at the bottom of the screen. It is CHROME, so a game that
   sizes its board from --play-h adds var(--pad-h) to its own --chrome and the
   whole layout lands above the pad (tetris measured its Pause/New row at
   y750-812 with the pad's top at 622 — the A button sat on top of them). */
body.pad-on { --pad-h: 148px; }

@media (max-height: 460px) { /* landscape phone: tuck the pad in and shrink it a little */
  .gamepad { padding-bottom: calc(4px + var(--sab)); }
  .gamepad .side-left { grid-template-columns: repeat(3, 40px); grid-template-rows: repeat(3, 40px); }
  .gamepad .round { width: 54px; height: 54px; min-width: 54px; min-height: 54px; }
}

/* ---------- the first-run rules card ----------
   One sentence of how-to over the board, once per game per device. It is REMOVED
   on dismiss, not hidden: a transparent element still hit-tests and would sit on
   the board forever (the toast bug). */
.howto {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 640;
  top: calc(8px + var(--sat)); width: min(560px, calc(100vw - 24px));
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px 10px 14px; border-radius: 14px;
  background: rgba(18,24,32,.96); border: 1px solid var(--line);
  box-shadow: 0 10px 30px rgba(0,0,0,.45);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.howto-text { margin: 0; font-size: 13px; line-height: 1.4; color: var(--fg); }
.howto-hint { margin: 8px 0 0; font-size: 12.5px; line-height: 1.4; color: var(--dim, #9aa3c0); text-align: center; }
.howto-text b { display: block; font-size: 12px; letter-spacing: .4px; text-transform: uppercase; color: var(--dim); margin-bottom: 2px; }
.howto-ok {
  flex: none; min-width: 44px; min-height: 44px; padding: 0 14px;
  border-radius: 12px; border: 1px solid rgba(120,190,255,.5);
  background: rgba(74,163,255,.2); color: #dcebff;
  font: 700 13px/1 -apple-system, system-ui, sans-serif;
}
@media (max-width: 420px) { .howto { flex-direction: column; align-items: stretch; } .howto-ok { width: 100%; } }

/* ── the featured banner ─────────────────────────────────────────────────────
   Second pass. A vision critique of the first version named it exactly: "the skeleton is
   premium, the skin isn't" — a muddy emoji inside a second rounded square (a rectangle in
   a rectangle), ~40% of the banner dead, and three times taller than its content needed.
   So: no nested box — the glyph sits ON the gradient and bleeds toward the edge; roughly
   half the height; text 55 / art 45; a saturated diagonal wash with a light sweep, a top
   edge highlight and a soft outer shadow so it reads as a raised surface; a warm glow
   behind the art so a red glyph does not vibrate against a green field; and the game's
   own category as the micro-detail, because a recommendation needs a reason.
   The accent is still hashed from the id, so no per-game art exists anywhere. */
.hero {
  position: relative; display: flex; align-items: center; gap: 10px;
  margin: 0 0 16px; padding: 14px 0 14px 16px; border-radius: 16px; overflow: hidden;
  text-decoration: none; color: var(--fg); isolation: isolate;
  background:
    radial-gradient(52% 165% at 99% 42%, rgba(255, 214, 150, .30), transparent 66%),
    radial-gradient(120% 200% at 92% 60%, rgba(255, 190, 140, .13), transparent 60%),
    radial-gradient(120% 150% at 100% 0%, var(--gc-glow), transparent 58%),
    linear-gradient(118deg,
      color-mix(in srgb, var(--gc-accent) 46%, #0c1119) 0%,
      color-mix(in srgb, var(--gc-accent) 20%, #101621) 46%,
      #0e131c 100%);
  border: 1px solid color-mix(in srgb, var(--gc-accent) 18%, rgba(255,255,255,.09));
  box-shadow: 0 12px 26px rgba(0,0,0,.38), inset 0 1px 0 rgba(255,255,255,.14);
  animation: heroIn .42s cubic-bezier(.2,.9,.3,1.05) both;
}
/* a light sweep across the top-left, which is what stops a gradient reading as flat */
.hero::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(102deg, rgba(255,255,255,.09) 0%, rgba(255,255,255,0) 34%);
}
@keyframes heroIn { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
.hero:active { transform: translateY(1px); }
.hero-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 55%; }
.hero-eyebrow {
  display: flex; align-items: center; gap: 7px;
  font-size: 10.5px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase;
  color: color-mix(in srgb, var(--gc-accent) 54%, #ffffff);
}
.hero-cat {
  font-size: 10px; font-weight: 700; letter-spacing: .6px; padding: 1.5px 7px; border-radius: 999px;
  color: #f2f6fb; background: rgba(255,255,255,.22); border: 0;
  text-transform: none;
}
.hero-name { font-size: 23px; font-weight: 800; letter-spacing: -.3px; line-height: 1.12; }
.hero-blurb { font-size: 12.5px; color: #e8eef7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hero-cta {
  align-self: flex-start; margin-top: 7px; display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 13px; border-radius: 999px; font-size: 13px; font-weight: 800;
  background: linear-gradient(180deg, #ffffff, #dfe8f4); color: #0d1522;
  box-shadow: 0 2px 8px rgba(0,0,0,.26), inset 0 1px 0 rgba(255,255,255,.92);
}
.hero:active .hero-cta { transform: translateY(1px); }
/* the art bleeds past the padding to the right edge — that emptiness was the complaint */
.hero-art { flex: 0 0 auto; align-self: stretch; display: grid; place-items: center; padding-right: 2px; }
.hero-glyph {
  font-size: 60px; line-height: 1; transform: rotate(-14deg) translateX(6px);
  filter: drop-shadow(3px 7px 12px rgba(0,0,0,.55));
}
@media (max-width: 420px) {
  .hero { padding: 13px 0 13px 14px; }
  .hero-name { font-size: 21px; }
  .hero-glyph { font-size: 54px; }
}
@media (prefers-reduced-motion: reduce) { .hero { animation: none; } }


/* HUD action buttons: "Pause" overflowed its 33px box by 17px and "New" by 7px, both
   wrapping mid-word. Measured, not guessed — they size to their text and the stat block
   gives up the room instead of the label breaking. */
#btn-pause, #btn-new, .hud button {
  flex: 0 0 auto;
  white-space: nowrap;
  min-width: max-content;
  padding-inline: 7px;
}
.pad-btn, .gamepad button { white-space: nowrap; }


/* the mid cluster's STASH chip was 58x26 — shorter than a fingertip */
.gamepad .side-mid .pad-btn { min-height: 36px; min-width: 58px; font-size: 11px; border-radius: 10px; }

/* landscape: the board is the scarce resource there (~106px tall), so the pad stays tight */
@media (orientation: landscape) and (any-pointer: coarse) {
  .gamepad .side-left { grid-template-columns: repeat(3, 48px); grid-template-rows: repeat(3, 48px); }
  .gamepad .pad-btn { min-width: 48px; min-height: 48px; font-size: 17px; }
  .gamepad .side-mid .pad-btn { min-height: 30px; min-width: 48px; }
}
