* { box-sizing: border-box; }

/* ---- Design tokens: single source of truth for the crimson accent and
   reusable elevation shadows, instead of each component inventing its own
   near-miss red / shadow value. Same visual colors as before (this just
   points every use at one canonical value), so this pass carries no risky
   layout changes -- only Play Online/Bots/Friends button + wordmark tweaks
   noted where the value actually shifts. ---- */
/* ==========================================================================
   COLOUR SYSTEM (Sept 2026)
   ==========================================================================
   Audited and consolidated. Before this there were seven reds, five greens
   and five golds, and red alone was carrying five unrelated meanings: the
   primary button, the destructive button, error text, penalty scores and
   the pips on hearts. When one colour means "do this", "don't do this",
   "that failed" and "you lost points" all at once, it means nothing.

   FOUR COLOURS, ONE JOB EACH:

     THE APP IS RED. THE TABLE IS GREEN.

     RED    the brand, and every primary action: wordmark, Start, Create
            Room, Join, the active tab, the active footer item. This is
            what the app is remembered by.
     JADE   the play surface -- felt, in-game panels, routine in-game
            controls. A card table has to read as felt.
     GOLD   value ONLY: scores, the wild rank, winning, and Least Count!.
            Not chrome, not headings, not ordinary buttons. The moment gold
            appears on something you merely tap, it stops meaning anything.
     WHITE  the cards themselves.

     "Press this" and "don't press this" are both red, which is exactly the
     ambiguity this system was built to remove -- so they are separated by
     WEIGHT instead of hue: primary is a filled red, danger is an outlined
     red. Same family, unmistakably different, and an outline makes you
     pause before leaving a game.

   The consequence worth calling out: Discard is no longer red. It is the
   routine move -- tapped a couple of hundred times a game -- and it should
   not be the loudest thing on the screen. Least Count! keeps the gold,
   because that genuinely is the moment. One loud button per screen.

   Contrast, measured against the jade canvas (#123d30):
     --red at 2.7:1 and --red-dark at 1.6:1 both FAILED the 3.0 minimum for
     UI elements, so red was unusable as a border or outline on felt. The
     fill red below is darker and the text red is a separate, lighter value
     that measures 5.3:1.
   ========================================================================== */
:root {
  /* Ink printed on a white card face. Belongs to the card, not the UI
     palette -- white cards are their own surface with their own two inks. */
  --card-red-ink: #8b1a1a;
  /* Red: exactly two values now. --red-fill for filled surfaces (white text
     on it measures 7.5:1), --red-text for anything sitting ON the felt. */
  /* REVERTED to the original bright red (Sept 2026). Darkening it was my
     call, not a requirement: white text on #e5252c measures 4.5:1 and
     passes. The real defect was red used as TEXT or BORDERS on the felt,
     at 2.0:1 -- and that is fixed by --red-text below, not by dulling the
     fill. So the fill keeps the colour that actually looked right.

     THE RULE THAT MATTERS: --red / --red-fill are for FILLED surfaces with
     white text on them, and nothing else. Anything red sitting directly on
     the dark canvas uses --red-text. */
  --red-fill: #e5252c;
  --red-text: #ff8a80;
  --red: #e5252c;
  --red-dark: #a8151a;
  --red-rgb: 229, 37, 44;
  /* One jade canvas for the entire app (replaces the old near-black
     landing/lobby, bright-green felt body background, plain white modals,
     and light-cream round-result/scorecard -- four different systems that
     used to make each screen look like a different app). Deep, slightly
     blue-leaning green rather than a generic bright "poker felt" green, so
     it still reads as a card table to anyone who plays this genre without
     being a copy of every other rummy/teen-patti app's default green. */
  --jade-bg: #123d30;
  --jade-bg-dark: #081f19;
  --jade-panel: #0d2a22;
  --jade-panel-alt: #0a231c;
  --jade-border: rgba(255,255,255,0.14);
  --text-light: #e8e6ec;
  --text-muted: #9fc4b5;
  /* Two-tier gold: bright for headings/highlights/score moments, a richer
     deep gold reserved for the declare button -- never the same value
     reused for both roles. */
  /* ==================================================================
     BRAND COLOUR -- ONE SWITCH
     ==================================================================
     Which colour leads the app is undecided, so it is a variable rather
     than something spread across twenty rules. Everything that expresses
     "this is the brand / this is the primary action" reads these six and
     nothing else: the primary button, the active tab, the active footer
     item, focus rings, the room code, and your own name in chat.

     CURRENTLY: GOLD.

     To switch to red, swap the six values for the commented pair below.
     Nothing else in the file needs touching, and the contrast work holds
     either way because --brand-line is always the light-on-dark value.

       RED:   --brand: #e5252c;  --brand-2: #c81d23;  --brand-3: #a8151a;
              --brand-on: #ffffff;  --brand-line: #ff8a80;
              --brand-rgb: 229, 37, 44;
     ================================================================== */
  --brand:      #f2c14e;
  --brand-2:    #d4a017;
  --brand-3:    #b8860f;
  --brand-on:   #241a05;
  --brand-line: #f2c14e;
  --brand-rgb:  212, 160, 23;

  --gold: #d4a017;
  --gold-bright: #f2c14e;
  /* Success / positive. Was three separate greens (#4ade80, #9fd8b8,
     #6ba889) used interchangeably; these are the two that survive, with
     distinct jobs -- a border/marker tone and a text tone. */
  --good: #4ade80;
  --good-text: #9fd8b8;
  /* Routine controls. Quiet by design: this is what the Discard button and
     other everyday actions now use, so the one gold button on a screen is
     unambiguously the decisive one. */
  --action-bg: #1b4b3c;
  --action-bg-hover: #236052;
  --action-border: rgba(159,216,184,0.45);
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
  --shadow-md: 0 8px 20px rgba(0,0,0,0.4);
  --shadow-lg: 0 16px 32px rgba(0,0,0,0.5);
}
/* body's own background doesn't cover the page past its own height -- on any
   screen where content (mainly the landing screen's stacked mode-blocks +
   footer nav) runs taller than the viewport, scrolling revealed plain white
   above/below it for a moment, since html itself had no background of its
   own to fall back on. Same dark tone as body's gradient settles to. */
html { background: var(--jade-bg-dark); }
body {
  margin: 0;
  /* Georgia leads the stack for that classic-serif look on English text --
     Telugu support is no longer needed since all Telugu text was removed. */
  font-family: Georgia, 'Times New Roman', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: radial-gradient(circle at top, var(--jade-bg), var(--jade-bg-dark));
  color: var(--text-light);
  /* svh for the same reason as .screen below -- body was the other element
     still sized to the large viewport, which is what let a 100svh child
     still sit inside a taller-than-visible parent. */
  min-height: 100vh; min-height: 100svh;
}
.icon {
  width: 18px; height: 18px; display: inline-block; vertical-align: -4px;
  flex-shrink: 0;
}
button .icon { margin-right: 6px; }
button .icon:only-child { margin-right: 0; }

/* On mobile, 100vh can mean the LARGER viewport (as if the address bar were
   hidden), taller than what's actually visible. That leftover min-height
   was clamping the game screen upward and forcing a page-level scrollbar
   even after switching it to 100dvh. Locking body itself to the current
   dynamic viewport while the game screen is active removes that scroll.
   dvh, though, tracks EVERY change to the visible area -- including the
   on-screen keyboard opening for chat, which shrank this down to a sliver
   and crushed the oval table/seat chips into an unreadable overlapping mess
   while typing. svh (smallest viewport -- assumes browser chrome is at its
   most expanded, same as dvh's usual resting state) stays fixed once set,
   so opening the keyboard no longer resizes the game screen at all -- safe
   here specifically because the game screen never scrolls (overflow:hidden
   below), so there's no gesture that would ever need the browser chrome to
   collapse and reveal extra space in the first place. */
body.game-active {
  min-height: 0;
  height: 100vh;
  height: 100dvh;
  height: 100svh;
  overflow: hidden;
}

/* ------------------------------------------------------------------
   100svh, not 100vh -- the actual cause of "the ad is covering the menu"
   (Sept 2026, after two failed attempts at tuning --ad-safe-bottom).

   On Android, 100vh is the LARGE viewport: the height the page would have
   with system chrome retracted. The visible area is 100svh, which is
   smaller by the gesture bar. So a 100vh screen extends BELOW what you can
   see -- and it takes its bottom padding with it. The ad-safe reserve was
   being applied off-screen, which is why raising it from 50 to 64 changed
   nothing: the footer nav sat just above padding that wasn't visible.

   #screen-game already had height: 100svh for the same reason (see the
   comment there about the keyboard); the other screens never got it.
   The 100vh line stays first as the fallback for anything without svh.
   ------------------------------------------------------------------ */
.screen { display: none; min-height: 100vh; min-height: 100svh; padding: 16px; }
.screen.active { display: block; }
/* Reserve space for the native AdMob banner (zero footprint in the DOM on
   its own -- see admob-init.js) so it never sits on top of a screen's
   bottom-most buttons/nav. --ad-safe-bottom is 0px whenever the banner is
   hidden, which is always true on #screen-game, so this is harmless there
   too -- no #screen-game exclusion needed. */
.screen { padding-bottom: calc(8px + var(--ad-safe-bottom, 0px)); }

/* Game screen fits one viewport height, no page scroll, on any device.
   justify-content:center matters on screens taller than the oval table's own
   max-height cap below (a big desktop monitor, or a modern tall-aspect-ratio
   phone like a Pixel) -- on a SHORT screen the oval's flex-grow already
   consumes all available room before centering ever gets a say, so this
   changes nothing there. It only kicks in once the oval hits its cap and
   stops growing, and instead of the leftover room piling up as dead space
   below the action buttons, it gets split evenly as breathing room above
   and below the whole table+hand+buttons group. */
#screen-game.screen.active {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
  height: 100vh;
  height: 100dvh;
  /* svh wins over dvh above (see body.game-active for the full reasoning) --
     fixes the table visibly shrinking/crushing together the moment the
     on-screen keyboard opens for chat, since svh doesn't react to it. */
  height: 100svh;
  padding: 6px 8px;
  /* Reserves the same native-banner safe zone as every other screen (see
     --ad-safe-bottom in admob-init.js). Since box-sizing is border-box
     (see the * rule at the top of this file), this just shrinks the
     content box within the fixed height above -- the oval table (the only
     flex:1 1 auto, freely-shrinkable element in this column) absorbs the
     reduction, so the "one viewport, no scroll" layout stays intact. */
  padding-bottom: calc(6px + var(--ad-safe-bottom, 0px));
  overflow: hidden;
}

.card-panel {
  background: var(--jade-panel);
  color: var(--text-light);
  border: 1px solid var(--jade-border);
  border-radius: var(--radius-lg);
  padding: 24px;
  max-width: 420px;
  margin: 24px auto;
  box-shadow: var(--shadow-lg);
}

/* ---- Landing / lobby "tabletop hero" screens ---- */
/* Cards fanned out on a felt circle at the top, fading straight into the
   page's own dark green background, with the form sitting directly on it
   (no white card-panel here -- these two screens use their own dark theme). */
/* These two screens are full-bleed (the hero art and top bar run edge to
   edge), so they drop .screen's 16px padding entirely. The bottom is the
   exception: .screen reserves --ad-safe-bottom there so the native AdMob
   banner -- which is drawn OVER the WebView and takes up no layout space of
   its own -- can't cover anything. This rule's `padding: 0` shorthand was
   silently wiping that reservation out (it's more specific than .screen), and
   on the landing screen the bottom-most element is the Home/Stats/Rules nav,
   which ended up sitting exactly underneath the banner and unreachable.
   Keeping the horizontal/top padding at 0 but restoring the bottom safe zone
   fixes it without touching the full-bleed look. */
/* ---------------------------------------------------------------------
   #screen-group was added in the Sept 2026 groups rebuild and was never
   added to ANY of these shared landing/lobby selector lists -- this rule
   and the six below. It uses the same .landing-topbar / .landing-body
   markup, so it looked like it should inherit the same layout, but matched
   none of it. The visible result, confirmed by measuring the live DOM:
     - the screen was display:block, not a flex column
     - the topbar was display:block too, so Back / title / the "..." menu
       stacked VERTICALLY, putting the overflow menu below-left instead of
       top-right
     - the body had no gap, so all four blocks sat flush with no spacing
     - no max-width, so it stretched edge to edge on desktop
   Adding #screen-group to each list is the whole fix; no new rules needed.
   --------------------------------------------------------------------- */
#screen-landing.screen.active, #screen-lobby.screen.active, #screen-group.screen.active {
  padding: 0 0 var(--ad-safe-bottom, 0px);
  /* --------------------------------------------------------------------
     Pin the chrome, scroll the middle (Sept 2026).

     These screens are topbar / body / footer-nav, and .landing-body had no
     height constraint and overflow-y: visible -- so when the content didn't
     fit, it didn't scroll, it simply pushed the footer nav off the bottom
     of the screen. Measured on the live build: 61 + 556 + 61 = 678px of
     content on a 640-tall phone, so Home/Stats/Rules sat 38px below the
     fold and looked, once again, like the ad was eating the menu.

     Height (not min-height) plus a scrollable body means the topbar and the
     nav are always reachable on ANY screen, and only the middle moves. The
     fluid sizing elsewhere keeps most phones from needing to scroll at all;
     this is the guarantee for the ones that still do -- tiny screens, or a
     user with large system font sizes, which no amount of tuning can plan
     for.
     -------------------------------------------------------------------- */
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}
#screen-landing.screen.active > .landing-topbar,
#screen-lobby.screen.active > .landing-topbar,
#screen-group.screen.active > .landing-topbar,
#screen-landing.screen.active > .landing-footer-nav {
  flex: 0 0 auto;
}
#screen-landing.screen.active > .landing-body,
#screen-lobby.screen.active > .landing-body,
#screen-group.screen.active > .landing-body {
  flex: 1 1 auto;
  /* Without min-height:0 a flex child refuses to shrink below its content,
     which would put the overflow straight back where it started. */
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.hero-landing { position: relative; height: 210px; overflow: hidden; }
.hero-landing-sm { height: 150px; }
.hero-felt {
  position: absolute; top: -150px; left: 50%; transform: translateX(-50%);
  width: 420px; height: 420px; border-radius: 50%;
  background: radial-gradient(circle, var(--jade-bg), var(--jade-bg-dark) 70%);
}
.hero-card {
  position: absolute; top: 46px; left: 50%;
  width: 52px; height: 74px; border-radius: 6px; background: #fff;
  display: flex; align-items: center; justify-content: center;
  font-family: Georgia, 'Times New Roman', serif; font-weight: 700; font-size: 19px;
  box-shadow: 0 6px 14px rgba(0,0,0,0.4);
}
.hero-card span { margin-left: 3px; }
.hero-card-1 { transform: translate(-84%, 0) rotate(-10deg); color: #c0392b; }
.hero-card-2 { transform: translate(-16%, 6px) rotate(7deg); color: #1a1a1a; }
.hero-card-3 { transform: translate(46%, 2px) rotate(-23deg); color: #c0392b; }
.hero-fade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(8,31,25,0) 0%, rgba(8,31,25,0.92) 55%, var(--jade-bg-dark) 78%);
}
.hero-content {
  position: absolute; left: 0; right: 0; bottom: 8px; text-align: center; padding: 0 16px;
}
.hero-content h1 { color: #fff; font-size: 27px; font-weight: 700; margin: 0; }
.hero-subtitle { color: var(--good-text); font-size: 12px; margin: 4px 0 0; }

/* ---- Landing screen only: cinematic dark hero, redesigned separately from
   the green "tabletop hero" the lobby screen still uses above (.hero-landing
   etc.) -- scoped under #screen-landing throughout so nothing here leaks
   into the lobby. Netflix-style bold dark backdrop + a chess.com-style
   glossy floating card visual as the hero, instead of a green felt circle. ---- */
#screen-landing.screen.active { background: var(--jade-bg-dark); }

.landing-hero {
  position: relative; height: 360px; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  background: var(--jade-bg-dark);
}
/* Soft crimson glow radiating from behind the card fan -- the one spot of
   color in an otherwise near-black hero, so the cards and wordmark read as
   the deliberate focal point instead of everything competing for attention. */
.landing-hero-glow {
  position: absolute; top: 18px; left: 50%; transform: translateX(-50%);
  width: 380px; height: 380px; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--red-rgb),0.50) 0%, rgba(212,160,23,0.22) 38%, rgba(var(--red-rgb),0) 70%);
  filter: blur(2px);
}
.landing-hero-cards {
  position: relative; width: 100%; height: 132px; margin-top: 26px;
  perspective: 900px;
}
.landing-card {
  position: absolute; top: 0; left: 50%;
  width: 68px; height: 96px; border-radius: 8px;
  background: linear-gradient(155deg, #ffffff 0%, #f2f2f4 55%, #e2e2e6 100%);
  display: flex; align-items: center; justify-content: center;
  font-family: Georgia, 'Times New Roman', serif; font-weight: 700; font-size: 23px;
  box-shadow: 0 18px 30px rgba(0,0,0,0.55), 0 2px 0 rgba(255,255,255,0.06) inset;
  transform-style: preserve-3d;
}
.landing-card span { margin-left: 3px; }
/* Continuous, slow floating/tilting motion -- like a product turntable shot
   or chess.com's animated hero piece -- rather than a static tilt, since a
   still image alone didn't read as "3D" no matter how the shadows/angles
   were set. Each card runs its own keyframe (translate stays fixed at its
   fanned-out position; only rotation and a small vertical bob animate) with
   a staggered delay so the three don't move in obvious lockstep. */
.landing-card-1 { color: #c0392b; z-index: 1; animation: landingCardFloat1 6s ease-in-out infinite; }
.landing-card-2 { color: #1a1a1a; z-index: 3; box-shadow: 0 22px 36px rgba(0,0,0,0.6), 0 2px 0 rgba(255,255,255,0.08) inset; animation: landingCardFloat2 6s ease-in-out infinite; animation-delay: -1.3s; }
.landing-card-3 { color: #c0392b; z-index: 2; animation: landingCardFloat3 6s ease-in-out infinite; animation-delay: -2.6s; }
@keyframes landingCardFloat1 {
  0%, 100% { transform: translate(-118%, 10px) rotate(-14deg) rotateY(14deg); }
  50%      { transform: translate(-118%, 0px) rotate(-18deg) rotateY(28deg); }
}
@keyframes landingCardFloat2 {
  0%, 100% { transform: translate(-50%, -6px) rotate(0deg) rotateY(0deg); }
  50%      { transform: translate(-50%, -16px) rotate(3deg) rotateY(8deg); }
}
@keyframes landingCardFloat3 {
  0%, 100% { transform: translate(18%, 10px) rotate(14deg) rotateY(-14deg); }
  50%      { transform: translate(18%, 0px) rotate(18deg) rotateY(-28deg); }
}
.landing-hero-fade {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(8,31,25,0) 0%, rgba(8,31,25,0.85) 60%, var(--jade-bg-dark) 88%);
}
.landing-hero-content {
  position: relative; text-align: center; padding: 0 16px 14px; z-index: 4;
}
.landing-wordmark {
  /* Cinzel -- Roman-inscription capitals, a regal/imperial display face --
     reserved exclusively for this wordmark, never used for body text or
     headings elsewhere, so it stays a distinctive brand moment rather than
     getting diluted. Cinzel wants open tracking (unlike Bebas Neue, which
     wanted it tight), so letter-spacing goes positive here.

     REWRITTEN Sept 2026. Three things were wrong with the old rule:

     1. It declared font-size: 40px, but every single use in index.html also
        carried .landing-wordmark-sm, which overrode it to 18px. The 40px
        "hero" wordmark had therefore never rendered anywhere in the app.
        18px in the topbar is what actually shipped and what we're keeping,
        so the size now lives here and the -sm override is deleted rather
        than left behind as CSS that looks like an intention.

     2. It was gradient-filled white -> #f2d9d9 (pink) -> var(--red), left
        over from the red-brand experiment. It was the last element in the
        app still asserting red as the brand identity while everything else
        resolved through --brand gold.

     3. That red base stop measured 2.67:1 against the jade background, so
        the bottom third of every letter was below the readable threshold.
        18px bold does NOT qualify for WCAG's large-text exemption (that
        needs 18.66px bold), so 4.5:1 was the bar, not 3.0:1.

     Now a flat --brand gold at 7.21:1. Flat, not a gradient: across 18px of
     letter height a three-stop vertical ramp has roughly 10px to work in,
     which reads as unevenly dim rather than as metallic depth. The glow
     went with it -- a 24px red shadow under 18px text was only haze.

     --brand-line, NOT --brand. This is light text on a dark bar, and
     --brand-line is the token the brand switch at the top of this file
     guarantees is the light-on-dark value; --brand is the fill value, meant
     for button backgrounds. Under gold the two happen to be identical
     (#f2c14e, 7.21:1), so --brand would have looked correct forever -- right
     up until someone flipped the switch to red, where --brand is #e5252c at
     2.67:1 and --brand-line is #ff8a80 at 5.30:1. Using the fill token here
     would have quietly re-created the exact contrast bug this rewrite was
     fixing, and only on the branch nobody was testing. */
  font-family: 'Cinzel', Georgia, 'Times New Roman', serif;
  font-size: 18px; letter-spacing: 1.5px; font-weight: 800; margin: 0;
  line-height: 1.1;
  color: var(--brand-line);
}

/* Group screen topbar title. Was sharing .landing-wordmark, which meant
   app.js line ~2939 setting it to group.name rendered whatever the user
   typed in Roman inscription capitals in brand gold -- so an ordinary group
   called "Sunday Cousins" looked like the product's own logo. Two problems
   at once: it diluted the wordmark, and Cinzel (caps-only by design, tight
   glyph set) handles arbitrary user text badly -- lowercase gets
   small-capped, emoji fall back mid-word.
   Same size and position in the bar, ordinary app serif, no brand colour. */
.topbar-title {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 18px; font-weight: 700; margin: 0; line-height: 1.1;
  color: var(--text-light);
  /* A long group name must not shove the back button or the kebab menu out
     of the bar -- it truncates instead. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.landing-tagline { color: #b9b9c0; font-size: 12px; margin: 6px 0 0; letter-spacing: 0.3px; text-align: center; }

/* ---- Landing screen rebuild: card-suit pattern woven into the background
   across the WHOLE screen (not a separate hero block), a chess.com-style
   top bar (avatar/streak left, wordmark center, premium/stats right), and
   three separate cards -- one per way to start a game -- instead of one
   long mixed form. Responsive from small phones up through desktop: full
   bleed under 1024px, capped to a centered phone-width column above that
   so it doesn't stretch edge-to-edge on a wide monitor. ---- */
#screen-landing.screen.active,
#screen-lobby.screen.active,
#screen-group.screen.active {
  background: var(--jade-bg-dark); position: relative;
  /* Horizontal overflow stays hidden -- that's what keeps the decorative
     suit-pattern ::before layer from widening the page. Vertical, though,
     must NOT be hidden: on a short screen (or with a tall ad banner eating
     the bottom), this screen's content is genuinely taller than the space
     available, and `overflow: hidden` silently CLIPPED the overflowing part
     -- which is the Home/Stats/Rules nav, since it sits last. Allowing a
     scroll means that in the worst case the nav can still be reached by
     scrolling, instead of being cut off with no way to get to it. On normal
     phones nothing scrolls at all, because the content fits. */
  overflow-x: hidden; overflow-y: auto;
  display: flex; flex-direction: column;
  min-height: 100vh; min-height: 100svh;   /* see the note on .screen above */
}
/* The suit-watermark layer that lived here has been replaced by the
   scattered-card backdrop at the end of this file -- real cards from
   cardEl(), not a drawing of suits. Two backdrops would have fought each
   other, so this one is gone rather than disabled. */
/* z-index:1 lifts these above the ::before pattern layer -- required, not
   decorative: without it the pattern paints over the content. */
#screen-landing .landing-topbar,
#screen-landing .landing-body,
#screen-landing .landing-footer-nav,
#screen-lobby .landing-topbar,
#screen-lobby .landing-body,
#screen-group .landing-topbar,
#screen-group .landing-body,
/* Added with the watermark: these two screens never had a ::before layer
   before, so nothing here needed lifting. The moment they got one, their
   only child would have been painted over -- the exact failure the note
   above describes. */
#screen-waiting-host .landing-form,
#screen-queue-waiting .landing-form { position: relative; z-index: 1; }

#screen-landing .landing-topbar,
#screen-lobby .landing-topbar,
#screen-group .landing-topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,0.08);
}
/* space-between, not center (Oct 2026). This was centred back when the lobby
   top bar held nothing but the wordmark -- centring a lone child was right
   then. It now carries a rules button on the right and a matching empty
   spacer on the left, so the three columns must spread; centring would bunch
   them in the middle and push the wordmark off-axis. */
#screen-lobby .landing-topbar { justify-content: space-between; }
.landing-topbar-left, .landing-topbar-right { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.landing-topbar-left { position: relative; }
.landing-avatar {
  width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,0.1);
  display: flex; align-items: center; justify-content: center; font-size: 14px;
  border: none; padding: 0; cursor: pointer;
}
.landing-avatar:hover { background: rgba(255,255,255,0.16); }
/* .landing-wordmark-sm deleted Sept 2026 -- it existed only to shrink the
   40px hero rule to 18px, and since every use carried it, 18px was the real
   size. That size moved into .landing-wordmark itself, so this class had
   nothing left to do. Removed from index.html at the same time; a no-op
   class left in the markup is the same trap as the dead 40px rule was. */

/* Profile panel -- reuses the app's existing .overlay (full-screen dim +
   centered flex) and .card-panel (white rounded card) pattern, the same
   one Rules/Scores/My Stats already use reliably. .profile-modal-panel
   only overrides the colors to match the dark/crimson theme instead of
   card-panel's default white -- no custom positioning of its own, so it
   can't drift, get clipped, or let page content bleed through the way the
   previous anchored-dropdown version repeatedly did. */
.profile-modal-panel { max-width: 320px; width: 100%; }
.profile-modal-panel h2 { color: #fff; font-size: 18px; text-align: center; }
.profile-modal-panel label { color: #9a9aa2; }
.profile-modal-panel input[type=text] {
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); color: #fff;
}
.profile-modal-panel .signin-status { color: #9a9aa2; }
.profile-modal-panel .secondary {
  background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: #e8e8ec;
}

/* Nudges attention to the avatar itself right after a blocked action (e.g.
   trying to create/join/start without a name yet) -- pairs with the toast
   banner below so there are two independent, hard-to-miss signals instead
   of relying on the dropdown alone (which is easy to not notice opening in
   a top corner while looking at the button just tapped). */
@keyframes avatarAttnPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--red-rgb),0.7); }
  50% { box-shadow: 0 0 0 9px rgba(var(--red-rgb),0); }
}
.landing-avatar.attn-pulse { animation: avatarAttnPulse 0.7s ease-out 2; }

/* Inline error banner in the main page flow (not tucked inside the
   dropdown) -- see #landing-toast-error in index.html. This is what makes
   "enter your name" etc. actually visible regardless of whether the
   profile dropdown happens to be open. */
.landing-toast-error {
  background: rgba(var(--red-rgb),0.14); border: 1px solid var(--red-text); color: var(--red-text);
  border-radius: 10px; padding: 10px 14px; font-size: 12.5px; text-align: center;
  margin: 0; cursor: pointer; animation: toastIn 0.35s ease-out;
}
.landing-toast-error.hidden { display: none; }
@keyframes toastIn {
  0% { transform: translateX(0); opacity: 0; }
  20% { transform: translateX(-6px); opacity: 1; }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
  100% { transform: translateX(0); }
}

#screen-landing .landing-body,
#screen-lobby .landing-body,
#screen-group .landing-body {
  flex: 1; display: flex; flex-direction: column; gap: 14px;
  padding: 14px 16px 10px; max-width: 480px; width: 100%; margin: 0 auto; box-sizing: border-box;
}

.mode-block {
  background: linear-gradient(170deg, rgba(255,255,255,0.085) 0%, rgba(255,255,255,0.03) 60%);
  border: 1px solid rgba(255,255,255,0.14);
  /* The inset top highlight is what makes this read as a lit surface rather
     than a flat rectangle -- one pixel of light along the top edge implies
     a light source, and that is most of what "not flat" means. */
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.10);
  border-radius: 14px; padding: 14px 16px;
}

.mode-blocks { display: flex; flex-direction: column; gap: 12px; }

/* ---- Landing tabs: Groups | Quick Play (Sept 2026) ----
   The landing screen had grown to 840px of content in an 812px viewport, so
   the Home/Stats/Rules nav sat below the fold -- and with the native AdMob
   banner drawn over the bottom ~50-64px on the app, it looked like the ad was
   eating the menu. Splitting four blocks across two panes brings it to 812px
   exactly, nav included, with the banner's safe zone still reserved.

   .mode-blocks supplies the 12px gap between blocks, but its children are now
   the two PANES rather than the blocks themselves -- so without this rule the
   two blocks inside a pane would sit flush against each other. */
/* 10px, not 12: the Quick Play pane gained a second "Join with a code" box
   (Sept 2026) and measured 814px in an 812px viewport -- 2px over, which put
   the footer nav back under the ad banner. Reclaimed here and in the two
   rules below rather than by cutting any copy. Measured after: 796px. */
.landing-pane { display: flex; flex-direction: column; gap: 10px; }
/* Two classes, so this beats .landing-pane's display:flex above. A bare
   .hidden would tie on specificity and lose on source order. */
.landing-pane.hidden { display: none; }
/* .landing-body is already a flex column with its own 14px gap; .segmented's
   default bottom margin would stack on top of that. */
.landing-tabs { margin-bottom: 0; }
/* Part of the same 18px reclaim as .landing-pane's gap above. Scoped to the
   landing screen so the group screen's blocks keep their roomier padding --
   that screen scrolls, so it isn't fighting for the last few pixels. */
#screen-landing .landing-body { gap: 12px; }
#screen-landing .mode-block { padding: 12px 16px; }
/* The Quick Play pane carries three blocks to the Groups pane's two, so it
   is the one that decides whether the landing screen fits. These three
   shave the internal spacing only -- no copy cut, no control shrunk below
   the 44px floor. Measured on 375x812 with a 90px banner (the tallest
   adaptive size Google will serve at this width): 822px before, 806 after. */
#screen-landing .mode-block h3 { margin-bottom: 2px; }
#screen-landing .mode-block-sub { margin-bottom: 6px; }
#screen-landing .mode-block .row { margin-top: 8px; }

/* ---- Short screens (<=700px tall: 360x640-class phones) ----
   Measured on 360x640 with the tabs in place: the layout survives a 50px and
   a 64px banner, but an adaptive banner of 90px pushes the footer nav 22px
   under it and 120px pushes it 51px under. AdMob picks the banner height at
   request time from the device, so we can't assume the small one.

   This claws back ~80px from padding, gaps and the groups list -- enough to
   clear the worst case with room to spare. Deliberately NOT touched: any
   button or nav-item height, all of which are held at the 44px minimum from
   the Sept 11 audit. Cramped is recoverable; untappable is not. */
@media (max-height: 700px) {
  #screen-landing .landing-topbar { padding: 8px 16px; }
  #screen-landing .landing-body { padding: 8px 12px 6px; gap: 10px; }
  #screen-landing .mode-block { padding: 10px 14px; }
  #screen-landing .mode-blocks,
  #screen-landing .landing-pane { gap: 8px; }
  #screen-landing .group-list { max-height: 96px; }
  #screen-landing .mode-block h3 { font-size: 14px; }
  #screen-landing .mode-block .row { margin-top: 6px; }
  #screen-landing .mode-block-sub { margin-bottom: 6px; }
  #screen-landing label { margin-bottom: 2px; }
}

/* The custom dropdown's own button measured 42px -- the only control on the
   landing screen still under the 44px floor the Sept 11 audit set. It has no
   min-height of its own (unlike .segmented .seg and .footer-nav-item, which
   were given one then), so it was sized purely by its 12px padding. */
.dd-toggle { min-height: 44px; }

/* ---- A block heading with an action on the same row (Sept 2026) ----
   Used by Members + Invite. A secondary action doesn't need a full-width
   button and a whole row of its own. */
.block-heading-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.block-heading-row h3 { margin: 0; }
.block-heading-row button {
  width: auto; margin: 0; padding: 0 14px; min-height: 36px;
  white-space: nowrap; flex: 0 0 auto;
}

/* ---- Group chat FAB ----
   Reuses .chat-fab wholesale (position, size, colour, the --ad-safe-bottom
   offset). This rule exists only to sit it ABOVE the table chat's own fab in
   the stacking order -- they're never both visible, but if a stale one ever
   lingered, the one for the screen you're actually on should win. */
/* The GROUP screen keeps a real floating action button, and deliberately so.
   The game screen's chat toggle moved into the hand-header row because that
   screen is a fixed, non-scrolling board where a floating button could only
   ever sit on top of the cards. This screen is the opposite: a scrolling
   feed of members and messages, which is exactly the layout a FAB was
   designed for, and there are no cards underneath it to cover.
   These rules restore the floating look on top of the now-in-flow .chat-fab
   base -- they must stay in sync if that base changes again. */
.group-chat-fab {
  position: fixed;
  bottom: calc(16px + var(--ad-safe-bottom, 0px));
  right: 16px;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: none;
  background: linear-gradient(135deg, var(--red), var(--red-dark));
  color: #fff;
  box-shadow: var(--shadow-md);
  z-index: 21;
}
.group-chat-fab .icon { width: 24px; height: 24px; }
.group-chat-fab::before { content: none; }   /* the 44px tap pad is redundant at 52px */

/* ---- Read-only chat preview on the group screen ----
   Shares .group-chat-list's styling so a message looks identical here and
   in the sheet it opens. Only the sizing differs: no internal scrolling,
   because five messages is the whole point -- a scrollbar here would
   invite people to read the history in a window built for glancing. */
.group-chat-preview-block { cursor: pointer; }
.group-chat-preview { max-height: none; overflow: visible; margin-bottom: 0; }
.group-chat-preview-block .group-count { font-size: 11px; color: var(--text-muted); }
#group-chat-preview-empty { margin: 6px 0 0; }
/* The group chat's empty-state line sits inside the sheet, where .hint's
   default margins leave it floating in the middle of the message area. */
.group-chat-empty { margin: 12px 16px; text-align: center; }
/* Plain, not gold: a heading sitting directly above a button must not
   compete with it. Gold is reserved for value now. */
.mode-block h3 { margin: 0 0 4px; font-size: 15px; color: var(--text-light); }
.mode-block-sub { margin: 0 0 8px; font-size: 11px; color: #9a9aa2; }
.mode-block .row { margin-top: 10px; }
.mode-block-join-row { display: flex; gap: 8px; margin-top: 8px; }
.mode-block-join-row input { flex: 1; margin: 0; }
.mode-block-join-row button { width: auto; margin: 0; padding: 0 16px; white-space: nowrap; }

/* Same dark-crimson palette applied to buttons/selects/labels sitting
   directly inside a .mode-block -- both the landing screen's Play
   Online/Friends/Bots cards AND the lobby's room-code/players/start cards
   below use this same wrapper, so one set of rules covers both screens.
   .mode-block itself only sets the CARD's own background/border, not its
   children's colors, so without this the buttons/select/labels inside would
   otherwise still show the app's original light-mode/green defaults. */
/* DELETED (Sept 2026): this repainted primary buttons red, more
   specifically than `button.primary`, so changing the palette in one place
   silently did nothing on the two screens that matter most. The global
   rule now owns this -- do not reintroduce a per-screen copy. */
#screen-landing .mode-block button.secondary,
#screen-lobby .mode-block button.secondary {
  background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: #e8e8ec;
}
#screen-landing .mode-block select,
#screen-lobby .mode-block select,
#screen-group .mode-block select {
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.14); color: #fff;
}
/* #screen-group added here too: the group screen is full of .hint text
   (bell hint, empty board, scoring note, empty chat) and an .error line,
   all of which were falling back to defaults meant for a light surface. */
#screen-landing .mode-block label,
#screen-lobby .mode-block label,
#screen-group .mode-block label,
#screen-lobby .mode-block .hint,
#screen-group .mode-block .hint { color: #9a9aa2; }
#screen-lobby .mode-block .error,
#screen-group .mode-block .error { color: var(--red-text); }

/* Lobby-specific content that isn't shared with the landing screen's
   mode-blocks -- room code, player list. */
#screen-lobby .room-code-label {
  color: #9a9aa2; font-size: 11px; text-align: center; text-transform: uppercase; letter-spacing: 1px;
}
#screen-lobby .room-code-display {
  background: transparent; color: var(--brand-line); font-size: 38px; letter-spacing: 8px; text-align: center;
  box-shadow: none; text-shadow: 0 2px 10px rgba(var(--red-rgb),0.3); padding: 4px 0;
}
#screen-lobby .player-list li { border-bottom: 1px solid rgba(255,255,255,0.12); color: #e8e8ec; }
#screen-lobby .player-list .host-tag { color: var(--text-muted); }
#screen-lobby .player-list .status { color: var(--good-text); }

#screen-landing .landing-footer-nav {
  display: flex; align-items: center; justify-content: space-around;
  padding: 8px 4px; border-top: 1px solid rgba(255,255,255,0.08);
  max-width: 480px; width: 100%; margin: 0 auto;
}
.footer-nav-item {
  background: none; border: none; color: #9a9aa2; font-size: 10px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 4px 10px; width: auto;
}
.footer-nav-item .nav-icon { width: 20px; height: 20px; display: flex; }
.footer-nav-item .icon { width: 20px; height: 20px; margin: 0; }
.footer-nav-item.active { color: var(--brand-line); }

.queue-waiting-choice { margin-top: 18px; }
.queue-waiting-choice button { margin-top: 8px; }

/* ---- tablets (768-1023px): a bit more breathing room, everything else
   scales naturally since it's already flex-based rather than fixed px. ---- */
@media (min-width: 768px) {
  #screen-landing .landing-body,
  #screen-lobby .landing-body,
  #screen-group .landing-body { max-width: 560px; padding: 20px 24px 14px; gap: 18px; }
  #screen-landing .landing-footer-nav { max-width: 560px; }
  .mode-block { padding: 18px 20px; }
}

/* ---- laptop/desktop (1024px+): the phone-style full-bleed layout would
   look broken stretched across a wide browser window, so the content column
   gets capped and centered -- the background pattern still fills the full
   viewport width behind it, so it doesn't look like a narrow box floating
   on blank space. ---- */
@media (min-width: 1024px) {
  #screen-landing .landing-topbar,
  #screen-lobby .landing-topbar { max-width: 480px; margin: 0 auto; width: 100%; }
  #screen-landing .landing-body,
  #screen-lobby .landing-body { max-width: 480px; padding: 24px 16px 14px; }
  #screen-landing .landing-footer-nav { max-width: 480px; }
}

/* ---- 3-second intro splash (matches the reference chess.com launch clip
   the user sent: solid near-black screen, one simple mark centered and held,
   then a quick cut/fade straight into the real app underneath -- no busy
   motion, no separate loading text). Same jade-bg-dark backdrop as the landing
   hero so the fade-out is seamless with zero color flash. Pure CSS, timed
   entirely with animation-delay so it can never get stuck open even if JS
   is slow to attach anything. ---- */
#screen-intro {
  position: fixed; inset: 0; z-index: 999;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--jade-bg-dark); overflow: hidden;
  animation: introFadeOut 0.6s ease forwards;
  animation-delay: 2.2s;
}
.intro-glow {
  position: absolute; width: 420px; height: 420px; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--red-rgb),0.4), rgba(var(--red-rgb),0) 68%);
  filter: blur(2px);
  animation: introGlowPulse 2.6s ease-in-out infinite;
}
.intro-cards {
  position: relative; width: 100%; height: 132px; perspective: 900px;
  margin-bottom: 22px;
  opacity: 0;
  animation: introCardsIn 0.7s cubic-bezier(.22,.9,.3,1.2) forwards;
  animation-delay: 0.15s;
}
.intro-wordmark {
  position: relative; font-size: 40px;
  opacity: 0;
  animation: introWordmarkIn 0.6s ease forwards;
  animation-delay: 0.55s;
}
@keyframes introCardsIn {
  0%   { opacity: 0; transform: scale(0.7) translateY(20px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes introWordmarkIn {
  0%   { opacity: 0; transform: translateY(10px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes introGlowPulse {
  0%, 100% { transform: scale(1); opacity: 0.7; }
  50%      { transform: scale(1.15); opacity: 1; }
}
/* Holds fully visible, then fades the WHOLE overlay away and drops it out of
   the layout/click-path entirely (visibility+pointer-events) -- the landing
   screen underneath has already been sitting there fully loaded/interactive
   the entire time, so this is a pure visual reveal, nothing to wait on. */
@keyframes introFadeOut {
  0%   { opacity: 1; visibility: visible; }
  99%  { opacity: 0; visibility: visible; }
  100% { opacity: 0; visibility: hidden; pointer-events: none; }
}

/* Sign-in row + form on the landing screen only -- overrides the shared
   green-tinted .landing-form defaults below with the crimson/neutral palette,
   scoped with #screen-landing so the lobby screen's own use of .landing-form
   is completely unaffected. */
#screen-landing .landing-form label,
#screen-waiting-host .landing-form label,
#screen-queue-waiting .landing-form label,
#screen-landing .landing-form .hint,
#screen-waiting-host .landing-form .hint,
#screen-queue-waiting .landing-form .hint,
#screen-landing .landing-form .divider,
#screen-landing .signin-status { color: #9a9aa2; }
#screen-landing .landing-form input[type=text] {
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.14); color: #fff;
}
#screen-landing .landing-form input[type=text]:focus { border-color: var(--brand-line); }
/* DELETED: third copy of the same button. See the note above. */
#screen-landing .landing-form button.secondary,
#screen-waiting-host .landing-form button.secondary,
#screen-queue-waiting .landing-form button.secondary {
  background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: #e8e8ec;
}
#screen-landing .icon-btn { background: rgba(255,255,255,0.08); color: #fff; }
#screen-landing .signin-row { border-bottom: 1px solid rgba(255,255,255,0.1); }

/* The two flow screens that branch off the landing screen (mid-game join
   "waiting for host", and Play Online's "finding players") share its dark
   backdrop and heading color so the whole flow reads as one continuous
   experience instead of snapping back to the lobby's green theme mid-flow. */
#screen-waiting-host.screen.active,
#screen-queue-waiting.screen.active {
  background: var(--jade-bg-dark);
  position: relative; overflow-x: hidden;
}
#screen-waiting-host .landing-form h2,
#screen-queue-waiting .landing-form h2 { color: #fff; }

.landing-form { max-width: 420px; margin: 10px auto 24px; padding: 0 24px 24px; }
.landing-form label { color: var(--good-text); }
.landing-form input[type=text] {
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); color: #fff;
}
.landing-form input[type=text]::placeholder { color: rgba(255,255,255,0.4); }
.landing-form input[type=text]:focus { border-color: var(--brand-line); }
/* DELETED: fourth copy. button.primary is the single source now. */
.landing-form button.secondary {
  background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: var(--text-light);
}
.landing-form .divider { color: var(--text-muted); }
.landing-form .hint { color: var(--good-text); }
.landing-form .error { color: var(--red-text); }
.landing-form h3 {
  color: var(--gold); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;
  margin: 20px 0 8px;
}
.landing-form .room-code-label {
  color: #d4e8da; font-size: 11px; text-align: center; text-transform: uppercase; letter-spacing: 1px;
  text-shadow: 0 1px 3px rgba(0,0,0,0.6); margin-top: 4px;
}
.landing-form .room-code-display {
  background: transparent; color: var(--brand-line); font-size: 38px; letter-spacing: 8px;
  box-shadow: none; text-shadow: 0 2px 10px rgba(0,0,0,0.4); padding: 4px 0;
}
.landing-form .player-list li { border-bottom: 1px solid rgba(255,255,255,0.12); }
.landing-form .player-list .host-tag { color: var(--gold-bright); }
.landing-form .player-list .status { color: var(--text-muted); }
.landing-form .leave-btn { border-color: var(--red-text); color: var(--red-text); }

/* The native <select> stays in the DOM for its value/behavior (see
   initDropdown() in app.js) but is visually replaced by the .dd themed
   button+list below it, so its own appearance barely matters -- kept
   reasonably sane here only as a fallback if JS hasn't run yet. */
select {
  width: 100%; padding: 12px; font-size: 16px; border-radius: var(--radius-sm);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt); color: var(--text-light); outline: none;
}
select:focus { border-color: var(--brand-line); }
.landing-form select {
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); color: #fff;
}
.landing-form select option { color: #1a1a1a; }
.lobby-maxscore-row { margin: 14px 0; }
.lobby-maxscore-row.hidden { display: none; }
.lobby-maxscore-row label { color: var(--good-text); }

.solo-panel { margin: 12px 0; }
.solo-panel.hidden { display: none; }
.solo-panel label { color: var(--good-text); }

/* ---- Custom themed dropdown (replaces the native <select> UI everywhere --
   see initDropdown()/syncDropdown() in app.js). The <select> itself is
   pushed off-screen rather than display:none, so it stays focusable/
   readable to a screen reader and keeps working as the source of truth for
   .value that the rest of app.js already reads/writes. ---- */
.dd { position: relative; }
.dd-native-select {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.dd-toggle {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; font-size: 15px; border-radius: var(--radius-sm);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt); color: var(--text-light);
  cursor: pointer; font-family: inherit; font-weight: 600;
}
.dd-toggle[aria-expanded="true"] { border-color: var(--brand-line); }
.dd-chevron { width: 16px; height: 16px; flex-shrink: 0; transition: transform 0.15s; }
.dd-toggle[aria-expanded="true"] .dd-chevron { transform: rotate(180deg); }
.dd-list {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 30;
  background: var(--jade-panel); border: 1px solid var(--jade-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
  list-style: none; margin: 0; padding: 4px; max-height: 240px; overflow-y: auto;
}
.dd-list.hidden { display: none; }
.dd-list li {
  padding: 9px 12px; border-radius: 6px; font-size: 14px; color: var(--text-light);
  cursor: pointer;
}
.dd-list li:hover, .dd-list li:focus { background: rgba(255,255,255,0.08); outline: none; }
.dd-list li[aria-selected="true"] { background: rgba(212,160,23,0.18); color: var(--gold-bright); font-weight: 700; }

.rules-btn { margin-top: 14px; font-size: 13px; padding: 9px; }

/* Small, non-blocking sign-in row at the top of the landing form -- lets a
   guest optionally upgrade to a real Google account (keeping their existing
   stats via the linkWithPopup path in firebase-init.js) without forcing a
   choice before they can just start playing. */
.signin-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 16px; padding-bottom: 12px;
  border-bottom: 1px solid rgba(255,255,255,0.12);
}
.signin-status { color: var(--good-text); font-size: 12px; flex: 1; }
.signin-row button.tiny { margin: 0; white-space: nowrap; }
.icon-btn {
  /* 44x44 is the floor for anything a thumb has to hit. These were 36x30,
     which is a mis-tap waiting to happen on a control that opens a settings
     menu -- and the icon itself stays small, only the hit area grows. */
  width: auto; flex: 0 0 auto; min-width: 44px; min-height: 44px;
  padding: 6px 9px; font-size: 15px; line-height: 1;
  background: rgba(255,255,255,0.12); color: #fff; border-radius: 8px;
}

.rules-panel { max-width: 480px; width: 92vw; text-align: left; }
.rules-panel h2 { text-align: center; display: flex; align-items: center; justify-content: center; gap: 8px; }
.rules-body { max-height: 60vh; overflow-y: auto; padding-right: 4px; margin-bottom: 14px; }
.rules-item { margin-bottom: 14px; }
.rules-item:last-child { margin-bottom: 0; }
/* Gold marks a heading/highlight moment throughout the app -- a rules
   section title counts, same role as a score or achievement callout. */
.rules-heading { font-weight: 700; color: var(--gold-bright); font-size: 14px; margin-bottom: 3px; }
.rules-item p { margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-light); }

h1 { text-align: center; margin: 0 0 4px; font-size: 28px; }
h2 { margin-top: 0; }
.subtitle { text-align: center; color: var(--text-muted); margin-top: 0; }

label { display: block; font-size: 13px; margin: 14px 0 4px; color: var(--text-muted); }
input[type=text] {
  width: 100%; padding: 12px; font-size: 18px; border-radius: var(--radius-sm);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt); color: var(--text-light); outline: none;
}
input[type=text]:focus { border-color: var(--brand-line); }
input[type=text]::placeholder { color: var(--text-muted); }

.row { margin-top: 16px; }
button {
  width: 100%; padding: 14px; font-size: 16px; border-radius: var(--radius-sm);
  border: none; cursor: pointer; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
/* Primary = the main call-to-action on a screen -- always red (action /
   urgency), never a screen-specific color. Declare = a scoring moment, so
   it keeps the deep-gold token. Secondary = a plain ghost button everywhere
   now that every screen shares the same dark canvas, instead of the old
   light-gray fill that only made sense back when some screens were white. */
/* Primary = a DECISIVE action: start a game, create a room, join. Gold,
   because gold means "the good thing" and these are moments, not chores.
   Near-black text on gold measures 7.3:1. */
button.primary {
  background: linear-gradient(160deg, var(--brand) 0%, var(--brand-2) 55%, var(--brand-3) 100%);
  color: var(--brand-on); box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.30);
}
button.primary:hover:not(:disabled) {
  background: linear-gradient(160deg, #ffd873 0%, var(--brand) 55%, var(--brand-2) 100%);
}
button.secondary { background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: var(--text-light); }
/* Danger = it costs you something or cannot be undone. The ONLY filled red
   in the app now, so red carries one meaning instead of five. */
/* Filled again: with a gold primary there is nothing for red to be
   confused with, so danger can carry its full weight. If the brand switches
   back to red, make this outlined again -- see the note on --brand. */
button.danger {
  background: linear-gradient(160deg, var(--red) 0%, var(--red-dark) 100%);
  color: #fff; margin-top: 8px;
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.18);
}
button.declare {
  background: linear-gradient(160deg, var(--gold-bright) 0%, var(--gold) 55%, #b8860f 100%);
  color: #241a05; box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.35);
}
/* Routine in-game actions. Quiet on purpose -- see the note at the top of
   this file about Discard no longer being the loudest thing on screen. */
button.action {
  background: linear-gradient(160deg, #24604e 0%, var(--action-bg) 100%);
  color: var(--text-light);
  border: 1px solid var(--action-border);
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.10);
}
button.action:hover:not(:disabled) { background: var(--action-bg-hover); }
/* .tiny (below) only ever controlled size, never color -- any button that
   had .tiny and no other color class (Scores, mute, chat-close, GIF-close)
   fell through to the browser's native gray/black button chrome instead of
   the theme. Scoped with :not() so this never fights an already-colored
   combo like .tiny.primary (chat-send) or .secondary.tiny (Google sign-in,
   already covered by .secondary itself) -- same ghost look as .secondary,
   since these are all secondary-weight actions, not primary ones. */
button.tiny:not(.primary):not(.secondary):not(.danger):not(.declare) {
  background: transparent; border: 1.5px solid rgba(255,255,255,0.35); color: var(--text-light);
}
button:disabled { opacity: 0.4; cursor: not-allowed; }
/* min-height 44 rather than more padding: the button stays visually small
   (13px text, tight padding) while the hit area reaches the touch minimum.
   .tiny is used for Invite on the group screen and the chat send button,
   both of which measured 36px tall. */
button.tiny { width: auto; min-height: 44px; padding: 8px 14px; font-size: 13px; }
button.leave-btn { background: transparent; color: var(--red-text); border: 1px solid var(--red-text); margin-top: 10px; }

.divider { text-align: center; margin: 20px 0; color: var(--text-muted); font-size: 12px; }
.error { color: var(--red-text); font-size: 13px; min-height: 16px; }
.hint { color: var(--text-muted); font-size: 12px; text-align: center; }

.room-code-row {
  display: flex; align-items: stretch; gap: 8px; margin: 8px 0;
}
.room-code-display {
  flex: 1; min-width: 0;
  font-size: 40px; letter-spacing: 8px; text-align: center; font-weight: 800;
  background: var(--jade-panel-alt); color: var(--text-light); border-radius: var(--radius-md); padding: 12px 4px; margin: 0;
  overflow: hidden;
}
/* Small square icon buttons, same look as the other icon-btns (mute, remove
   ads) elsewhere in the app, sized to match the room-code box's height
   rather than the usual inline text-button padding. */
.room-code-row .icon-btn {
  flex: 0 0 auto; width: 44px; height: auto;
  border-radius: 12px; background: rgba(255,255,255,0.14);
  display: flex; align-items: center; justify-content: center;
}
.room-code-row .icon-btn svg { width: 20px; height: 20px; }
.roomcode-feedback {
  min-height: 15px; color: var(--good-text); font-weight: 600; margin: 2px 0 0; text-align: center;
}

.player-list { list-style: none; padding: 0; }
.player-list li {
  padding: 10px; border-bottom: 1px solid var(--jade-border); display: flex; justify-content: space-between;
  position: relative; /* anchors the report/mute popover for tappable rows */
}
.player-list li.tappable { cursor: pointer; }
.player-list .status { font-size: 11px; color: var(--text-muted); }
.player-list .host-tag { color: var(--gold-bright); font-weight: 700; font-size: 11px; }

/* ---------------- Game screen ---------------- */
.game-top {
  display: flex; justify-content: space-between; align-items: center;
  /* Caps at the original 13px/3px on any phone 394px wide or more. */
  font-size: clamp(11px, 3.3vw, 13px); padding: 2px 4px; flex-shrink: 0;
}
/* Equal-weight side columns (flex:1 1 0) are what actually centre the round
   number: with plain space-between it would only look centred when the two
   button groups happened to be the same width, which they never are. The
   centre column doesn't grow, so it stays put as button labels change. */
.game-top > .top-actions { flex: 1 1 0; min-width: 0; }
.game-top > .top-actions-left { justify-content: flex-start; }
.game-top > .top-actions-right { justify-content: flex-end; }
/* Buttons carry a 10px vertical margin from the base button rule. In a 58px
   bar that makes the leave button's margin box 64px -- taller than the row,
   so align-items:center can't centre it and it drops to top+10, landing 5px
   below Scores and mute. Scores and mute happen to have their margin zeroed
   elsewhere, which is why only one of the three looked wrong. */
.game-top .tiny { margin: 0; }
.game-top-centre {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center;
  /* Bottom-aligned so the round number holds its position when the timer
     above it appears and disappears each turn. */
  justify-content: flex-end; gap: 2px;
  min-width: 0;
}
.game-round {
  font-size: 12px; color: var(--text-muted); letter-spacing: 0.02em;
  text-align: center; white-space: nowrap;
}
/* In the top bar the timer is a normal flex child, not the fixed, centred
   pill it used to be -- see the markup comment for why. */
.game-top .turn-timer {
  position: static; transform: none; z-index: auto;
  box-shadow: none; padding: 3px 10px;
}
.round-result-round { font-size: 12px; color: var(--text-muted); margin-bottom: 2px; }
.top-actions { display: flex; gap: 6px; align-items: center; }
.mute-btn { padding: 6px 9px !important; font-size: 13px !important; line-height: 1; }
.mute-btn .icon { width: 16px; height: 16px; margin: 0; }

/* Floating, not in the normal document flow -- and crucially, a z-index
   above .chat-panel's (25), so the countdown stays visible even if chat is
   reopened mid-turn instead of getting buried under it (see issue #5: the
   chat panel used to be able to cover this entirely, and the auto-minimize
   in app.js only handles the moment your turn STARTS, not every subsequent
   tap of the chat bubble during that turn). */
.turn-timer {
  position: fixed; top: 8px; left: 50%; transform: translateX(-50%); z-index: 30;
  text-align: center; font-size: 12px; font-weight: 700;
  background: rgba(0,0,0,0.55); border: 1px solid var(--jade-border);
  border-radius: 20px; padding: 4px 12px;
  width: fit-content; transition: color 0.2s;
  box-shadow: var(--shadow-sm);
}
.turn-timer.low { color: var(--red-text); }
.turn-timer.hidden { display: none; }

/* ---- Oval table ---- */
/* Height-driven (not width-driven) so it shrinks to fit short phone
   screens automatically; aspect-ratio derives the matching width.
   This is now the flex-grow element in the game screen's column layout --
   it absorbs whatever vertical space the hand row (below) isn't using,
   instead of that space sitting empty. Still capped by max-height/max-width
   so it can't grow unreasonably large on tall/wide screens -- raised from
   the original 400px (tuned for a fairly short phone) to 560px so a big
   monitor or a modern tall-screened phone (Pixel-class, 900px+ of usable
   height) gets a visibly bigger table instead of just more empty margin;
   any space left over past this cap is now centered by #screen-game's
   justify-content instead of dumped below the action buttons. */
.oval-table {
  position: relative;
  width: auto;
  max-height: min(56vh, 560px);
  /* Capped at 94vw minus a fixed side inset (not just a smaller vw
     percentage) so the table itself never spans truly edge-to-edge on a
     narrow phone. Every seat is positioned as a plain percentage of this
     box's own width/height (see renderOvalTable() in app.js), so tightening
     the container here pulls every seat in with it automatically -- the
     side seats no longer land flush against the screen bezel with zero
     breathing room, without needing to touch the seat-angle math at all. */
  max-width: min(94vw, calc(100vw - 40px));
  aspect-ratio: 1 / 1.05;
  margin: 2px auto 6px;
  flex: 1 1 auto;
  min-height: 0;
}
/* The felt was a flat two-stop gradient inside a plain dark ring, which is
   most of why the biggest element on screen read as a shape rather than an
   object. Three cheap additions give it depth: a warmer centre so the light
   appears to fall on the middle of the table, a much deeper vignette at the
   edge, and a two-tone rim (highlight on top, shadow beneath) so the border
   reads as a raised edge catching light rather than a drawn outline.
   All gradients -- no images, no extra requests. */
.table-felt {
  position: absolute; inset: 12%;
  background:
    radial-gradient(circle at 50% 38%, #1c5540 0%, var(--jade-bg) 42%, var(--jade-bg-dark) 78%, #041009 100%);
  border-radius: 50%;
  border: 6px solid #061712;
  box-shadow:
    inset 0 0 44px rgba(0,0,0,0.55),
    inset 0 2px 1px rgba(255,255,255,0.06),
    inset 0 -3px 6px rgba(0,0,0,0.5),
    0 6px 18px rgba(0,0,0,0.4);
}
/* Faint concentric ring, the way a real felt table has a stitched edge.
   Very low contrast on purpose -- it should register as texture, not as a
   line someone drew. */
.table-felt::after {
  content: '';
  position: absolute; inset: 7%;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.045);
  pointer-events: none;
}
.stock-badge {
  /* Hidden from players by request -- stock count is still tracked and
     broadcast by the server as normal, just not shown in the UI. */
  display: none;
}
/* The open card and the joker sit at the exact vertical midpoint of the
   oval -- which is precisely where the east and west chairs also sit. At a
   fixed 58px each plus an 18px gap they needed 134px of the table's middle,
   and on a 273px-wide table that left the side seats nowhere to go: 27px of
   overlap on each side, measured. Sizing them from the table's width keeps
   the open card and the chairs out of each other's way at every screen
   size, and resolves to the original 58px on a full-size table. */
.table-center {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; gap: var(--centre-gap, 18px); align-items: center; z-index: 2;
}
.table-center .pile-label { font-size: 11px; }
.table-center .card {
  width: var(--centre-card-w, 58px);
  /* 82/58 -- keeps the real playing-card proportion at any width. */
  height: calc(var(--centre-card-w, 58px) * 1.414);
}
.table-center .card .card-corner { display: none; }
/* Ratios taken from the original 58px card (20/58 and 26/58) so the rank
   and suit keep filling the same share of the face as it shrinks. */
.table-center .card .center-rank { font-size: calc(var(--centre-card-w, 58px) * 0.345); }
.table-center .card .center-suit { font-size: calc(var(--centre-card-w, 58px) * 0.448); }
#joker-indicator { display: flex; }

/* Width now comes from renderOvalTable(), derived from the table's own
   measured width (~26% of it, floored at 62px, capped at the old 96px).
   The fallback is the previous fixed value, so if the JS ever fails to run
   the seats simply look exactly as they did before rather than collapsing. */
.seat {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; z-index: 3; width: var(--seat-w, 96px); text-align: center;
}
.seat-chip {
  /* Outlined rather than a flat wash (Oct 2026). With the avatar gone, eight
     identical translucent rectangles read as a list, not as eight chairs. The
     outline and the shadow give each one an edge, so it sits ON the felt
     instead of being a lighter patch OF it. Costs 2px of width. */
  background: rgba(255,255,255,0.10); border-radius: var(--radius-sm); padding: 5px 7px;
  border: 1px solid rgba(255,255,255,0.20);
  box-shadow: 0 2px 6px rgba(0,0,0,0.35);
  font-size: 11.5px; text-align: center;
  max-width: 100%; width: 100%;
  position: relative;
}
.seat.active .seat-chip { border-color: rgba(255,255,255,0.55); }

/* The player's colour + initial. Replaces the avatar: same job -- tell eight
   chips apart instantly -- for 15px instead of 22px and no network request.
   "Bot 3" shows 3 rather than B, since eight B's would tell you nothing. */
.seat-dot {
  flex: 0 0 auto;
  width: 15px; height: 15px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 700; line-height: 1;
  color: #0d1a16;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.3);
}

/* ---- Turn countdown ring ------------------------------------------------
   The server already runs a real per-turn clock (TURN_SECONDS for people, a
   shorter per-character delay for bots) and already broadcasts turnDeadline,
   so this is the existing clock drawn in a second place -- on the chair whose
   turn it is -- rather than a new timer. --turn-pct is written by the tick in
   updateTurnTimerDisplay().

   Drawn as a masked conic gradient so it is a RING, not a filled wedge. The
   @supports guard matters: without mask-composite the ::after would paint a
   solid gold rectangle over the whole chip, so a WebView too old for it gets
   no ring at all rather than a broken one. */
@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
  .seat.active .seat-chip::after {
    content: ''; position: absolute; inset: -2px;
    border-radius: calc(var(--radius-sm) + 2px);
    padding: 2px;
    background: conic-gradient(from -90deg,
      var(--turn-ring, var(--gold-bright)) calc(var(--turn-pct, 100) * 1%),
      rgba(0,0,0,0.30) 0);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
    pointer-events: none;
  }
}

/* ---- Seat identity + hand (Sept 2026) ----------------------------------
   A seat used to be two lines of text. Now it's a face, a name, and a
   visible hand -- the three things a player at a real table can see. */
.seat-id {
  /* STACKED, not side-by-side (Oct 2026).
     Side by side, the 22px avatar plus its 5px gap ate 27px of the chip's
     82px of usable width, leaving 55px for the name -- about six characters
     at 11.5px, so anything longer than "Manika" was already an ellipsis.
     Stacking gives the name the full 82px, roughly thirteen characters, for
     no extra width at all. It costs ~14px of seat height, which is the
     trade: at 96px wide there is no other way to find that space.
     It also fixes the alignment: the name was text-align:left inside a
     centre-aligned column, so it never lined up with anything above or
     below it. */
  /* Back to a ROW (Oct 2026). The avatar it used to stack above is gone,
     so the only two things on this line are the name and the score, and
     side by side they read as one label: "Ravi 25". Stacking them would
     cost a whole extra line of chip height to say the same thing. */
  display: flex; flex-direction: row; align-items: center;
  justify-content: center; gap: 4px;
  min-width: 0; width: 100%;
}
.seat-name {
  flex: 0 1 auto;
  max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center;
  font-weight: 600;
}
/* The score used to be grey text at the tail of "10 cards . 0 pts" -- the
   number you check most often, in the least visible place in the chip.
   Now it is gold and bold, right after the name. */
.seat-score {
  flex: 0 0 auto;
  font-size: 11.5px; font-weight: 800; line-height: 1.1;
  color: var(--gold-bright);
}
/* The separator. In the markup rather than the text so that :empty below
   still collapses the score cleanly during the deal and the reveal -- a
   dash left hanging after the name would be worse than no score at all. */
.seat-score::before {
  content: '\2013';
  margin-right: 3px;
  color: var(--text-muted); font-weight: 400;
}
.seat-score:empty { display: none; }
/* The active seat's chip turns gold with dark text, so gold-on-gold would
   vanish. Same flip the rest of the chip already does. */
.seat.active .seat-score { color: #1a1a1a; }
.seat.active .seat-score::before { color: rgba(0,0,0,0.45); }

/* Artwork loaded from an avatar pack (public/avatars/). Fills the circle
   and crops, so a portrait that isn't perfectly square still sits right.
   image-rendering: pixelated matters for pixel art specifically -- without
   it the browser smooths the upscale and the crisp pixel edges turn to
   mush, which is exactly what makes pixel art look cheap. It's harmless
   for non-pixel artwork at these sizes. */


/* Three overlapping backs. Each is nudged by its index via --i, so the
   markup stays a flat list and the overlap is one line of CSS. Negative
   margin rather than absolute positioning keeps the fan in flow, so the
   chip still sizes itself correctly on a 71px seat. */
.seat-fan {
  display: flex; align-items: center; justify-content: center;
  gap: 0; margin: 4px 0 2px;
}
.seat-back {
  width: 13px; height: 18px; border-radius: 2px;
  background:
    repeating-linear-gradient(45deg, #8d2733 0 3px, #6f1d28 3px 6px);
  border: 1px solid rgba(255,255,255,0.35);
  box-shadow: 0 1px 2px rgba(0,0,0,0.4);
  margin-left: calc(var(--i) * -5px);
  transform: rotate(calc((var(--i) - 1) * 7deg));
}
.seat-fan-count {
  font-size: 10.5px; font-weight: 700;
  color: var(--text-light);
}
/* Fan on top, count underneath. Beside the backs the number read as a
   separate fact; below them it reads as a label on the hand. */
.seat-hand {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  flex: 0 0 auto; min-width: 0;
}
.seat-meta:empty { display: none; }

/* ---- Avatar picker (profile menu) ---- */



.seat.active .seat-chip { background: var(--gold); color: #1a1a1a; font-weight: 700; }
/* "This is my seat" gets a plain neutral ring, deliberately NOT gold --
   gold is reserved for "whose turn is it" (the fill above, plus the pulse
   animation further down), both of which can already be active on this
   exact element at the same time as this ring. A third gold treatment
   stacked on top would just look muddy instead of adding information. */
.seat.own-seat .seat-chip { box-shadow: 0 0 0 2px rgba(255,255,255,0.55); }
.seat.eliminated { opacity: 0.4; }
.seat.quit { opacity: 0.35; }
.seat.tappable { cursor: pointer; }

/* ---------------- player action popover (report / mute) ----------------
   Replaces the old 🚩/🔇 icons that used to sit on every chat message.
   Tap a player's seat (in-game) or their row in the lobby list (pre-game)
   and this appears anchored to that element -- both .seat and .player-list
   li are already a positioning context, so it's just appended as a child
   and placed with plain CSS. Tap anywhere else on screen to dismiss it. */
.player-action-popover {
  position: absolute; z-index: 25;
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border); border-radius: var(--radius-sm); padding: 6px;
  display: flex; flex-direction: column; gap: 4px;
  box-shadow: 0 6px 18px rgba(0,0,0,0.4);
  animation: popoverFadeIn 0.12s ease-out both;
}
@keyframes popoverFadeIn { from { opacity: 0; } to { opacity: 1; } }
.seat .player-action-popover {
  bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px;
}
/* Seats at the top of the table open their popover DOWNWARDS.
   The rule above hard-codes bottom:100%, so the popover always grew upwards --
   fine for the seats around the sides and bottom, but for the seat at the top
   of the oval "upwards" is off the top of the screen, behind the Scores/Mute
   bar. Tapping the top player to see their stats showed nothing at all.
   .anchor-top is already applied by renderOvalTable to any seat sitting within
   30px of the table's top edge, so the signal existed -- it just was not being
   used for this. Same approach as .seat.revealing.grow-up, which solved the
   identical problem for the round-reveal box. */
.seat.anchor-top .player-action-popover {
  bottom: auto; top: 100%;
  margin-bottom: 0; margin-top: 8px;
}
.seat .player-action-popover.align-left { left: 0; transform: none; }
.seat .player-action-popover.align-right { left: auto; right: 0; transform: none; }
.player-list li .player-action-popover {
  top: 100%; right: 0; margin-top: 2px;
}
.player-action-btn {
  background: var(--jade-panel); color: var(--text-light); border: none; border-radius: var(--radius-sm);
  padding: 8px 12px; font-size: 12.5px; white-space: nowrap;
  justify-content: flex-start; gap: 6px; cursor: pointer;
}
.player-action-btn:hover { background: var(--jade-bg); }
.player-action-btn:disabled { opacity: 0.6; cursor: default; }
.player-action-btn.report { color: var(--red-text); }
.player-action-btn.mute { color: var(--gold-bright); }
.player-action-stats {
  color: var(--text-muted); font-size: 11.5px; padding: 4px 8px 6px; white-space: nowrap;
  border-bottom: 1px solid rgba(255,255,255,0.12); margin-bottom: 2px;
}

/* Your-turn visual pulse: only ever applied to your OWN seat, and only once
   you've gone quiet for 5+ seconds on your own turn (see app.js) -- a purely
   local, self-only nudge, no vibration, no broadcast to anyone else. */
@keyframes turnPulseRing {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212,160,23,0.65); }
  50% { box-shadow: 0 0 0 12px rgba(212,160,23,0); }
}
.seat.my-turn-pulse .seat-chip { animation: turnPulseRing 1.1s ease-in-out infinite; }

/* Chat speech-bubble at the sender's seat -- shown alongside (not instead
   of) the separate chat panel, for 5-10s scaled by message length.
   A comic-style bubble with a tail pointing straight at the seat, sitting
   tight against it so it visually reads as "this came from their chair".
   Width is a fixed value (not shrink-to-fit) -- an auto/max-width-only box
   combined with word-break:break-word was computing down to a near-zero
   width on some phones, wrapping text to 2-3 letters per line. A fixed
   width sidesteps that shrink-to-fit ambiguity entirely. */
.seat-chat-bubble {
  position: absolute; left: 50%; transform: translateX(-50%);
  width: min(150px, 56vw); box-sizing: border-box;
  background: #fff; color: #1a1a1a; font-size: 12px; line-height: 1.35;
  padding: 7px 10px; border-radius: 14px; box-shadow: 0 3px 10px rgba(0,0,0,0.4);
  z-index: 6; word-break: break-word; animation: chatBubbleIn 0.18s ease-out both;
}
.seat-chat-bubble.below { top: calc(100% + 4px); }
.seat-chat-bubble.above { bottom: calc(100% + 4px); }
.seat-chat-bubble::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  width: 0; height: 0; border-left: 7px solid transparent; border-right: 7px solid transparent;
}
.seat-chat-bubble.below::after { top: -7px; border-bottom: 8px solid #fff; }
.seat-chat-bubble.above::after { bottom: -7px; border-top: 8px solid #fff; }
.seat-chat-gif { display: block; width: 100%; border-radius: 8px; }

/* Text bubbles near a table edge could in principle also run wide, but at
   150px max they've been fine in practice -- these align classes exist
   specifically for the GIF thumbnail below, reusing the same left/right/
   center pattern as the per-seat discard history and old final-hand reveal. */
.seat-chat-bubble.align-left { left: 0; transform: none; }
.seat-chat-bubble.align-right { left: auto; right: 0; transform: none; }
.seat-chat-bubble.align-left::after { left: 14px; transform: none; }
.seat-chat-bubble.align-right::after { left: auto; right: 14px; transform: none; }

/* GIF thumbnail on the table -- much smaller than a full chat bubble (which
   was tall enough to cover the joker/open card in the middle of the table).
   Just enough to notice someone sent a GIF; the chat panel shows it full size. */
.seat-chat-bubble.gif-thumb {
  width: 62px; padding: 0; border-radius: 8px; overflow: hidden;
  box-shadow: 0 3px 8px rgba(0,0,0,0.4); line-height: 0;
}
.seat-chat-bubble.gif-thumb .seat-chat-gif { width: 62px; height: 62px; object-fit: cover; border-radius: 0; }
.seat-chat-gif-tag {
  position: absolute; bottom: 2px; right: 2px; background: rgba(0,0,0,0.6); color: #fff;
  font-size: 7px; font-weight: 700; padding: 1px 3px; border-radius: 3px; line-height: 1.2;
}

/* Small card tile, reused anywhere a full-size 44x62 card would be too big:
   the round-result reveal rows, the per-seat discard history. */
.card.mini { width: 19px; height: 26px; }
.card.mini .card-corner { display: none; }
.card.mini .center-rank { font-size: 7.5px; }
.card.mini .center-suit { font-size: 7.5px; }
.card.mini .card-count-badge { width: 13px; height: 13px; font-size: 7px; top: -5px; right: -5px; }
@keyframes chatBubbleIn {
  from { transform: translateX(-50%) scale(0.7); opacity: 0; }
  to { transform: translateX(-50%) scale(1); opacity: 1; }
}
.seat-emoji-bubble {
  /* Centered squarely on the seat chip itself (covering the name, as
     intended) rather than floating above it -- staying within the seat's
     own footprint avoids any risk of the popup being clipped by the
     screen's overflow:hidden on cramped mobile layouts, especially for
     seats near the top of the table. */
  position: absolute; top: 50%; left: 50%; width: 44px; height: 44px; border-radius: 50%;
  background: #1a4a2c; border: 2px solid var(--gold); display: flex; align-items: center; justify-content: center;
  font-size: 28px; z-index: 5; animation: seatReactionPop 3.4s ease both;
}
@keyframes seatReactionPop {
  0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
  10% { transform: translate(-50%, -50%) scale(1.3); opacity: 1; }
  18% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  85% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(0.85); opacity: 0; }
}
/* .seat-name was defined twice -- here and up at the .seat-id block. The two
   disagreed on alignment and on display, and whichever came last silently won.
   Folded into the single definition above; this note is all that remains. */
.seat-meta { font-size: 10px; opacity: 0.85; }
/* Cards and points on one line (Oct 2026). They used to be two stacked rows
   -- a fan of card backs with its count, then "0 pts" underneath -- which in a
   96px box was two mostly-empty lines and a large part of why the seat looked
   oversized for what it actually said. */
.seat-stats {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  min-width: 0; max-width: 100%;
}
/* Your hand and your discards are two different facts; butted together at a
   4px gap they read as one run of cards. A hairline between them separates
   the two without costing a pixel of width the way a label would. */
.seat-stats .seat-hand + .seat-meta + .seat-discard-history::before,
.seat-stats .seat-hand ~ .seat-discard-history::before {
  content: ''; align-self: stretch;
  width: 1px; margin-right: 5px;
  background: rgba(255,255,255,0.16);
}
.seat.active .seat-stats .seat-hand ~ .seat-discard-history::before {
  background: rgba(0,0,0,0.22);
}
.seat-stats:empty { display: none; }
/* The fan sat in its own block with vertical margins when it was a row of its
   own; inside the shared row those margins would push the line off centre. */
.seat-stats .seat-fan { margin: 0; }

/* Reserved space for the 320x100 native ad on the round-result screen. The
   ad floats over the WebView and contributes no layout height of its own, so
   without this box the score rows would render underneath it. Height matches
   the ad exactly; app.js measures this element's real position each time and
   tells the native side where to draw, since the panel's height shifts with
   the player count. */
.round-result-ad { height: 100px; width: 100%; max-width: 320px; margin: 8px auto; }
.round-result-ad.hidden { display: none; }

/* Compact one-line score rows. These used to carry each player's card tiles,
   which made every row ~48px and, at six players, pushed Next Round below
   the fold once the ad slot was added. The cards now live at the chairs
   during the reveal phase, so a row only needs the name, the hand value and
   the running-total sum -- which fits on one line at ~34px. */
#round-result-hands .reveal-row { padding: 5px 8px; margin-bottom: 4px; }
#round-result-hands .reveal-name { font-size: 13px; }
#round-result-hands .reveal-main {
  display: flex; align-items: center; justify-content: space-between; margin-top: 0;
}
#round-result-hands .reveal-value { font-size: 12px; }
#round-result-hands .reveal-math { font-size: 12px; }
/* The celebratory emoji is the cheapest thing on this panel to give up, and
   the headline already says whether the declare was right or wrong. */
#round-result-emoji { display: none; }
#round-result-title { font-size: 17px; line-height: 1.25; margin: 0 0 4px; }

/* ---- Round-end reveal: each player's remaining cards, at their own chair.
   Shown for a few seconds after a round ends, before the scorecard (see
   startRevealPhase in app.js).

   The box hangs off its own seat and is never dragged toward the middle.
   Hanging it INWARD was tried first and works fine at 4 players, but at 6 it
   collapses -- the top, upper-left and upper-right boxes all land on top of
   each other, since six chairs around an oval leave no inward room. Sitting
   under the chip (over it, for bottom seats) survives a full 6-player table
   with zero overlaps, which is why it's built this way.

   Every number below is bounded by one hard constraint, measured on a real
   6-player table at 375px wide: the upper-right and lower-right seats leave
   ~150px of vertical room between them for TWO boxes, so a box may not
   exceed ~73px tall. That rules out a 2-column grid -- six slots in two
   columns is three rows, which overflows and makes those two boxes collide.
   Three columns is two rows and comes in at 71px, and at 70px wide it still
   sits well inside the 96px seat, so it can't clip at the screen edge the
   way a wider box does on the left/right seats.

   The type is then pushed as large as that 71px allows: at the first
   attempt (18x22 cards at the stock .card.mini 7.5px) the faces were
   unreadable -- blank white tiles where only the ×N badges showed, which
   defeats the entire point of showing the cards. 20x24 with a 10px bold
   rank is the largest that fits, and the badge is shrunk to 10px so it
   stops covering the rank it's annotating. ---- */
/* Sept 2026: this is now a FLOW CHILD of .seat-chip, not an absolutely
   positioned overlay. It sits under the player's name, so it can never
   drift onto a neighbouring seat and never needs to be told which
   direction to open. It inherits the chip's avatar + name, which is what
   makes each panel self-identifying. */
.seat-reveal-box {
  position: relative; z-index: 1;
  width: 100%; box-sizing: border-box;
  margin-top: 3px; padding: 3px;
  background: rgba(6,26,20,0.94);
  border: 1px solid var(--jade-border); border-radius: var(--radius-sm);
}
/* Severity: a scratch and a quarter of the way to elimination should not
   look the same. Applied to the panel edge, not the number, so it reads
   from across the table at a glance. */
.seat-reveal-box.sev-none { border-color: rgba(74,222,128,0.55); }
.seat-reveal-box.sev-low  { border-color: rgba(240,198,116,0.45); }
.seat-reveal-box.sev-mid  { border-color: rgba(240,150,90,0.65); }
.seat-reveal-box.sev-high { border-color: rgba(255,107,107,0.75); }
.seat-reveal-box.sev-mid  .seat-reveal-score { color: var(--gold-bright); }
.seat-reveal-box.sev-high .seat-reveal-score { color: var(--red-text); }
/* Wrap rather than truncate -- the grid gets taller, never wider. */
.seat-reveal-grid { display: flex; flex-wrap: wrap; gap: 2px; justify-content: center; }
/* The seat has to be allowed to grow downward now that it carries cards. */
.seat.revealing { z-index: 8; }
.seat.revealing .seat-chip { padding-bottom: 4px; }
/* Anchor by edge, not centre, while a panel is open. A centred seat grows
   in both directions, which sent the top seat's panel up into the round
   number in the header. These keep growth pointed inward. */
.seat.revealing.grow-up   { transform: translate(-50%, -100%); }

/* Applies in EVERY phase, not just the reveal. A top-of-table seat carries
   four rows during ordinary play (name, cards, points, last discard) and,
   centred on its point, its top edge reaches the round number in the
   header. Anchoring the top edge means the seat can only ever grow
   downward, toward the middle of the table. */
.seat.anchor-top { transform: translate(-50%, 0); }
.seat-reveal-box.declared { border: 2px solid var(--gold); }
.seat-reveal-tag {
  font-size: 7px; color: var(--gold-bright); font-weight: 700; letter-spacing: 0.04em;
}
.seat-reveal-value {
  font-size: 12px; font-weight: 600; color: var(--gold-bright); line-height: 1.1;
}
/* justify-items keeps each card at its own natural mini size rather than
   stretching it to the column -- a card stretched to fill a 1fr column comes
   out wider than it is tall, which stops reading as a playing card at all. */
.seat-reveal-grid {
  /* FOUR columns, not three. The cap is three cards plus the "+N more" chip,
     which is four items -- at three columns the chip wrapped onto a row of its
     own, so the panel paid a whole extra row of height to say "+4 more". */
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 2px; margin-top: 2px;
  justify-items: center;
}
.seat-reveal-grid .card.mini { width: 20px; height: 24px; }

/* ---- Crowded reveal (Oct 2026) -----------------------------------------
   Set on the table whenever there are 6+ players. Measured at six players the
   reveal panels overlapped their neighbours by 94px -- the panel was ~135px
   tall and the vertical gap between chairs is about 60px. Three things get it
   under budget, in order of how much they buy:

     - ONE ROW. Three cards plus the "+N more" chip across four columns
       instead of wrapping to two or three rows. Worth ~50px.
     - NO TAG LINE. "DECLARED / LOWEST" is dropped; the panel border already
       says it in gold. Worth ~12px (set in app.js, not here).
     - TIGHTER EVERYTHING. Smaller cards, less padding, no margin above.

   Below six players none of this applies and the panel is unchanged -- there
   is room, and the fuller panel is better when there is. */
.oval-table.compact-reveal .seat-reveal-grid { gap: 1px; margin-top: 1px; }
.oval-table.compact-reveal .seat-reveal-grid .card.mini {
  width:  calc(19px * var(--seat-scale));
  height: calc(23px * var(--seat-scale));
}
.oval-table.compact-reveal .seat-reveal-grid .card.mini .center-rank { font-size: calc(9px * var(--seat-scale)); }
.oval-table.compact-reveal .seat-reveal-grid .card.mini .center-suit { font-size: calc(7px * var(--seat-scale)); }
.oval-table.compact-reveal .seat-reveal-more {
  width:  calc(19px * var(--seat-scale));
  height: calc(23px * var(--seat-scale));
  font-size: calc(6px * var(--seat-scale)); line-height: 1;
}
.oval-table.compact-reveal .seat-reveal-box { margin-top: 2px; padding: 2px; }
.oval-table.compact-reveal .seat-reveal-value { font-size: calc(11px * var(--seat-scale)); }
.oval-table.compact-reveal .seat-reveal-score { font-size: calc(12px * var(--seat-scale)); }
.oval-table.compact-reveal .seat-reveal-total { font-size: calc(11px * var(--seat-scale)); }
.oval-table.compact-reveal .seat-reveal-arrow { margin: 0 2px; font-size: calc(10px * var(--seat-scale)); }
/* Bigger than .card.mini's stock 7.5px -- at this size the rank is the whole
   point (it's what tells you the card's value), so it gets the pixels. */
.seat-reveal-grid .card.mini .center-rank { font-size: 10px; font-weight: 700; }
.seat-reveal-grid .card.mini .center-suit { font-size: 8px; }
/* Stock mini badge is 13px on a 20px card -- it sat squarely on top of the
   rank underneath it. Shrunk so it annotates the card instead of hiding it. */
.seat-reveal-grid .card.mini .card-count-badge {
  width: 10px; height: 10px; font-size: 6px; top: -3px; right: -3px;
}
.seat-reveal-more {
  width: 20px; height: 24px;
  background: rgba(255,255,255,0.10); border: 1px dashed rgba(255,255,255,0.35);
  border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  font-size: 6.5px; color: var(--text-muted); text-align: center; line-height: 1.05;
}
/* For the duration of the reveal the open card, joker and stock count carry
   no information -- the round is already decided -- so they're hidden, which
   is exactly what frees the middle of the oval for the boxes to sit in. */
#screen-game.reveal-phase .table-center,
#screen-game.reveal-phase .stock-badge { display: none; }

/* Per-seat recent-discards line -- lives inside the chip itself so it never
   needs its own edge-aware positioning (see renderOvalTable()). */
.seat-discard-history {
  display: flex; align-items: center; justify-content: center; gap: 2px;
  white-space: normal; margin-top: 0;
  flex: 0 1 auto; min-width: 0; overflow: hidden;
}
.seat-discard-label { font-size: 8px; color: var(--gold-bright); font-weight: 700; margin-right: 1px; }
.mini-card-back {
  width: 26px; height: 36px; border-radius: 4px;
  background: repeating-linear-gradient(45deg, #234, #234 4px, #1a2f42 4px, #1a2f42 8px);
  border: 1px solid #0b1a26; margin-bottom: 2px;
}

.pile-group { text-align: center; }
.pile-label { font-size: 11px; color: #cde; margin-bottom: 4px; }

/* ---- Realistic playing cards ---- */
.card {
  width: 44px; height: 62px; border-radius: 6px; background: white;
  position: relative; user-select: none; flex-shrink: 0;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3); border: 2px solid transparent;
  display: inline-block;
}
.card.red .corner-rank, .card.red .corner-suit, .card.red .center-suit, .card.red .center-rank { color: #c0392b; }
.card.black .corner-rank, .card.black .corner-suit, .card.black .center-suit, .card.black .center-rank { color: #1a1a1a; }

.card-corner {
  position: absolute; display: flex; flex-direction: column; align-items: center;
  line-height: 1; font-weight: 700;
}
.corner-tl { top: 3px; left: 3px; }
.corner-br { bottom: 3px; right: 3px; transform: rotate(180deg); }
.corner-rank { font-size: 10px; }
.corner-suit { font-size: 8px; margin-top: 1px; }

.card-center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
.card-center .center-rank { font-size: 14px; font-weight: 800; line-height: 1; }
.card-center .center-suit { font-size: 18px; line-height: 1; margin-top: 2px; }

.card-count-badge {
  position: absolute; top: -6px; right: -6px; background: #c0392b; color: #fff;
  border-radius: 50%; font-size: 9px; font-weight: 700; width: 17px; height: 17px;
  display: flex; align-items: center; justify-content: center; z-index: 2;
}

.card.joker { color: var(--card-red-ink); }
.joker-letters {
  font-size: 7px; font-weight: 700; line-height: 1.15; color: var(--card-red-ink);
  letter-spacing: -0.5px;
}
.joker-cap {
  width: 22px; height: 22px; position: relative;
}
.joker-cap::before {
  content: ''; position: absolute; inset: 0;
  background: var(--card-red-ink);
  clip-path: polygon(50% 0%, 20% 45%, 35% 40%, 50% 70%, 65% 40%, 80% 45%);
}
.draw-reveal-cards .card.joker .joker-cap, .table-center .card.joker .joker-cap { width: 30px; height: 30px; }

.card.back {
  background: repeating-linear-gradient(45deg, #234, #234 6px, #1a2f42 6px, #1a2f42 12px);
  border: 2px solid #0b1a26;
}
.card.selected { border-color: var(--gold); transform: translateY(-8px); }
.card.selectable { cursor: pointer; }

.chain-banner {
  background: #c0392b22; border: 1px solid #c0392b; border-radius: 10px;
  padding: 6px 10px; text-align: center; font-size: 12px; margin-bottom: 4px;
  flex-shrink: 0;
}
.chain-banner.hidden { display: none; }
.chain-banner button.hidden { display: none; }

/* Brief flash announcing a 2 was just played/escalated/resolved -- visible
   to every player at the table, not just whoever's turn it is. */
.chain-flash {
  flex-shrink: 0; text-align: center; font-size: 13px; font-weight: 700;
  background: linear-gradient(90deg, #c0392b, var(--gold)); color: #fff;
  border-radius: 20px; padding: 6px 14px; margin: 0 auto 4px; width: fit-content;
  max-width: 92%; animation: chainPop 0.25s ease-out;
}
.chain-flash.hidden { display: none; }
@keyframes chainPop {
  from { transform: scale(0.85); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* Everything below the table shares one centered max-width column with it
   on wide screens (a phone is already narrower than this cap, so nothing
   changes there) -- otherwise on a desktop/laptop the hand of cards and the
   action buttons stretch edge-to-edge while the table stays centered above
   them, leaving the cards looking left-aligned and disconnected from the
   table instead of sitting under it. */
.hand-header, .hand, .action-bar, .hint-banner {
  max-width: 640px; width: 100%; margin-left: auto; margin-right: auto;
}

/* Joker/wild and normal cards used to be two stacked sections, each with
   its own label line -- merged into one tray (#hand) so both groups flow
   into a single wrapping row instead. display:contents drops these two
   inner divs out of the box model entirely, so their card tiles become
   direct flex items of #hand and share its one gap/wrap behavior, while
   app.js can still target #hand-jokers/#hand-normal independently to
   render each group. */
.hand-jokers-cards, .hand-normal-cards { display: contents; }
/* Thin vertical rule between the two groups, only shown when jokers are
   present (see app.js). align-self:stretch makes it match the height of
   whichever row it's wrapped into, so it always reads as a proper divider
   rather than a short floating tick mark. */
.hand-divider {
  align-self: stretch; width: 1px; background: var(--jade-border);
  margin: 2px 1px; flex-shrink: 0;
}
.hand-divider.hidden { display: none; }
/* Cards worth 0 this round get a gold border instead of the old separate
   "Jokers / Wild -- only you see this" label line -- the border plus their
   position on the joker side of the divider is enough of a cue on its own,
   and the Rules screen covers the mechanic in full for anyone new. */
/* ---- Rejoin offer on the round-result screen (Sept 2026) ----------------
   A 30-second decision, so it gets a bordered panel rather than a line of
   text: it has to be findable the instant the scorecard appears, on a screen
   that already carries a podium, an ad slot and a hand reveal.
   Gold, not red -- this is an opportunity, not a warning. --------------- */
.rejoin-offer {
  border: 1px solid var(--gold);
  background: rgba(212,175,55,0.10);
  border-radius: var(--radius-md);
  padding: 12px;
  margin: 12px 0;
  text-align: center;
}
.rejoin-offer-text { margin: 0 0 10px; font-size: 14px; color: var(--text-light); }
.rejoin-offer-actions { display: flex; gap: 8px; }
.rejoin-offer-actions button { flex: 1; margin: 0; min-height: 44px; }
.rejoin-offer-status { margin: 8px 0 0; font-size: 12.5px; color: var(--text-muted); }
.rejoin-offer-status.hidden, .rejoin-offer-actions button.hidden { display: none; }

/* ---- Cards in flight (Sept 2026) --------------------------------------
   A fixed, full-viewport layer because a discard crosses from the hand tray
   to the middle of the oval, and those sit in different stacking contexts.
   z-index sits above the table and below the overlays, so a flying card is
   never drawn over the round-result or chat sheets. */
.fly-layer {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 9;
}
.flying-card {
  position: absolute;
  pointer-events: none;
  margin: 0;
  will-change: left, top;
  box-shadow: 0 6px 16px rgba(0,0,0,0.45);
}

/* ---- Press feedback -----------------------------------------------------
   The whole app had six :active rules, which is why every tap felt like
   submitting a form rather than handling a card. These are the two controls
   touched most, so they're where the difference is felt.
   transform only -- no layout properties -- so nothing reflows on touch. */
.card.selectable {
  transition: transform 90ms ease-out, box-shadow 90ms ease-out;
}
.card.selectable:active {
  transform: translateY(-4px) scale(1.03);
  box-shadow: 0 8px 18px rgba(0,0,0,0.45);
}
.action-bar button { transition: transform 80ms ease-out, filter 80ms ease-out; }
.action-bar button:active:not(:disabled) { transform: scale(0.96); filter: brightness(1.12); }
@media (prefers-reduced-motion: reduce) {
  .card.selectable, .card.selectable:active,
  .action-bar button, .action-bar button:active:not(:disabled) {
    transition: none; transform: none;
  }
}

/* ---- Settings row + toggle (profile menu) ---- */
.settings-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 14px 0 4px;
  padding-top: 12px; border-top: 1px solid var(--jade-border);
  text-align: left;
}
.settings-label { display: block; font-size: 14px; color: var(--text-light); }
.settings-hint { display: block; font-size: 11.5px; color: var(--text-muted); margin-top: 1px; }
/* ---- Switch -------------------------------------------------------------
   Replaces a pill button that displayed the word "On".
   THE BUG IT FIXES: the global `button { width: 100% }` rule applies to
   every button in the app. The old .toggle-pill set min-width but never
   width, so it inherited 100% and ran off the right edge of the settings
   row. `width: auto` below is load-bearing, not decoration -- any future
   control in a flex row needs the same.
   Sized 52x30 with a 24px knob: comfortably over the 44px minimum touch
   target once the row's padding is counted, and unmistakably a switch
   rather than a button you have to read. */
.switch {
  flex: 0 0 auto;
  width: 52px; min-width: 0; height: 30px; padding: 0; margin: 0;
  border-radius: 15px; border: 1px solid var(--jade-border);
  background: rgba(255,255,255,0.10);
  display: flex; align-items: center; justify-content: flex-start;
  transition: background 0.18s ease, border-color 0.18s ease;
  cursor: pointer;
}
.switch-knob {
  width: 24px; height: 24px; margin-left: 2px;
  border-radius: 50%; background: #cfd8d4;
  box-shadow: 0 1px 3px rgba(0,0,0,0.45);
  transition: transform 0.18s ease, background 0.18s ease;
}
.switch.on { background: rgba(212,175,55,0.30); border-color: var(--gold); }
.switch.on .switch-knob { transform: translateX(21px); background: var(--gold); }
.switch:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Someone with reduced motion still needs to see the state change -- the
   knob just arrives rather than sliding. */
@media (prefers-reduced-motion: reduce) {
  .switch, .switch-knob { transition: none; }
}

.card.wild-zero { border-color: var(--gold); }

/* ---- Jokers in hand (Sept 2026) ------------------------------------------
   Covers BOTH kinds -- the printed Joker and this round's wild rank. They
   score the same (zero) and the engine now refuses to discard either, so
   they need to read as one category. Before this, only the wild rank got a
   gold border and a printed Joker was visually just another card, which is
   why they were easy to throw away by mistake.
   Gold is already this app's "this is special" colour (round joker slot,
   active seat), so it carries the meaning without a new convention. The
   0 badge states the value outright rather than expecting players to
   remember it. ------------------------------------------------------- */
.card.is-joker {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px rgba(212,175,55,0.45), 0 2px 6px rgba(0,0,0,0.3);
}
.card.is-joker::after {
  content: '0';
  position: absolute; top: -5px; left: -5px;
  min-width: 16px; height: 16px; padding: 0 3px;
  border-radius: 50%;
  background: var(--gold); color: #1a1a1a;
  font-size: 10px; font-weight: 700; line-height: 16px;
  text-align: center;
  box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
/* Not selectable, and it should look it: no lift, no pointer, slightly
   receded so the eye goes to the cards you CAN play. Deliberately not
   greyed out -- these are your best cards, not disabled junk. */
.card.is-joker:not(.selectable) { cursor: default; opacity: 0.92; }

.hand-header {
  font-size: 12px; margin-top: 2px; margin-bottom: 2px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}

/* ---- Bots-only "Help me play" assist: one button, tap any turn to reveal
   a suggestion (never auto-plays for you). ---- */
.hint-btn {
  width: auto; background: rgba(212,160,23,0.18); border: 1px solid var(--gold);
  color: var(--gold-bright); font-size: 11px; padding: 4px 10px; border-radius: 14px;
}
@keyframes hintBtnPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212,160,23,0.6); }
  50% { box-shadow: 0 0 0 8px rgba(212,160,23,0); }
}
.hint-btn.attn-pulse { animation: hintBtnPulse 1.6s ease-in-out infinite; }
.hint-banner {
  background: rgba(212,160,23,0.14); border: 1px solid var(--gold); color: var(--gold-bright);
  border-radius: 10px; padding: 8px 12px; margin-top: 0; margin-bottom: 6px;
  font-size: 12.5px; line-height: 1.4; flex-shrink: 0;
}
.card.hint-suggested {
  box-shadow: 0 0 0 3px var(--gold), var(--shadow-sm); transform: translateY(-4px);
}
#btn-declare.hint-suggested {
  box-shadow: 0 0 0 3px var(--gold);
}
.hand {
  display: flex; flex-wrap: wrap; align-content: flex-start; gap: 5px; padding: 4px;
  /* FIXED height, not max-height -- this used to be a cap that shrank
     whenever the hand had fewer cards/rows, which meant the oval table
     above (the sibling flex-grow element that absorbs whatever the hand
     row ISN'T using) visibly grew and shrank turn to turn as cards were
     drawn/discarded. A constant height here makes the table's leftover
     space constant too -- it only differs device to device now, never
     turn to turn on the same device. Rare huge hands (after several
     stacked +2 penalties) scroll within this fixed box instead of pushing
     the action buttons off-screen or growing the tray itself. */
  /* Still a FIXED height on any given device (the reasoning above holds --
     the table must not grow and shrink turn to turn), but that height is now
     a share of the viewport rather than 148px everywhere. At 148px flat, the
     tray plus the rest of the fixed chrome came to 288px, which on a 320x568
     phone left the oval just 214px -- smaller than the furniture around it.
     21svh gives the table 40px back there while resolving to the same 148px
     cap on anything 705px tall or more, so nothing changes on a normal
     phone. Floor of 84px keeps two rows of cards visible. */
  flex: 0 0 auto; height: clamp(84px, 21svh, 148px); overflow-y: auto;
}

/* ---- Fluid game chrome (Sept 2026) --------------------------------------
   The oval table was the only element on this screen sized from the
   viewport, so it absorbed every device difference alone: 513px tall on a
   412x915 phone, 214px on a 320x568 one, while the furniture around it
   stayed a constant 288px. These clamps let the chrome give a little on
   small screens (worth +40px of table, measured) and every one of them
   resolves to its existing value at 412x915 -- 11.5vw = 47px capped to 44,
   21svh = 192px capped to 148 -- so nothing moves on a normal phone.
   Scoped to #hand so the reveal/centre/start-sequence cards, which have
   their own deliberate sizes, are untouched. -------------------------- */
#hand .card {
  width: clamp(32px, 11.5vw, 44px);
  height: calc(clamp(32px, 11.5vw, 44px) * 1.409);   /* 62/44 */
}
.action-bar { display: flex; gap: 6px; padding: 2px; flex-shrink: 0; }
.action-bar button {
  padding: clamp(5px, 1.4svh, 10px);
  font-size: clamp(12px, 3.5vw, 14px);
}

.overlay {
  position: fixed; top: 0; left: 0; right: 0;
  /* Same banner safe-zone as .screen -- 0px whenever no banner is showing
     (e.g. an overlay like Rules opened from mid-game), so this only takes
     effect when it actually needs to. */
  bottom: var(--ad-safe-bottom, 0px);
  background: rgba(0,0,0,0.6);
  display: flex; align-items: center; justify-content: center; z-index: 10;
  padding: 16px;
}
.overlay.hidden { display: none; }

.hidden { display: none !important; }

/* ---- mid-game join approval: requester's waiting screen + host's banner ---- */
.waiting-host-panel { text-align: center; padding-top: 60px; }
.waiting-host-panel > .leave-btn { margin-top: 20px; }
.waiting-host-spinner {
  width: 44px; height: 44px; margin: 0 auto 20px; border-radius: 50%;
  border: 3px solid rgba(212,160,23,0.25); border-top-color: var(--gold);
  animation: waitingHostSpin 0.9s linear infinite;
}
@keyframes waitingHostSpin { to { transform: rotate(360deg); } }

/* Floating, host-only strip -- sits above whatever screen (lobby or the
   live game) the host happens to be on, since a join request can arrive at
   any time. Stacks vertically if more than one person is waiting at once. */
/* The fixed positioning moved up to this wrapper so the join and rejoin
   banners stack rather than landing on top of each other -- both are
   host-facing and can be pending at the same moment.
   pointer-events is off on the wrapper and back on for the banners
   themselves, so the container can span the width without swallowing taps
   meant for the top bar behind it. */
.top-banners {
  position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
  width: min(92vw, 420px); z-index: 60;
  display: flex; flex-direction: column; gap: 8px;
  pointer-events: none;
}
.top-banners > * { pointer-events: auto; }
.join-requests-banner {
  display: flex; flex-direction: column; gap: 8px;
}
/* Gold edge marks this as the rejoin decision, matching the offer the
   eliminated player is looking at -- so the host can tell at a glance which
   of the two questions they're answering. */
.rejoin-requests-banner .join-request-row {
  border: 1px solid var(--gold);
}
.join-requests-banner.hidden { display: none; }
.join-request-row {
  background: var(--jade-panel-alt); border: 1px solid rgba(212,160,23,0.4); border-radius: var(--radius-sm);
  padding: 10px 12px; display: flex; align-items: center; gap: 10px;
  box-shadow: 0 8px 20px rgba(0,0,0,0.5);
}
.join-request-row .jr-text { flex: 1; font-size: 13px; color: var(--text-light); }
.join-request-row .jr-text b { color: var(--gold-bright); }
.join-request-row button {
  width: auto; padding: 7px 12px; margin: 0; font-size: 12px; border-radius: var(--radius-sm);
}
/* Admitting someone is a positive action, so it uses the success role
   rather than its own one-off green. */
.join-request-row .jr-admit { background: var(--action-bg-hover); color: #fff; border: 1px solid var(--action-border); }
.join-request-row .jr-ignore { background: transparent; border: 1px solid rgba(255,255,255,0.3); color: var(--text-muted); }

.round-result-emoji { text-align: center; font-size: 48px; line-height: 1; margin-bottom: 4px; }
@keyframes scoreCardEmojiPop {
  0% { transform: scale(0); opacity: 0; }
  12% { transform: scale(1.25); opacity: 1; }
  22% { transform: scale(1); opacity: 1; }
  85% { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.85); opacity: 0; }
}
.result-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--jade-border); font-size: 14px; color: var(--text-light); }
.result-row.winner-row { font-weight: 700; color: var(--gold-bright); }

/* ---- Full scorecard (players in rows, rounds in columns + Total) ---- */
.link-btn {
  /* Was var(--red) -- the same colour as .error, so a benign "See full
     scorecard" read as a warning. Gold is already the app's highlight, and
     red is left to mean errors and destructive actions only. */
  background: none; border: none; color: var(--gold-bright); font-size: 12.5px;
  padding: 6px 0; width: 100%; text-decoration: underline;
}
.full-scorecard { margin: 4px 0 8px; }
.scorecard-hint { text-align: center; color: var(--text-muted); font-size: 11px; margin: 0 0 8px; }
.scorecard-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* One shared #scorecard-table living on its own dedicated overlay
   (#overlay-full-scorecard), opened from either the in-game Scores overlay
   or the round-result screen's own link -- so there's only ever one table
   to style (and keep in sync). */
#scorecard-table {
  border-collapse: collapse; font-size: 12.5px; white-space: nowrap; width: 100%;
  color: var(--text-light);
}
#scorecard-table th, #scorecard-table td { padding: 6px 8px; text-align: center; }
#scorecard-table th { color: var(--text-muted); font-weight: 600; border-bottom: 2px solid var(--jade-border); font-size: 11px; }
#scorecard-table th:first-child, #scorecard-table td:first-child {
  position: sticky; left: 0; background: var(--jade-panel-alt); text-align: left;
  color: var(--text-light); font-weight: 600; box-shadow: 2px 0 4px rgba(0,0,0,0.3);
}
#scorecard-table th:last-child, #scorecard-table td:last-child {
  font-weight: 700; border-left: 2px solid var(--jade-border);
}
#scorecard-table tr:not(:last-child) td { border-bottom: 1px solid var(--jade-border); }
.scorecard-out { color: var(--red-text); font-size: 9px; font-weight: 700; margin-left: 3px; }
.scorecard-dash { color: var(--text-muted); }

/* ---- Podium-style score card (shown every round) ---- */
.podium-panel {
  max-width: 480px; width: 92vw;
  margin: 8px auto; padding: 14px 16px;
  /* Safety net only -- the podium shrink above is what makes this fit on a
     normal phone screen without scrolling; this just prevents the panel
     from ever being clipped outright on an unusually short viewport. */
  max-height: 96dvh; overflow-y: auto;
}
.round-result-maxscore {
  text-align: center; font-size: 12px; color: var(--gold-bright); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px;
}
.round-result-note { text-align: center; font-size: 12.5px; color: var(--red-text); margin-bottom: 8px; }
.podium {
  display: flex; align-items: flex-end; justify-content: center; gap: 7px;
  margin: 4px 0 8px;
}
/* Shrunk down (not the reveal rows below) so the whole round-result screen
   fits on one mobile viewport without needing to scroll. */
.podium-slot {
  background: var(--jade-panel-alt); border: 1.5px solid var(--gold); border-radius: var(--radius-sm);
  padding: 5px 7px; text-align: center; flex: 1; min-width: 0;
}
.podium-medal { font-size: 17px; line-height: 1; }
.podium-name {
  font-weight: 700; font-size: 10.5px; margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.podium-score { font-size: 10px; color: var(--gold-bright); margin-top: 1px; font-weight: 700; }
.podium-delta { font-size: 9px; color: var(--gold-bright); margin-top: 1px; font-weight: 700; }
/* 1st place sits centered (order: 2, flanked by 2nd/3rd) and visibly higher
   than the other two -- via extra padding/scale + the other two carrying
   their own bottom margin, not a bar-chart height. */
.podium-place-1 {
  order: 2; padding-top: 8px; padding-bottom: 8px;
  background: rgba(212,160,23,0.16); border-width: 2px; transform: scale(1.03);
  box-shadow: 0 3px 10px rgba(212,160,23,0.35);
}
.podium-place-1 .podium-medal { font-size: 21px; }
.podium-place-2 { order: 1; margin-bottom: 4px; }
.podium-place-3 { order: 3; margin-bottom: 6px; }
/* Marks the current player's own podium slot without eating into the
   already-tight .podium-name width -- same idea as .seat.own-seat above,
   just a neutral ring around the whole card instead of appended text. */
.podium-slot.own-podium { box-shadow: 0 0 0 2px rgba(255,255,255,0.55); }
.podium-place-1.own-podium { box-shadow: 0 3px 10px rgba(212,160,23,0.35), 0 0 0 2px rgba(255,255,255,0.55); }

/* ---- Round-result reveal rows: replaces the old plain scoreboard table AND
   the old separate at-the-seat card reveal -- everyone's actual final hand
   for the round (priciest cards first, capped tiles + "+N"), its value, and
   the round-score math, all in one merged screen. See renderHandRevealRows(). ---- */
/* 34vh was sized for the old rows, which carried card tiles and ran ~48px
   each. The rows are ~41px now, and six of them need 276px -- exactly 34vh on
   a 812px phone, so the sixth player was landing right on the cap and getting
   clipped. 40vh clears a full 6-player table with the panel still inside its
   own max-height. Beyond six the list scrolls, which is the right thing to
   give up: the buttons underneath stay put either way. */
.reveal-rows { border-top: 1.5px solid var(--jade-border); padding-top: 6px; max-height: 40vh; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.reveal-row { background: var(--jade-panel-alt); border-radius: var(--radius-sm); padding: 7px 9px; }
.reveal-row.declared { background: rgba(212,160,23,0.14); border: 1px solid var(--gold); }
.reveal-row.eliminated { opacity: 0.55; }
.reveal-name { font-size: 11.5px; font-weight: 700; color: var(--text-light); margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reveal-main { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.reveal-cards { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; min-width: 0; }
.reveal-more { font-size: 10px; color: var(--gold-bright); font-weight: 700; margin-left: 1px; }
.reveal-value { font-size: 10.5px; color: var(--gold-bright); font-weight: 700; margin-left: 3px; white-space: nowrap; }
.reveal-math { font-size: 11px; color: var(--text-muted); white-space: nowrap; flex-shrink: 0; }
.reveal-math b { color: var(--gold-bright); font-size: 12px; }

.round-maxscore-row { margin: 14px 0; }
.round-maxscore-row.hidden { display: none; }
.round-maxscore-row label { color: var(--text-muted); font-size: 12px; }
.final-round-delta { font-weight: 700; color: var(--gold-bright); background: rgba(212,160,23,0.14); padding: 1px 6px; border-radius: 6px; }
.final-round-banner {
  background: rgba(212,160,23,0.14); border: 1.5px solid var(--gold); border-radius: var(--radius-sm);
  padding: 10px 14px; margin-bottom: 14px; text-align: center; font-size: 14px; color: var(--text-light);
}
.final-round-banner-label {
  text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; font-weight: 700;
  color: var(--gold-bright); margin-bottom: 4px;
}
.final-round-banner b { color: var(--gold-bright); }

/* ---- Install prompt (website only, after a finished game) ---- */
/* Sits at the foot of the game-over scorecard, so it reads as an aside to a
   result the player already cares about rather than an ad interrupting one.
   Deliberately NOT gold/red: those are the game's own accents, and this is
   not part of the game. */
.install-card {
  margin-top: 18px; padding: 14px;
  background: var(--jade-panel-alt);
  border: 1px solid var(--jade-border);
  border-radius: var(--radius-md);
  text-align: left;
}
.install-card-title {
  font-size: 15px; font-weight: 700; color: var(--text-light); margin-bottom: 4px;
}
.install-card-hint {
  font-size: 12.5px; line-height: 1.45; color: var(--text-muted); margin: 0 0 12px;
}
.install-card-actions { display: flex; gap: 10px; }
/* 44px floor, per the Sept 11 audit -- these are the newest buttons in the
   app and should not reintroduce the problem that audit just cleared. */
.install-card-btn {
  flex: 1; min-height: 44px; display: inline-flex; align-items: center;
  justify-content: center; font-size: 14px; text-decoration: none;
  border-radius: var(--radius-sm); box-sizing: border-box; margin: 0;
}
/* Install is an <a>, not a <button>, so a real tap opens the Play link even
   when a popup blocker would kill window.open -- and on Android that link has
   to be able to hand off to the Play app. But EVERY .primary rule in this
   stylesheet is scoped to `button.primary`, so the anchor inherits none of
   them and renders as a default blue underlined link. It needs its own
   styling; do not delete this on the assumption .primary covers it. */
a.install-card-btn,
a.install-card-btn:link,
a.install-card-btn:visited,
a.install-card-btn:hover {
  background: var(--gold); color: #1a1a1a;
  font-family: inherit; font-weight: 700; border: none;
  text-decoration: none;
}

/* ---- Drawn card reveal ---- */
.draw-reveal {
  position: fixed; inset: 0; background: rgba(0,0,0,0.55);
  display: flex; align-items: center; justify-content: center; z-index: 30;
  animation: fadeIn 0.15s ease-out;
}
.draw-reveal.hidden { display: none; }
.draw-reveal-box {
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25);
  border-radius: 20px; padding: 24px 28px; text-align: center;
  animation: popIn 0.25s ease-out; max-width: 92vw; box-sizing: border-box;
}
.draw-reveal-label { color: #fff; font-size: 14px; margin-bottom: 14px; font-weight: 600; }
/* A +4/+6 penalty draw needs more than one row -- wrapping (plus a width
   cap on the box above) keeps a big draw from overflowing the screen edge
   or getting crushed unreadably small trying to fit in one line. */
.draw-reveal-cards { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; max-width: 78vw; }
.draw-reveal-cards .card { width: 76px; height: 108px; }
.draw-reveal-cards .card-center .center-rank { font-size: 26px; }
.draw-reveal-cards .card-center .center-suit { font-size: 32px; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn { from { transform: scale(0.7); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* ---- Game-start sequence: countdown -> live deal -> joker/open reveal ---- */
.confetti-canvas {
  position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: 50; pointer-events: none;
}
.confetti-canvas.hidden { display: none; }

/* Countdown + "dealing" label -- a small floating, non-blocking badge (NOT
   a full-screen takeover) so the real oval table underneath stays visible
   while cards fly to the actual chair positions during dealing. */
.deal-phase-badge {
  /* Centred ON THE TABLE, not on the viewport (Oct 2026).
     Was: position: fixed; top: 12%. Twelve percent of a phone's viewport is
     roughly where the top seat sits, so "Dealing cards..." was printed across
     whoever was sitting there -- the one seat guaranteed to be covered.
     Now absolutely positioned inside .oval-table (which is position:relative)
     and centred with the same left/top/translate trio .reveal-banner uses for
     the round outcome. The deal announcement and the round announcement now
     appear in the same spot, which is what the player expects. */
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 7; text-align: center; pointer-events: none;
}
.deal-phase-badge.hidden { display: none; }
.deal-phase-badge .start-seq-countdown { font-size: 64px; }
.start-seq-countdown {
  font-weight: 700; color: var(--gold);
  text-shadow: 0 2px 12px rgba(0,0,0,0.6);
  animation: popIn 0.3s ease-out;
}
.start-seq-countdown.hidden { display: none; }
.deal-phase-label {
  color: #fff; font-size: 14px; font-weight: 600;
  background: rgba(9,38,22,0.6); border-radius: 20px; padding: 6px 16px;
  text-shadow: 0 2px 6px rgba(0,0,0,0.5); animation: fadeIn 0.2s ease-out;
}
.deal-phase-label.hidden { display: none; }

/* The flying "card" itself lives inside the real #oval-table and travels
   from the table's center to each actual seat in turn -- a real chair-to-
   chair deal, not an abstract side-panel animation. */
.deal-flyer {
  position: absolute; top: 50%; left: 50%; width: 24px; height: 34px; border-radius: 4px;
  background: repeating-linear-gradient(45deg, #234, #234 5px, #1a2f42 5px, #1a2f42 10px);
  border: 1px solid #0b1a26; box-shadow: 0 3px 8px rgba(0,0,0,0.45);
  transform: translate(-50%, -50%); opacity: 0; z-index: 6;
}
.deal-flyer.hidden { display: none; }

/* Pre-deal intro: N deck stacks appear (matching the real deck count for
   this table size) then riffle-merge into a shuffle, all before the
   existing per-card deal animation above starts. Same card-back look as
   .deal-flyer so it reads as the same physical deck throughout. */
.deck-intro {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
}
.deck-intro.hidden { display: none; }
.deck-stack {
  position: absolute; width: 40px; height: 56px; border-radius: 4px;
  background: repeating-linear-gradient(45deg, #234, #234 5px, #1a2f42 5px, #1a2f42 10px);
  border: 1px solid #0b1a26; box-shadow: 0 3px 8px rgba(0,0,0,0.45);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 9px; color: var(--gold); letter-spacing: 0.5px;
  opacity: 0; transform: translateY(-30px) scale(0.7);
  transition: opacity 0.4s ease, transform 0.4s ease, left 0.3s ease, top 0.3s ease;
}
.shuffle-card {
  position: absolute; width: 22px; height: 31px; border-radius: 3px;
  background: repeating-linear-gradient(45deg, #234, #234 5px, #1a2f42 5px, #1a2f42 10px);
  border: 1px solid #0b1a26; box-shadow: 0 3px 8px rgba(0,0,0,0.45);
}
/* Brief highlight the instant a card "lands" at a seat. */
.seat.dealt-flash .seat-chip { background: var(--gold); color: #1a1a1a; transition: background 0.1s; }

.start-sequence {
  position: fixed; inset: 0; background: rgba(9, 38, 22, 0.92);
  display: flex; align-items: center; justify-content: center; z-index: 40;
  animation: fadeIn 0.2s ease-out;
}
.start-sequence.hidden { display: none; }
.start-seq-box { text-align: center; }
.start-seq-reveal { display: flex; flex-direction: column; gap: 18px; animation: popIn 0.3s ease-out; }
.start-seq-reveal.hidden { display: none; }
.start-seq-reveal-row { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.start-seq-reveal-label { color: var(--gold); font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; }
.start-seq-reveal-row .card { width: 76px; height: 108px; }
.start-seq-reveal-row .card-center .center-rank { font-size: 26px; }
.start-seq-reveal-row .card-center .center-suit { font-size: 32px; }

/* ---- Chat ---- */
/* Chat toggle -- an in-flow control on the hand-header row (Sept 2026).
   It used to be position:fixed in the bottom-right corner, which meant it
   was anchored to the viewport while the game content overflowed past the
   bottom of short screens: on a 360x640 phone it landed exactly on top of
   the player's cards. Being a normal flex child of .hand-header-actions,
   it now cannot overlap anything on any screen, and the ad-safe-bottom
   offset it used to need is irrelevant -- the row is inside the layout,
   which already sits above the banner.
   Also no longer a solid red circle: red is reserved here for the
   irreversible controls (Leave), and a chat button shouldn't outrank
   Discard / Least Count! for attention. */
.chat-fab {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid var(--jade-border);
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.10); color: var(--text-light);
  cursor: pointer; flex: 0 0 auto;
}
.chat-fab:active { background: rgba(255,255,255,0.2); }
/* Unread is the one state worth shouting about, so the accent goes here
   rather than on the resting button. */
.chat-fab.has-unread { border-color: var(--gold); color: var(--gold); }
.chat-fab .icon { width: 19px; height: 19px; margin: 0; }
/* Keeps the row compact while still giving the button a full 44px touch
   target, by letting the tap area spill outside the painted box instead of
   growing it -- vertical space is the scarcest thing on a short phone. */
.chat-fab::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}
.hand-header-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.chat-fab.hidden { display: none; }
.badge {
  position: absolute; top: -4px; right: -4px; background: var(--red); color: white;
  border-radius: 50%; font-size: 10px; min-width: 16px; height: 16px; padding: 0 3px;
  display: flex; align-items: center; justify-content: center; font-weight: 700;
}
.badge.hidden { display: none; }

/* Tap-outside-to-close target ONLY -- deliberately transparent (Sept 2026).
   It used to be a 50% black scrim, which hid the whole table the moment
   chat opened. The reference we're matching (Instagram's comment sheet)
   never dims what's behind it, and in a card game that matters more than
   in a photo feed: you have to be able to keep watching whose turn it is,
   what the open card is, and the timer, while you're typing. So the layer
   still exists (it's what makes tapping the table close the sheet) but it
   paints nothing. */
.chat-backdrop {
  position: fixed; inset: 0; background: transparent; z-index: 24;
}
.chat-backdrop.hidden { display: none; }

/* ------------------------------------------------------------------
   Chat sheet -- Instagram-comments model (rewritten Sept 2026)
   ------------------------------------------------------------------
   The previous version positioned itself from its own HEIGHT: a flat
   60vh/480px box anchored only at the bottom, with app.js then clamping
   max-height and adding a keyboard offset. Two things went wrong with
   that, both visible in the user's screen recording:

     1. DOUBLE LIFT. app.js added `keyboardHeight` to `bottom`. But on the
        real device the browser ALREADY shrinks the layout viewport when
        the keyboard opens (interactive-widget=overlays-content is not
        honoured there -- see index.html), which means a position:fixed
        element with bottom:0 is ALREADY sitting on top of the keyboard.
        Adding the keyboard height again shoved the sheet a full keyboard
        further up, off the top of the screen. That is the "it jumps up
        and I can't see what I'm typing" symptom exactly.

     2. TOP EDGE DERIVED, NOT ANCHORED. With only `bottom` fixed, the top
        edge is wherever `bottom + height` lands -- so any error in the
        height math (or any keyboard taller than the room left) pushes the
        header, drag handle and close button above y=0. Every previous
        fix here was an attempt to keep that derived number in range.

   The fix is structural: app.js now sets BOTH `top` and `bottom` in
   pixels, so the sheet's height is whatever is left between two points
   that are each independently clamped on-screen. It is impossible for
   this sheet to overflow the top edge, because its top is an input, not
   an output. When the keyboard opens, the bottom edge rises, the top
   edge stays put, and .chat-messages (flex:1, min-height:0) absorbs the
   difference -- which is precisely how a comments sheet behaves.

   No height / max-height / bottom values are declared here on purpose:
   they are owned entirely by positionChatSheet() in app.js. The only
   fallback is for the (desktop) case where the sheet is opened before
   that ever runs. */
.chat-panel {
  position: fixed;
  top: 40%; bottom: var(--ad-safe-bottom, 0px);
  right: 0; left: 0; margin: 0 auto; width: 100%; max-width: 420px;
  background: var(--jade-panel); color: var(--text-light);
  border: 1px solid var(--jade-border); border-bottom: none;
  border-radius: 16px 16px 0 0; z-index: 25;
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: 0 -8px 32px rgba(0,0,0,0.45);
  /* No transition on top/bottom: the keyboard opens in ~250ms and an
     easing curve on top of that reads as lag/jitter rather than polish.
     The open animation below is the only movement. */
  animation: chatSlideUp 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.chat-panel.hidden { display: none; }
@keyframes chatSlideUp {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
/* The grab handle is the swipe-to-dismiss target (wired up in app.js), so
   it needs a real finger-sized hit area, not a 4px hairline. The padding
   is transparent; only the inner bar is drawn -- hence content-box and a
   background on a ::before rather than on the element itself.
   touch-action: none is required, not cosmetic: the drag listeners are
   passive (so they can't preventDefault), which means without this the
   browser would try to scroll the page underneath the gesture at the same
   time as the sheet follows the finger. */
.chat-drag-handle {
  height: 4px; box-sizing: content-box; padding: 10px 0 6px;
  flex-shrink: 0; touch-action: none; cursor: grab;
  display: flex; align-items: center; justify-content: center;
}
.chat-drag-handle::before {
  content: ''; display: block;
  width: 40px; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.28);
}
.chat-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 4px 14px 10px; border-bottom: 1px solid var(--jade-border);
  font-weight: 700; font-size: 14px; touch-action: none;
  /* Never shrink -- the close button lives here, so it has to stay usable
     even when the keyboard leaves very little room for the sheet overall.
     .chat-messages (flex:1, min-height:0) absorbs the squeeze instead. */
  flex-shrink: 0;
}
.chat-header button.tiny .icon, .chat-header span .icon { margin: 0; }
/* min-height: 0 is required here, not optional -- a flex item defaults to
   min-height:auto, which refuses to shrink below its own content's natural
   height. Without this override, once enough messages pile up to overflow
   the panel, the browser just silently clips them instead of ever engaging
   overflow-y's scrollbar/touch-scroll -- the item never registers as
   "smaller than its content," so there's nothing to scroll. This is why
   chat looked stuck/unscrollable once a conversation got long enough.
   overscroll-behavior:contain stops a flick at the end of the list from
   chaining through to the page behind the sheet. */
.chat-messages {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 10px 14px 6px; font-size: 13px;
}

/* Comment row: round avatar on the left, name + text as one wrapped
   paragraph on the right -- the Instagram layout, rather than the old
   single "Name: text" line. The avatar is what makes a fast-moving group
   chat scannable at a glance. */
/* 6px, not 12 (Sept 2026). Measured: a first-message row is 28px tall but
   its text is only 15px, because the avatar sets the row height and
   align-items is flex-start -- so 13px of dead space already sits under
   every first line. Adding 12px on top made the gap between a sender's
   first and second message ~25px, which read as the grouping not working
   even though it was. */
.chat-msg { display: flex; gap: 9px; margin-bottom: 6px; align-items: flex-start; }
/* 24px, down from 28. The earlier note here said shrinking the avatar
   "would cost legibility for a few pixels" -- true in isolation, but it
   misses that the avatar DICTATES the row height next to a 15px line, so
   those pixels are paid on every single first message. 24px still renders
   a clear initial and removes 4px of the dead space. Below ~20 the letter
   genuinely does get hard to read, so this is as far as it should go. */
.chat-avatar {
  flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: #fff; text-transform: uppercase;
  /* hue set inline per player in app.js */
  background: var(--jade-panel-alt);
}
.chat-body { min-width: 0; flex: 1; }
/* Gold for other players' names (matches the score/highlight role used
   everywhere else), red for your own -- same "you" accent as the primary
   button and the timer. */
.chat-msg .chat-name { font-weight: 700; color: var(--gold-bright); }
.chat-msg.me .chat-name { color: var(--brand-line); }
.chat-msg .chat-text { word-break: break-word; color: var(--text-light); }
/* Per-message timestamps are gone from the table chat -- they cost a full
   line each to tell people who are all playing the same round that a message
   arrived "now". A time is printed only after a real silence (.chat-gap). */
.chat-msg.grouped { margin-bottom: 3px; }
/* Lines up under the name when the avatar is omitted: 24px avatar + 9px gap.
   Must track .chat-avatar's width -- if they drift, grouped messages stop
   aligning with the name above them. */
.chat-msg.grouped .chat-body { padding-left: 33px; }
.chat-msg.mentions-me {
  background: rgba(242,193,78,0.10); border-left: 2px solid var(--gold-bright);
  border-radius: 0; padding: 3px 6px 3px 7px; margin-left: -7px;
}
.chat-gap { text-align: center; font-size: 11px; color: var(--text-muted); margin: 10px 0 8px; }
.chat-mention { color: var(--gold-bright); font-weight: 700; }
.chat-mention.me { color: var(--gold-bright); background: rgba(242,193,78,0.18); border-radius: 4px; padding: 0 3px; }

.mention-bar { display: flex; gap: 6px; overflow-x: auto; padding: 6px 10px 0; }
.mention-bar.hidden { display: none; }
.mention-chip {
  width: auto !important; flex: 0 0 auto; padding: 6px 12px !important;
  font-size: 13px !important; border-radius: 999px;
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border);
  color: var(--text-light);
}

/* Three classes deep so it beats .group-chat-msg.mine's padding shorthand
   regardless of source order -- a grouped message of your own must not
   reintroduce the top padding the grouping exists to remove. */
.group-chat-msg.grouped { padding-top: 0; }
.group-chat-msg.mine.grouped { padding-top: 0; }
.group-chat-msg.mentions-me { border-left: 2px solid var(--gold-bright); }
.group-chat-msg .gc-text { color: var(--text-light); }

/* ---- Collapse the live strip's blank lines ----
   #group-live-names, #group-bell-hint and #group-error are present but
   empty most of the time, and their margins still apply: measured, the
   three of them cost 84px of the live strip's 164px while showing
   nothing (#group-error alone burned 42px). :empty matches only when an
   element has no child nodes at all, and textContent = '' leaves exactly
   that -- so this collapses them with no JS and re-expands the instant
   anything is written. Verified against a write-then-clear cycle. */
#group-live-names:empty,
#group-bell-hint:empty,
#group-error:empty { display: none; }

/* ---- Live strip on one row ----
   "No one at the table" and "I want to play" were stacked, costing a full
   row for a five-word status. Side by side they fit the width with room
   over, and the ~40px saved is what lets the chat preview below clear the
   ad banner.

   flex-wrap is the safety net, not decoration: the status line is short
   when nobody's there but becomes "Mani, Vinayaka and Shaker are at the
   table" once people arrive, and the button becomes "Start Game". When
   that no longer fits on one line the text takes its own row and the
   button drops beneath it -- the old layout, reached automatically,
   instead of either one being squeezed. */
.group-live {
  display: flex; flex-wrap: wrap; align-items: center;
  column-gap: 12px; row-gap: 6px;
}
/* ORDER matters here, and not just aesthetically. In the markup
   #group-live-names sits BETWEEN the status text and the buttons. Since it
   takes a full row, it was landing between them and pushing the button onto
   a THIRD line -- the opposite of the intent. Explicit order pairs the
   status with its button and drops the names underneath. */
/* 100px, not 140. The widest button is the bell ("I want to play") at
   163px, and the strip's usable width is 310px on a 375 screen. At a
   140px basis that came to 315 and wrapped -- in the empty-table state,
   the one this row layout exists for. 100 + 12 + 163 = 275, with room for
   a button up to ~198px before wrapping kicks in. */
.group-live > #group-live-text { order: 0; flex: 1 1 100px; text-align: left; }
.group-live > button           { order: 1; flex: 0 0 auto; width: auto; margin: 0; padding: 0 18px; min-height: 44px; }
.group-live > #group-live-names { order: 2; flex: 1 1 100%; }
.group-live > #group-bell-hint  { order: 3; flex: 1 1 100%; }
.group-live > #group-error      { order: 4; flex: 1 1 100%; }

/* Same tightening the landing screen got, for the same reason: with the
   chat preview added, the group screen is now the one fighting for the
   last few pixels against the ad banner. Scoped to this screen only. */
#screen-group .landing-body { gap: 12px; }
#screen-group .mode-block { padding: 12px 16px; }
#screen-group .mode-block h3 { margin-bottom: 2px; }

.lobby-players li.own-row { cursor: pointer; }
.row-edit { font-size: 11px; color: var(--text-muted); margin-left: 8px; }
.chat-msg .chat-gif { display: block; max-width: 160px; border-radius: 8px; margin-top: 4px; }
.chat-empty { color: var(--text-muted); font-size: 12px; text-align: center; margin-top: 20px; }

/* Never shrink -- this is the actual text box, so it has to stay usable
   even when the keyboard leaves very little room for the sheet overall.
   .chat-messages (flex:1, min-height:0) absorbs the squeeze instead. */
.chat-input-row {
  display: flex; gap: 8px; align-items: center;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--jade-border); flex-shrink: 0;
  background: var(--jade-panel);
}
.chat-input-row input {
  flex: 1; min-width: 0; padding: 10px 14px; border-radius: 20px;
  border: 1px solid var(--jade-border);
  background: var(--jade-panel-alt); color: var(--text-light);
  /* 16px minimum: anything smaller and Android/iOS WebViews zoom the page
     in on focus, which on this fixed-position sheet looks like the layout
     lurching sideways the moment you tap the box. */
  font-size: 16px; line-height: 20px;
}
.chat-input-row button.tiny {
  width: 40px; height: 40px; flex-shrink: 0; padding: 0;
  border-radius: 50%; display: flex; align-items: center; justify-content: center;
}
.chat-input-row button.tiny .icon { width: 18px; height: 18px; margin: 0; }
#btn-gif-open { font-weight: 700; font-size: 11px; background: rgba(255,255,255,0.1); color: var(--text-light); }

/* ---- GIF picker (Giphy search) ---- */
.gif-picker {
  position: absolute; left: 0; right: 0; bottom: 56px; height: 260px;
  background: var(--jade-panel); border-top: 1px solid var(--jade-border); z-index: 26;
  display: flex; flex-direction: column;
}
.gif-picker.hidden { display: none; }
.gif-picker-header { display: flex; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--jade-border); }
.gif-picker-header input {
  flex: 1; padding: 8px 12px; border-radius: 16px; border: 1px solid var(--jade-border);
  background: var(--jade-panel-alt); color: var(--text-light); font-size: 13px;
}
.gif-picker-header button.tiny .icon { width: 16px; height: 16px; margin: 0; }
.gif-suggestions {
  display: flex; gap: 6px; overflow-x: auto; padding: 8px 10px 0; flex-shrink: 0;
}
.gif-suggestion-chip {
  flex-shrink: 0; background: var(--jade-panel-alt); border: 1px solid var(--jade-border); border-radius: 14px;
  padding: 5px 12px; font-size: 12px; color: var(--text-light); white-space: nowrap; cursor: pointer;
}
.gif-suggestion-chip:active { background: rgba(255,255,255,0.1); }
.gif-results {
  /* min-height: 0 -- same flex-item trap as .chat-messages above: without it
     this grid refuses to shrink below its own content height once enough
     GIF results load, so overflow-y:auto never actually engages. */
  flex: 1; min-height: 0; overflow-y: auto; padding: 8px; display: grid;
  grid-template-columns: repeat(3, 1fr); gap: 6px; align-content: start;
}
.gif-results img { width: 100%; height: 72px; object-fit: cover; border-radius: 6px; cursor: pointer; }
.gif-results-hint { grid-column: 1 / -1; text-align: center; color: var(--text-muted); font-size: 12px; padding: 24px 0; }

/* ==========================================================================
   Persistent groups + invite funnel (Sept 2026)
   ==========================================================================
   Everything here serves one flow: someone shares a link into a WhatsApp
   family group, a person taps it, and they're playing within seconds. See
   the comments in index.html and app.js for why each piece exists.
   ========================================================================== */

/* ---- Your Groups list on the landing screen ---- */
.group-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.group-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border);
  cursor: pointer; transition: border-color 0.15s ease, transform 0.05s ease;
}
.group-row:active { transform: scale(0.99); }
.group-row:hover { border-color: rgba(255,255,255,0.28); }
.group-row-name {
  font-weight: 700; font-size: 15px; color: var(--text-light);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.group-row-go { flex-shrink: 0; font-size: 12px; font-weight: 700; color: var(--gold-bright); }

/* Separator between the one-off room controls and the permanent-group
   controls inside the same "Play with Friends" block. */
.mode-block-divider {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0 10px; color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
}
.mode-block-divider::before, .mode-block-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--jade-border);
}

/* ---- Invite card (shown instead of the mode menu for ?g= / ?room= links) ---- */
.invite-card {
  background: var(--jade-panel); border: 1px solid var(--jade-border);
  border-radius: var(--radius-lg); padding: 20px 18px; box-shadow: var(--shadow-md);
  display: flex; flex-direction: column; gap: 12px; text-align: center;
}
.invite-card.hidden { display: none; }
.invite-headline { margin: 0; font-size: 18px; font-weight: 700; color: var(--text-light); }
.invite-room-line { font-size: 13px; color: var(--text-muted); }
.invite-room-line span { color: var(--gold-bright); font-weight: 700; }
.invite-card label { text-align: left; font-size: 12px; color: var(--text-muted); margin-bottom: -6px; }
.invite-card input {
  padding: 12px 14px; border-radius: var(--radius-md); border: 1px solid var(--jade-border);
  background: var(--jade-panel-alt); color: var(--text-light);
  /* 16px minimum, or Android zooms the page in when the field is focused. */
  font-size: 16px;
}

/* ---- WhatsApp invite button (the lobby's primary action while alone) ---- */
.invite-btn {
  width: 100%; min-height: 44px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 4px;
}
.invite-btn .icon { width: 18px; height: 18px; margin: 0; }

/* ---- Group name + leaderboard in the lobby ---- */
.lobby-group-name {
  font-size: 20px; font-weight: 700; color: var(--gold-bright);
  margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lobby-group-name.hidden { display: none; }

.standings-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.standings-row {
  display: grid;
  /* rank | name | wins, with the meta line wrapping underneath the name */
  grid-template-columns: 22px 1fr auto;
  align-items: center; gap: 4px 10px;
  padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border);
  font-size: 14px;
}
.standings-row:first-child { border-color: rgba(242,193,78,0.45); }
.st-rank { color: var(--text-muted); font-weight: 700; font-size: 12px; }
.st-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-wins { color: var(--gold-bright); font-weight: 700; }
.st-meta { grid-column: 2 / -1; font-size: 11px; color: var(--text-muted); }

/* ---- Landing: Quick Play segmented toggle (Online / vs Bots) ---- */
/* ---- Bottom sheet (Sept 2026) --------------------------------------------
   For content you READ rather than answer: standings, tables, lists. A
   .card-panel dialog shrinks to its content (no width, only max-width), so
   a four-row scorecard rendered as a 158px sliver floating in the middle of
   a 360px screen with 252px of dead space above and below -- measured. That
   shape says "confirm this", not "here are the standings".
   Anchored to the bottom, full width, capped at 82% height so the table
   underneath stays partly visible. Head and foot are pinned; only .sheet-body
   scrolls, so the tabs and close button never scroll away.
   -------------------------------------------------------------------- */
.sheet-overlay { align-items: flex-end; padding: 0; }
.sheet-panel {
  width: 100%;
  max-height: 82svh;
  display: flex;
  flex-direction: column;
  background: var(--jade-panel);
  color: var(--text-light);
  border-top: 1px solid var(--jade-border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 14px 16px calc(12px + var(--ad-safe-bottom, 0px));
  box-shadow: var(--shadow-lg);
}
.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex: 0 0 auto; margin-bottom: 10px;
}
.sheet-head h2 { margin: 0; font-size: 17px; }
/* min-height:0 or the flex child refuses to shrink and the sheet grows past
   its own max-height instead of scrolling -- same trap as .landing-body. */
.sheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.sheet-foot { flex: 0 0 auto; margin: 10px 0 0; font-size: 11.5px; color: var(--text-muted); }
.sheet-foot:empty { display: none; }
.scorecard-tabs { flex: 0 0 auto; }

/* Standings row: rank, name, score, and a bar showing how close this player
   is to the elimination score. The bar is the point -- in Least Count what
   matters is not that someone has 120, it's that they're 80 away from out. */
.sc-row { padding: 9px 0; border-bottom: 1px solid rgba(255,255,255,0.07); }
.sc-row:last-child { border-bottom: none; }
.sc-row-top {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 14px; margin-bottom: 6px;
}
.sc-rank { color: var(--text-muted); font-size: 12px; min-width: 14px; }
.sc-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-score { font-weight: 700; flex: 0 0 auto; }
.sc-bar { height: 5px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
.sc-bar span { display: block; height: 100%; border-radius: 3px; background: var(--jade-accent, #5dcaa5); }
/* Amber from 60% of the limit, red once out -- the same three-state read as
   the seat colours, so the sheet and the table agree at a glance. */
.sc-row.warn .sc-bar span { background: var(--gold); }
.sc-row.out .sc-bar span { background: var(--red); }
.sc-row.out .sc-name, .sc-row.out .sc-score { color: var(--text-muted); }
.sc-row.is-me .sc-name { font-weight: 700; }
.sc-tag {
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  color: var(--red-text); flex: 0 0 auto;
}

.segmented { display: flex; gap: 6px; margin-bottom: 10px; }
.segmented .seg {
  flex: 1; padding: 9px 8px; border-radius: var(--radius-md);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt);
  color: var(--text-muted); font-size: 13px; font-weight: 700; cursor: pointer;
}
.segmented .seg.active {
  /* Was hardcoded rgba(229,37,44) -- the old signal red, in a form the
     hex sweep could not see. Selection is a state, not a cost, so it
     takes gold like every other "this one is chosen" affordance. */
  background: rgba(var(--brand-rgb),0.18); border-color: var(--brand-line); color: var(--text-light);
}

/* Cap the groups list at roughly two rows and scroll the rest, so someone in
   eight groups can't push Quick Room and Quick Play off the screen. */
.group-list { max-height: 132px; overflow-y: auto; overscroll-behavior: contain; }
.group-row-menu {
  flex-shrink: 0; width: 32px; height: 32px; padding: 0;
  border-radius: 50%; border: 1px solid var(--jade-border);
  background: transparent; color: var(--text-muted);
  font-size: 18px; line-height: 1; cursor: pointer;
}
.group-row-menu:hover { color: var(--text-light); border-color: rgba(255,255,255,0.3); }

/* ---- Lobby: group name with the leaderboard icon pinned top-right ---- */
.lobby-group-header {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 8px;
}
.lobby-group-header.hidden { display: none; }
.lobby-group-header .lobby-group-name { margin-bottom: 0; flex: 1; min-width: 0; text-align: left; }

/* ---- Round outcome during the card reveal (Sept 2026) ----
   The reveal showed everyone's hands but never said what had happened --
   no winner, and a penalty looked the same as a clean declare. These are
   the verdict. */
.reveal-banner {
  position: absolute; left: 50%; top: 46%; transform: translate(-50%, -50%);
  z-index: 6; text-align: center; pointer-events: none;
  max-width: 74%;
}
.reveal-banner.hidden { display: none; }
.reveal-banner-title {
  font-size: 17px; font-weight: 700; color: var(--gold-bright);
  text-shadow: 0 2px 8px rgba(0,0,0,0.7); line-height: 1.25;
}
.reveal-banner-title.wrong { color: var(--red-text); }
.reveal-banner-sub {
  margin-top: 4px; font-size: 13px; font-weight: 700; color: var(--text-light);
  text-shadow: 0 2px 6px rgba(0,0,0,0.7);
}
.reveal-banner-sub.penalty { color: var(--red-text); }

/* Outcome states on the seat boxes. .declared (gold) already existed for
   "this person called it"; these say whether it worked. */
.seat-reveal-box.winner  { border: 2px solid var(--good); }
.seat-reveal-box.penalty { border: 2px solid var(--red-text); }
.seat-reveal-box.penalty .seat-reveal-tag { color: var(--red-text); }
.seat-reveal-box.winner  .seat-reveal-tag { color: var(--good); }

/* Round score sits next to the hand value -- the hand value alone never
   told you what the round actually cost. */
.seat-reveal-score { font-weight: 700; color: var(--gold-bright); font-size: 15px; }
.seat-reveal-arrow { margin: 0 4px; color: var(--text-muted); font-size: 12px; }
.seat-reveal-total { font-weight: 700; color: var(--text-light); font-size: 13px; }
.seat-reveal-score.zero { color: var(--good); }
.seat-reveal-score.penalty { color: var(--red-text); }

/* ==========================================================================
   Group screen (Sept 2026 rebuild -- see group-spec.md)
   ==========================================================================
   Tapping a group opens this, not a game lobby. Everything here exists to
   make the group say SOMETHING when you open it, whether or not anyone is
   playing -- which is what the first version failed to do.
   ========================================================================== */

/* ---- live strip: the state of play right now ---- */
.group-live { text-align: center; }
.group-live-text { font-size: 16px; font-weight: 700; color: var(--text-light); }
.group-live-names { font-size: 13px; color: var(--text-muted); margin-top: 2px; min-height: 16px; }
.group-live .invite-btn, .group-live button.primary, .group-live button.secondary { margin-top: 10px; }
.group-scoring-note { margin-top: 10px; font-size: 11px; line-height: 1.5; }

/* ---- members ---- */
.group-member-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.group-member {
  display: grid; grid-template-columns: 10px 1fr auto; align-items: center; gap: 9px;
  padding: 8px 2px; font-size: 14px;
}
/* Faded, not hidden -- a member who isn't around still belongs to the group,
   and "played yesterday" is what keeps the list from reading as empty. */
.group-member.away { opacity: 0.62; }
.gm-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); }
/* 'a game is live' is good news, not a cost -- green, like every other
   positive state. */
.gm-dot.playing { background: var(--good); box-shadow: 0 0 0 3px rgba(74,222,128,0.22); }
.gm-dot.table   { background: var(--gold-bright); box-shadow: 0 0 0 3px rgba(242,193,78,0.20); }
.gm-dot.online  { background: var(--good); }
.gm-dot.away    { background: rgba(255,255,255,0.22); }
.gm-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gm-host {
  font-size: 9px; font-weight: 700; letter-spacing: 0.06em; color: var(--gold-bright);
  border: 1px solid rgba(242,193,78,0.5); border-radius: 4px; padding: 1px 4px; margin-left: 4px;
}
.gm-meta { font-size: 11px; color: var(--text-muted); white-space: nowrap; }

/* ---- group code ----
   Understated but legible. It only matters at the moment somebody joins, so
   it must not compete with the group's name -- but the first attempt put it
   beside a width:100% button, which squashed it to an unreadable sliver at
   the right edge. Its own line, left-aligned, in a quiet chip. */
.group-code-row {
  display: flex; align-items: center; gap: 8px;
  margin: 12px 0 10px;
}
.group-code-label { font-size: 11px; color: var(--text-muted); }
.group-code-value {
  font-size: 15px; letter-spacing: 0.18em; color: var(--text-light); font-weight: 700;
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border);
  border-radius: var(--radius-sm); padding: 4px 10px;
}
.group-count { font-size: 12px; font-weight: 400; color: var(--text-muted); }

/* ---- recent games ---- */
.group-recent-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.group-recent-row {
  display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: baseline;
  font-size: 13px; padding: 5px 0;
}
.gr-when { color: var(--text-muted); font-size: 11px; min-width: 62px; }
.gr-who { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gr-n { color: var(--text-muted); font-size: 11px; }

/* Four tabs on a narrow phone: shrink the type and padding rather than let
   them wrap onto two rows. */
.segmented-4 .seg { padding: 8px 4px; font-size: 12px; letter-spacing: -0.01em; }

/* ==========================================================================
   Overflow menu -- WhatsApp pattern (Sept 2026)
   ==========================================================================
   A plain list dropping from the three dots at top right.

   Positioned FIXED to the viewport rather than anchored to the button. This
   project already tried anchoring a dropdown to a topbar control once (the
   profile menu) and it went through several rounds of position/z-index/
   overflow bugs before being replaced with a centred modal. The topbar is
   always in the same place, so top-right is always top-right -- no anchoring
   maths, nothing to escape.
   ========================================================================== */
.menu-backdrop { position: fixed; inset: 0; background: transparent; z-index: 60; }
.menu-backdrop.hidden { display: none; }

.menu-sheet {
  position: fixed; top: 52px; right: 8px; z-index: 61;
  min-width: 200px; max-width: 74vw;
  background: var(--jade-panel); border: 1px solid var(--jade-border);
  border-radius: var(--radius-md); box-shadow: 0 10px 30px rgba(0,0,0,0.5);
  padding: 6px 0; overflow: hidden;
  transform-origin: top right;
  animation: menuPop 0.12s ease-out;
}
.menu-sheet.hidden { display: none; }
@keyframes menuPop { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: scale(1); } }

/* Plain rows, left-aligned, no icons -- these override the app-wide
   button rule (width:100%, centred, chunky padding) that made the old
   version look like a stack of call-to-action buttons. */
.menu-row {
  width: 100%; display: block; text-align: left;
  padding: 13px 18px; font-size: 15px; font-weight: 400;
  background: transparent; border: none; border-radius: 0;
  color: var(--text-light); cursor: pointer;
}
.menu-row:active { background: rgba(255,255,255,0.07); }
.menu-row.hidden { display: none; }
.menu-row.danger { color: var(--red-text); }

/* ---- small confirm / input dialogs launched FROM the menu ---- */
.dialog-panel { max-width: 320px; }
.dialog-panel h2 { margin-bottom: 10px; }
.dialog-panel input {
  width: 100%; padding: 12px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt);
  color: var(--text-light); font-size: 16px; margin-bottom: 4px;
}
.dialog-actions { display: flex; gap: 8px; margin-top: 14px; }
.dialog-actions button { flex: 1; }

/* ==========================================================================
   Marathon strip + group chat
   ========================================================================== */
.group-marathon-strip {
  display: flex; flex-direction: column; gap: 4px;
  padding: 10px 12px; margin-bottom: 10px;
  background: var(--jade-panel-alt); border: 1px solid var(--jade-border);
  border-radius: var(--radius-md);
}
.group-marathon-state { font-size: 15px; font-weight: 600; color: var(--text-light); }
.group-marathon-sub { font-size: 12px; color: var(--text-muted); }
.group-marathon-strip button { margin-top: 8px; }
.group-marathon-strip button.hidden { display: none; }

/* Fixed height so the chat can never push the input off the bottom of a
   phone as messages arrive -- the list scrolls, the page doesn't grow. */
/* ------------------------------------------------------------------
   Group chat message density (rewritten Sept 2026)
   ------------------------------------------------------------------
   Every message used to be its own filled card -- background, radius,
   padding: 7px 10px -- with a 6px flex gap between cards. Measured on
   seven real messages covering every state the renderer produces:

     7 messages     = 247px, of which only 126px was text
     text-to-text   = 15-20px between consecutive lines

   So over half the chat was whitespace, and the gap came from three
   places at once: 7px bottom padding + 6px gap + 7px top padding.

   Worse, it defeated the grouping. app.js already drops the name/time
   line for consecutive messages from one person, but .grouped only
   trimmed padding-top to 2px and each message still drew as a separate
   chip -- so one person's two lines read as two unrelated cards.

   Now: no card. Plain lines, like the table chat (which uses padding:0
   and a 12px margin, and was never the one that looked wrong). The
   background survives ONLY where it carries meaning -- your own
   messages, and @mentions of you. Same seven messages now measure
   152px with a 3-4px gap.
   ------------------------------------------------------------------ */
.group-chat-list {
  list-style: none; margin: 0 0 8px; padding: 0;
  max-height: 190px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
}
.group-chat-list.hidden { display: none; }
.group-chat-msg {
  padding: 1px 0; border-radius: 0; background: none;
  font-size: 14px; color: var(--text-light); word-break: break-word;
}
.group-chat-msg .gc-name { font-weight: 600; font-size: 12px; color: var(--gold-bright); margin-right: 6px; }
.group-chat-msg .gc-time { float: right; font-size: 11px; color: var(--text-muted); }
/* The two states that still earn a background. Horizontal padding and a
   radius give the tint a shape; the negative margin keeps the text itself
   aligned with the unstyled lines above and below it. */
.group-chat-msg.mine {
  background: rgba(46,160,120,0.18);
  border-radius: 6px; padding: 1px 8px; margin: 0 -8px;
}
.group-chat-msg.mentions-me { padding-left: 7px; margin-left: -7px; }

.group-chat-row { display: flex; gap: 8px; align-items: center; }
.group-chat-row input {
  flex: 1; min-width: 0; padding: 12px 14px; border-radius: var(--radius-md);
  border: 1px solid var(--jade-border); background: var(--jade-panel-alt);
  color: var(--text-light);
  /* 16px or iOS zooms the whole page in when the field is focused. */
  font-size: 16px;
}
/* Overrides the app-wide button{width:100%}, which would otherwise push the
   input down onto its own line -- the same rule that once squeezed the group
   code into a sliver at the screen edge. */
.group-chat-row button { width: auto; flex: 0 0 auto; padding: 12px 18px; }


/* ==========================================================================
   Sept 2026 audit fixes
   ========================================================================== */

/* --- Touch targets ---------------------------------------------------------
   Measured on the live build at 375x812. Discard and Least Count! were 37px
   tall -- the two most-tapped controls in the game were the smallest things
   on screen that mattered. 44px is the platform minimum for a finger. */
/* 44px is the platform minimum for a finger and stays the target on any
   normal phone (6.8svh clears 44px from 647px tall upward). It relaxes only
   on genuinely tiny screens, where an untappable-but-visible button is
   still better than one pushed off the bottom entirely. */
#btn-discard, #btn-declare { min-height: clamp(38px, 6.8svh, 44px); }
#btn-profile-menu { min-width: 44px; min-height: 44px; }
#btn-mute, #btn-scores, #btn-leave-game { min-height: 44px; min-width: 44px; }
#btn-game-rules { min-height: 44px; }
.segmented .seg { min-height: 44px; }
.footer-nav-item { min-height: 44px; }

/* Leave sits beside Scores in the game bar. Outlined, not filled: it's the
   one irreversible control up there. */
.leave-btn { color: var(--text-muted); }
.leave-btn:active { color: var(--red-text); }

/* --- Queued leave (Sept 2026) ---------------------------------------------
   A leave tapped mid-round can't be honoured until the hand ends, so it's
   held. The old build said so once, in the #game-error line, which the next
   turn change wiped -- after that nothing on screen knew a leave was coming
   and the button looked broken.
   This state lives on the button, so it survives every state push, and the
   dot gives it a glance-readable "something is queued" that the label alone
   (a bare icon) can't. Cancelling is a second tap; the dialog offers it. */
.leave-btn.leave-pending {
  color: var(--red-text);
  position: relative;
}
.leave-btn.leave-pending::after {
  content: '';
  position: absolute; top: 4px; right: 4px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--red);
  animation: leave-pending-pulse 1.6s ease-in-out infinite;
}
@keyframes leave-pending-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}
@media (prefers-reduced-motion: reduce) {
  .leave-btn.leave-pending::after { animation: none; }
}

/* The round advances by itself, so the countdown is the actual news on this
   screen -- it sat as small grey text at the very bottom while "Start Now"
   shouted above it. #round-result-hint is the element that carries it. */
#round-result-hint {
  font-size: 14px; color: var(--text-light); margin-top: 10px;
}

/* Leave Game sat directly under Start Now in near-identical red, a thumb-width
   from the one action that can't be undone. Step it back to a quiet outline
   and put air between them. */
#btn-leave-round-result, #btn-leave-gameover {
  color: var(--text-muted); border-color: var(--jade-border);
  background: transparent; margin-top: 14px;
}
#btn-leave-round-result:active, #btn-leave-gameover:active { color: var(--red-text); }

/* --- Lobby own row: Edit belongs next to the name it edits, not after the
   status, where it read as "Mani HOST online Edit". --- */
.lobby-players li.own-row { display: flex; align-items: center; gap: 6px; }
.lobby-players li.own-row .row-edit { order: 1; margin-left: 2px; }
.lobby-players li.own-row .status { order: 2; margin-left: auto; }

/* --- Respect "remove animations" -------------------------------------------
   There was no prefers-reduced-motion rule anywhere in this stylesheet. It
   matters most for the intro splash, which is dismissed BY an animation: with
   motion disabled it could sit over the whole app forever. app.js now removes
   the node regardless, and this makes it never appear in the first place. */
@media (prefers-reduced-motion: reduce) {
  #screen-intro { display: none !important; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   Tutorial coach (Sept 2026)
   ==========================================================================
   One line of guidance at a time, directly above the hand tray. Placed
   there rather than floating over the table because every action it refers
   to -- selecting, discarding, declaring -- happens in the tray or the two
   buttons under it, and advice that sits away from the control it describes
   makes people hunt.

   Deliberately NOT an overlay: it must never block the table, because the
   whole point is that the learner is watching a real game play out.
   ========================================================================== */
.coach {
  display: flex; align-items: flex-start; gap: 10px;
  margin: 0 0 6px; padding: 9px 11px;
  background: rgba(212,175,55,0.13);
  border: 1px solid var(--gold);
  border-radius: var(--radius-sm);
}
.coach-body { flex: 1 1 auto; min-width: 0; }
.coach-step {
  display: block; font-size: 10.5px; letter-spacing: 0.05em;
  color: var(--gold-bright); font-weight: 700; margin-bottom: 2px;
}
.coach-text { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--text-light); }
.coach-actions { flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; align-items: stretch; }
.coach-btn {
  width: auto; min-width: 74px; padding: 7px 12px; font-size: 13px;
  background: var(--gold); color: #1a1a1a; border: none; border-radius: var(--radius-sm);
}
.coach-skip {
  width: auto; padding: 4px 8px; font-size: 11.5px;
  background: transparent; color: var(--text-muted);
  border: none; text-decoration: underline;
}
/* Pulses whatever the current step is telling you to touch. */
.coach-target { animation: coachPulse 1.4s ease-in-out infinite; }
@keyframes coachPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212,175,55,0); }
  50%      { box-shadow: 0 0 0 4px rgba(212,175,55,0.45); }
}
@media (prefers-reduced-motion: reduce) {
  .coach-target { animation: none; outline: 2px solid var(--gold); }
}

/* Post-tutorial handoff strip on the landing screen. (The first-launch
   offer that also used these is a modal now -- see .tutorial-modal.) */
.tutorial-offer {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 12px; padding: 12px;
  background: rgba(212,175,55,0.12);
  border: 1px solid var(--gold); border-radius: var(--radius-sm);
  text-align: left;
}
.tutorial-offer-text { flex: 1 1 150px; min-width: 0; }
.tutorial-offer-text strong { display: block; font-size: 14px; color: var(--gold-bright); }
.tutorial-offer-text span { display: block; font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
.tutorial-offer-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.tutorial-offer-actions .primary { width: auto; padding: 9px 16px; font-size: 14px; }
.tutorial-offer-no {
  width: auto; padding: 6px 8px; font-size: 12px;
  background: transparent; border: none; color: var(--text-muted); text-decoration: underline;
}

/* ==========================================================================
   Scattered-card backdrop (Sept 2026)
   ==========================================================================
   Replaces the flat fill behind every non-game screen with the app's own
   playing cards, tumbling. Built from real .card elements rather than an
   image or a drawing, which means: no licence to worry about, nothing to
   download on a slow connection, nothing that can 404, and it is impossible
   for the backdrop to show cards that look different from the ones in play.

   Every card is tilted on two axes, never square to the edge -- a card lying
   flat and straight reads as a rectangle, and a rectangle reads as a box
   rather than a card.

   position:fixed so it does not scroll with content and costs nothing to
   repaint; the whole layer is inert to input.
   ========================================================================== */
.bg-cards {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none; overflow: hidden;
  perspective: 1200px;
}
.bg-cards .card {
  position: absolute;
  /* Fixed size regardless of the in-game responsive card sizing -- this
     layer must not resize itself when the table does. */
  width: 74px; height: 104px; min-width: 0; min-height: 0;
  margin: 0; cursor: default;
  transform-style: preserve-3d;
  will-change: transform;
}
/* The scrim is what makes text readable over the cards. Without it this is
   the "spectacular in a screenshot, unusable in the hand" background --
   bright card faces directly behind body copy. Heavier towards the bottom
   where the panels and buttons live. */
.bg-scrim {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(circle at 50% 6%, rgba(var(--red-rgb),0.20), transparent 52%),
    linear-gradient(180deg,
      rgba(8,31,25,0.62) 0%,
      rgba(8,31,25,0.84) 45%,
      rgba(8,31,25,0.94) 100%);
}
/* The table has its own felt and its own cards. Two layers of cards is
   noise, and on the one screen where reading a card matters it would be
   actively harmful. */
body.game-active .bg-cards,
body.game-active .bg-scrim { display: none; }

/* Everything on a screen sits above the backdrop. */
.screen { position: relative; z-index: 1; }


/* Tutorial invitation modal. Buttons are full-width and stacked so both
   clear the 44px touch minimum -- the inline version this replaces had a
   35px button and a 26px text link, which are mis-tap targets. */
.tutorial-modal { text-align: center; }
.tutorial-modal h2 { margin: 0 0 6px; color: var(--gold-bright); }
.tutorial-modal-sub {
  margin: 0 0 16px; font-size: 13.5px; line-height: 1.5; color: var(--text-muted);
}
.tutorial-modal button { min-height: 48px; }
.tutorial-modal button + button { margin-top: 8px; }

/* ==========================================================================
   Tab hierarchy on the landing screen (Sept 2026)
   ==========================================================================
   Two rows of tabs sat on this screen at identical size, weight and colour:
   "Quick Play / Groups", which chooses what KIND of thing you are doing, and
   "vs Bots / Online", which is a setting INSIDE Quick Play. Nothing said one
   contained the other, so the screen read as four equal options.

   Fixed with weight rather than a new pattern: the outer row is bigger and
   separated by real space; the inner row is smaller, lighter and flatter, so
   it reads as a control rather than navigation.

   Both keep a 44px minimum hit area -- the inner row is quieter, not
   smaller to the thumb. Shrinking a touch target to express hierarchy is
   the wrong trade.
   ========================================================================== */
.landing-tabs { gap: 8px; margin-bottom: 18px; }
.landing-tabs .seg {
  min-height: 50px; font-size: 15px; letter-spacing: 0.01em;
  border-radius: var(--radius-md);
}
.landing-tabs .seg.active {
  /* The parent row's selected state is the strongest thing in this group. */
  background: rgba(var(--brand-rgb),0.22);
  border-color: var(--brand-line);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.10);
}

#quickplay-toggle { gap: 6px; margin-bottom: 12px; }
#quickplay-toggle .seg {
  min-height: 44px;            /* unchanged: quieter, not harder to hit */
  font-size: 12.5px; font-weight: 600;
  border-radius: var(--radius-sm);
  background: transparent;
  border-color: rgba(255,255,255,0.10);
}
#quickplay-toggle .seg.active {
  background: rgba(255,255,255,0.07);
  border-color: rgba(255,255,255,0.28);
  color: var(--text-light);
  box-shadow: none;
}


/* ==========================================================================
   Rewarded-ad sheet (Sept 2026)
   ==========================================================================
   Reuses .overlay + .card-panel, the same pattern as the tutorial offer and
   the profile panel, so it inherits the dim, the centring and the dark card
   without inventing a third modal style.

   The only additions are spacing and the note line. Deliberately plain: this
   appears at a moment the player is already impatient to get back into a
   game, and a decorated advertisement pitch would read as pushier than it is.
   ========================================================================== */
.ad-reward-modal { max-width: 340px; text-align: center; }
.ad-reward-modal h2 { margin: 0 0 6px; font-size: 19px; }
.ad-reward-sub {
  margin: 0 0 16px; font-size: 13.5px; line-height: 1.45;
  color: var(--text-muted);
}
.ad-reward-modal button { width: 100%; }
.ad-reward-modal button + button { margin-top: 8px; }
.ad-reward-note {
  margin: 12px 0 0; font-size: 11.5px; line-height: 1.4;
  color: var(--text-muted); opacity: 0.85;
}
/* While the video is loading or playing, the buttons must not be tappable --
   a second tap would request a second ad for a seat they are already getting. */
.ad-reward-modal.busy button { opacity: 0.55; pointer-events: none; }


/* ==========================================================================
   In-game invite sheet (Sept 2026)
   ========================================================================== */
.game-invite-modal { max-width: 340px; text-align: center; }
.game-invite-modal h2 { margin: 0 0 6px; font-size: 19px; }
.game-invite-sub {
  margin: 0 0 14px; font-size: 13px; line-height: 1.45; color: var(--text-muted);
}
/* Reuses .room-code-display from the lobby so the code reads identically in
   both places -- same size, same letter-spacing, same weight. Only the
   centring and spacing are set here. */
.game-invite-modal .room-code-display { margin: 0 auto 14px; }
.game-invite-actions { display: flex; gap: 8px; }
.game-invite-actions button { flex: 1; }
#btn-game-invite-close { width: 100%; margin-top: 10px; }
#game-invite-feedback { min-height: 16px; margin: 8px 0 0; }


/* The ad note on the waiting-for-host screen. Slightly dimmer than the line
   above it: it is a heads-up, not the main message, and this screen already
   asks the player to be patient. */
.waiting-host-ad-note { opacity: 0.8; margin-top: 6px; }


/* The open card and joker slots occupy the centre of the table, which is now
   also where the deal countdown appears. They hold nothing during the deal --
   there IS no open card until the cards have been dealt -- so the empty
   "Open Card" and "Joker" labels fade out while the badge is up rather than
   sitting behind it. Mirrors #screen-game.reveal-phase, which frees the same
   space for the round outcome. Keyed off a class on the screen rather than a
   sibling selector, because the badge sits after .table-center in source
   order and CSS cannot look backwards. */
#screen-game.deal-phase .table-center { opacity: 0; transition: opacity 0.2s; }
#screen-game.deal-phase .stock-badge { display: none; }


/* .rules-btn is now unused: the lobby's Game Rules went from a full-width
   button in the panel to an icon in the top bar (Oct 2026), and nothing else
   in index.html carries the class. Left in place rather than deleted because
   it is harmless and removing styles for markup that might come back is how
   you get a button with no styling six months later. */


/* ==========================================================================
   Lobby bot controls (Oct 2026)
   ==========================================================================
   One row, inside the Players panel. The lobby was already overflowing on a
   phone -- Start Game and Leave Table sat below the fold -- so this had to
   cost almost nothing: a label and a stepper on a single 36px line, not a
   fourth panel with its own padding and heading.
   ========================================================================== */
.lobby-bots-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--jade-border);
}
.lobby-bots-row.hidden { display: none; }
.lobby-bots-label { font-size: 12px; color: var(--text-muted); font-weight: 600; }
.lobby-bots-stepper { display: flex; align-items: center; gap: 4px; }
/* 36px, not the 44px touch minimum used elsewhere: these sit well away from
   any destructive control, a mis-tap costs one tap to undo, and 44px here
   would push the panel taller than the space this screen can spare. */
.lobby-bots-stepper .icon-btn {
  width: 36px; min-width: 36px; height: 32px; min-height: 32px;
  font-size: 17px; font-weight: 700; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.lobby-bots-count {
  min-width: 22px; text-align: center;
  font-size: 14px; font-weight: 700; color: var(--text-light);
}
.lobby-bots-stepper .icon-btn:disabled { opacity: 0.35; pointer-events: none; }


/* ---- Bot setup: count + difficulty side by side (landing screen) ---- */
.bot-setup-row { display: flex; gap: 10px; }
.bot-setup-field { flex: 1; min-width: 0; }
/* The native select is replaced by the custom .dd component at runtime (see
   wireThemedDropdowns in app.js); this keeps the two halves equal before that
   script runs, so the row does not jump on load. */
.bot-setup-field select { width: 100%; }

/* ---- Difficulty on the lobby bots row ---- */
.lobby-bots-difficulty {
  flex: 0 0 auto; width: auto; min-width: 92px;
  margin: 0; padding: 4px 8px;
  font-size: 12px; min-height: 32px;
}


/* ---- Bot trait lines ---- */
/* The characters have generic names (Bot 1, Bot 2) on purpose, so this one
   line is what carries the personality. Shown in the lobby player list and in
   the seat popover. Humans have no trait, so neither selector matches them. */
.player-trait {
  display: block; margin-top: 1px;
  font-size: 10.5px; font-style: italic;
  color: var(--text-muted);
}
.player-action-trait {
  font-size: 11.5px; font-style: italic;
  color: var(--gold-bright);
  padding: 0 2px 6px;
  border-bottom: 1px solid var(--jade-border);
  margin-bottom: 6px;
  max-width: 180px; line-height: 1.3;
}


/* ==========================================================================
   Seat scaling by player count (Oct 2026)
   ==========================================================================
   --seat-scale is set per render in renderOvalTable, from the measured gap
   between neighbouring seats. 1 at four players on a roomy phone, down to
   about 0.68 on a busy table or a short screen.

   Scaling the WIDTH alone was not enough: the overlaps at 7+ players are
   vertical, between seats stacked down the left and right sides, so the chip
   has to get shorter too. Everything inside it is therefore expressed in terms
   of the scale -- type, avatar, padding, the card fan.

   Done with real sizes rather than transform: scale(), because .seat is
   absolutely positioned with translate(-50%, -50%) and a second transform on
   the same element would fight it.
   ========================================================================== */
.oval-table { --seat-scale: 1; }

.seat-chip {
  padding: calc(5px * var(--seat-scale)) calc(7px * var(--seat-scale));
  font-size: calc(11.5px * var(--seat-scale));
}
.seat-name   { font-size: calc(11.5px * var(--seat-scale)); }
.seat-meta   { font-size: calc(10px   * var(--seat-scale)); }
.seat-id     { gap: calc(4px * var(--seat-scale)); }
.seat-stats  { gap: calc(7px * var(--seat-scale)); }
.seat-hand   { gap: calc(1px * var(--seat-scale)); }
.seat-dot    {
  width:  calc(15px * var(--seat-scale));
  height: calc(15px * var(--seat-scale));
  font-size: calc(9px * var(--seat-scale));
}
.seat-score  { font-size: calc(11.5px * var(--seat-scale)); }
/* The seat no longer draws an avatar (Oct 2026) -- see renderOvalTable. The
   rule is kept, scoped and scaled, so that if one is ever put back it inherits
   the sizing rather than reintroducing the fixed 22px that caused the overflow
   in the first place. */
/* The fan of card backs and the "Last:" strip are the two blocks that add the
   most height, so they shrink hardest on a crowded table. */
.seat-back {
  width:  calc(13px * var(--seat-scale));
  height: calc(18px * var(--seat-scale));
}
.seat-fan-count { font-size: calc(10.5px * var(--seat-scale)); }
.seat-discard-label { font-size: calc(8px * var(--seat-scale)); }
.seat-discard-history { gap: calc(2px * var(--seat-scale)); margin-top: calc(3px * var(--seat-scale)); }
.seat-discard-history .card.mini {
  width:  calc(20px * var(--seat-scale));
  height: calc(26px * var(--seat-scale));
}


/* Crowded table (revised Oct 2026).
   This used to hide the discard strip outright at 7+ players, which meant
   the busiest tables -- the ones where knowing what people are throwing
   matters most -- were the ones that stopped showing it. Now that the strip
   shares a row with the hand instead of starting a third line, it costs no
   height at all, so compact mode trims it to the two most recent cards and
   shrinks them rather than removing the information.
   Set by renderOvalTable when the measured vertical gap between neighbouring
   chairs is smaller than a full-height chip. */
.oval-table.compact-seats .seat-discard-history .card.mini:nth-child(n+3) { display: none; }
.oval-table.compact-seats .seat-discard-history .card.mini {
  width:  calc(16px * var(--seat-scale));
  height: calc(21px * var(--seat-scale));
}


/* ---- Nine and ten players -----------------------------------------------
   The compact panel above clears 6, 7 and 8 players outright. At ten it was
   still 16px into its neighbours, so this tier takes the last of the fat off:
   smaller cards, smaller numbers, and the chip's own padding reduced for the
   duration of the reveal. Nothing is removed that the compact tier kept --
   three cards, the "+N more" chip and the round value are all still there,
   just drawn tighter. Ten players on a phone is the hardest case the layout
   has, and this is what fits. */
.oval-table.tight-reveal .seat.revealing .seat-chip { padding: 3px 5px 3px; }
.oval-table.tight-reveal .seat.revealing .seat-name { font-size: calc(10px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-box { margin-top: 1px; padding: 1px; }
.oval-table.tight-reveal .seat-reveal-grid { gap: 1px; margin-top: 0; }
.oval-table.tight-reveal .seat-reveal-grid .card.mini,
.oval-table.tight-reveal .seat-reveal-more {
  width:  calc(16px * var(--seat-scale));
  height: calc(19px * var(--seat-scale));
}
.oval-table.tight-reveal .seat-reveal-grid .card.mini .center-rank { font-size: calc(8px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-grid .card.mini .center-suit { font-size: calc(6px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-more { font-size: calc(5.5px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-value { font-size: calc(9.5px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-score { font-size: calc(10.5px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-total { font-size: calc(9.5px * var(--seat-scale)); }
.oval-table.tight-reveal .seat-reveal-arrow { margin: 0 1px; font-size: calc(8.5px * var(--seat-scale)); }

/* ---- Waiting on a seat (Oct 2026) ---------------------------------------
   The table is held while a player the host has already approved watches
   their rewarded ad. button:disabled above already dims it; this is the
   in-flight state for the tap itself, so a slow reply never reads as a dead
   button -- which is what the old optimistic dismissal was hiding. */
#overlay-round-result.busy #btn-next-round {
  opacity: 0.55; pointer-events: none;
}
#btn-next-round.waiting-seat { cursor: progress; }

/* The line above the name field when the app is asking for one -- after the
   first game, or before a table others can see. Friendly, not an error. */
.name-ask-hint {
  margin: 2px 0 8px; padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: rgba(242,193,78,0.12); border: 1px solid rgba(242,193,78,0.35);
  color: var(--gold-bright); font-size: 13px; font-weight: 600; line-height: 1.35;
}
