:root {
  --felt: #0e3d2b;
  --felt-dark: #0a2c1f;
  --gold: #d9c9a3;
  --cream: #f4efe3;
  --danger: #c0392b;
  --warn: #e0a000;
  /* How big a card on the stock/discard piles is, and the margin of slop the discard's stack
     box adds around it. Declared here rather than on #piles so the breakpoints that grow the
     card can override them by specificity - on #piles they would have been the same
     specificity as the defaults and simply lost to whichever came later in the file.
     --discard-slop must equal DISCARD_SLOP_PAD in main.ts, which positions cards inside the
     stack; the stack's own size is derived from both (see .discard-stack). */
  --pile-card-w: 92px;
  --pile-card-h: 130px;
  --discard-slop: 16px;
  color-scheme: dark;
}

* { box-sizing: border-box; }


/* Author `display` rules beat the UA stylesheet's `[hidden] { display: none }`, so any
   element we style with a display AND toggle via `hidden` would silently stay visible.
   This makes `hidden` authoritative everywhere. */
[hidden] { display: none !important; }

/* Holding a card to open a gap must not also start selecting the page text underneath it -
   and holding an avatar or deck-back thumbnail to pick it must not trigger the OS's own
   "save/copy image" popup, which is exactly what a bare <img> invites on a long touch in iOS
   Safari. That's not just a table-view problem: avatar and deck-back pickers live on the
   home and setup screens too, so this applies everywhere. Inputs opt back in below. */
body {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
input, textarea {
  user-select: text; -webkit-user-select: text;
}

body {
  margin: 0;
  font-family: 'Georgia', 'Times New Roman', serif;
  background: radial-gradient(circle at top, var(--felt) 0%, var(--felt-dark) 70%);
  color: var(--cream);
  min-height: 100vh;
  /* Tilted fan cards and the sloppy discard pile intentionally splay a few px past their
     containers; never let that turn into a horizontally scrolling page. */
  overflow-x: hidden;
}

/* Outside #app on purpose, same corner the old always-on hard-refresh button used to sit -
   only shown once a table exists (see render()'s inGame check). */
/* Lives in the table's own top bar now (see .topbar-btn) rather than floating over the felt
   in a fixed corner, which was reserving a column of unusable table width. */
#btn-table-talk[hidden] { display: none; }

#table-talk-popover {
  position: fixed; top: 3rem; right: 0.5rem; z-index: 91; width: min(300px, calc(100vw - 1rem));
  background: var(--felt-dark); border: 1px solid var(--gold); border-radius: 12px;
  padding: 0.7rem; box-shadow: 0 10px 26px rgba(0,0,0,0.5);
}
#table-talk-popover .link-back { display: block; margin-bottom: 0.4rem; font-size: 0.8rem; }
#table-talk-form { display: flex; gap: 0.4rem; }
#table-talk-input {
  flex: 1 1 auto; min-width: 0; padding: 0.5rem; border-radius: 6px; border: 1px solid var(--gold);
  font-family: inherit;
}
#table-talk-form button[type=submit] {
  border: none; border-radius: 6px; padding: 0.5rem 0.8rem; cursor: pointer;
  font-family: inherit; font-weight: bold; background: var(--gold); color: #1a1a1a;
}
.table-talk-emoji-row { display: flex; flex-wrap: wrap; gap: 0.15rem; margin-top: 0.5rem; }
.table-talk-emoji {
  background: none; border: 1px solid transparent; border-radius: 6px; font-size: 1.15rem;
  cursor: pointer; padding: 0.15rem 0.3rem; -webkit-tap-highlight-color: transparent;
}
.table-talk-emoji:hover { background: rgba(255,255,255,0.1); }

/* The ephemeral bubble itself - avatar, name, and the one line, faded in and out. No log
   behind it: renderTableTalkToast is purely reactive to state.tableTalk, nothing more. */
/* Below the notch AND below the app's own top bar. A flat 3.2rem from the top of the
   viewport put this behind the status bar and address bar on a phone - you'd catch the
   bottom edge of somebody's message going past and never get to read it. env() clears
   whatever the device reserves up there, and the rest clears the table's top bar. */
/* Table talk lives in its own labelled corner of the viewer's column now, under the table
   and over the meld drawer's button - not floating across the table on a fixed position.
   Overlaid, it landed wherever there happened to be room, which on a phone was regularly off
   the top of the screen behind the safe area, or squarely on top of the cards being read.
   Here it is always in the same place, so a glance finds it and nothing covers anything. */
/* Fills the column beside the piles rather than sizing to what was said - a bubble that grew
   to fit its text had nowhere to grow but leftwards, straight under the cards. */
.talk-rail {
  display: flex; flex-direction: column; align-items: stretch; gap: 0.1rem;
  width: 100%; max-width: 100%; min-width: 0; flex: 1 1 auto; overflow: hidden;
}
.talk-rail-label { text-align: right; }
/* Always present, whether or not anyone is talking - a label that appears and disappears is
   more distracting than one that simply sits there. Pale green rather than the gold used for
   things you can press, because there is nothing here to press. */
.talk-rail-label {
  font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(150,200,150,0.55); padding-left: 0.15rem;
}
.table-talk-toast {
  display: flex; align-items: flex-start; gap: 0.3rem; min-width: 0; max-width: 100%;
  flex: 1 1 auto; overflow: hidden;
  background: rgba(10,44,31,0.92); border: 1px solid rgba(217,201,163,0.55);
  color: var(--cream); padding: 0.35rem 0.5rem; border-radius: 10px;
  animation: tableTalkPop 0.25s ease;
}
.table-talk-toast .avatar-bubble { flex: 0 0 auto; width: 18px; height: 18px; font-size: 0.7rem; }
@keyframes tableTalkPop {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.table-talk-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.table-talk-body strong { font-size: 0.64rem; color: var(--warn); line-height: 1.15; }
/* Wraps rather than truncates. Eighty characters is what the box lets somebody type, so
   eighty characters is what has to be readable - an ellipsis at forty would make the limit
   a lie. Two lines is where it stops, which at this width is comfortably more than eighty. */
.table-talk-text {
  display: flex; align-items: flex-start; gap: 0.35rem;
  font-size: 0.74rem; line-height: 1.2; overflow: hidden; white-space: normal;
  overflow-wrap: anywhere; text-align: left;
}
.table-talk-body { max-width: 100%; }
/* The discard-pickup notice's one card, shrunk into the toast line itself rather than a
   screen-covering reveal - same scale-wrapper trick the table melds use to stay small. */
.table-talk-card { flex: 0 0 auto; width: 22px; height: 31px; overflow: hidden; border-radius: 3px; }
.table-talk-card .card { transform: scale(0.239); transform-origin: top left; box-shadow: none; }

/* An inside thought, not a spoken line - no avatar, no name, no gold "somebody said this"
   border. Deliberately reads as private and a little unwelcome: desaturated, italic, a
   thought-bubble tail instead of a speech point, shown only on the one device it's about. */
.table-talk-toast--private {
  background: rgba(28,28,32,0.94); border: 1px dashed rgba(217,201,163,0.35);
  padding: 0.55rem 0.9rem;
}
.table-talk-text--private {
  font-style: italic; color: rgba(230,224,212,0.75); white-space: normal;
}



#app {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  /* --app-pad is what #table-view subtracts to fill the viewport exactly; keep them in step. */
  --app-pad: 1rem;
  padding: var(--app-pad);
  max-width: 1200px;
  margin: 0 auto;
}
/* On a phone every millimetre across matters more than a generous margin - this is width the
   placemats can use for bigger cards, and it was only ever being spent on empty felt. */
@media (max-width: 560px) {
  #app { --app-pad: 0.5rem; gap: 0.5rem; }
}

#game-screen, #setup-screen { flex: 1 1 700px; min-width: 0; }
.resume-game-btn {
  display: block; width: 100%; margin: 0 0 0.9rem; padding: 0.7rem 1rem; cursor: pointer;
  border: 1px solid var(--warn); border-radius: 10px; background: rgba(224,160,0,0.16);
  color: var(--warn); font-family: inherit; font-size: 1rem; font-weight: bold;
}
.resume-game-btn:hover { background: rgba(224,160,0,0.26); }

/* The home screen's entry point into "My active games" - only ever shown when there's
 * actually something parked (see refreshActiveTablesBadge), so it never opens onto an empty
 * list. Sits with the other home-screen links rather than competing with the two big mode
 * buttons above it. */
.active-tables-btn {
  /* Same width as the two mode buttons above it, which are capped and centred. Left at a
     plain width:100% it stretched the whole column on a desktop and made the three buttons
     look like two of one kind and one of another. */
  display: grid; grid-template-columns: 1.6rem 1fr 1.6rem; align-items: center; gap: 0.5rem;
  width: 100%; max-width: 340px; margin: 0.6rem auto 0; padding: 0.6rem 1rem; cursor: pointer;
  border: 1px solid rgba(217,201,163,0.5); border-radius: 10px; background: rgba(255,255,255,0.05);
  color: var(--cream); font-family: inherit; font-size: 0.95rem; font-weight: bold;
}
.active-tables-btn:hover { background: rgba(255,255,255,0.11); }
.active-tables-count { opacity: 0.75; font-weight: normal; }
/* Fixed-width side cells, so the label stays centred whether or not the badge is showing. */
.atb-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.2rem; font-size: 1.05rem; line-height: 1;
}
.atb-label { text-align: center; min-width: 0; }
.atb-fan svg { width: 1.6rem; height: 1.25rem; display: block; }

/* ---------- My active games ---------- */
.active-tables-section {
  text-align: left; font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase;
  opacity: 0.6; margin: 1rem 0 0.4rem;
}
.active-tables-section:first-child { margin-top: 0; }
.active-table-row {
  display: flex; align-items: center; gap: 0.6rem; text-align: left;
  padding: 0.7rem 0.8rem; margin-bottom: 0.5rem; cursor: pointer;
  border: 1px solid var(--gold); border-radius: 10px; background: rgba(255,255,255,0.05); color: var(--cream);
}
.active-table-row:hover { background: rgba(255,255,255,0.11); }
.active-table-row.is-stale { cursor: default; opacity: 0.5; border-style: dashed; }
/* The soft glow for "it's actually your turn" - a slow pulse rather than a static highlight,
 * so it reads as live/waiting-on-you rather than just another selected-looking row. */
.active-table-row.is-your-turn {
  border-color: var(--warn); background: rgba(224,160,0,0.12);
  animation: activeTableGlow 2.2s ease-in-out infinite;
}
@keyframes activeTableGlow {
  0%, 100% { box-shadow: 0 0 0 rgba(224,160,0,0); }
  50% { box-shadow: 0 0 14px rgba(224,160,0,0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .active-table-row.is-your-turn { animation: none; box-shadow: 0 0 10px rgba(224,160,0,0.45); }
}
.active-table-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.active-table-name { font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.active-table-code { font-family: monospace; opacity: 0.7; letter-spacing: 0.08em; }
.active-table-meta { font-size: 0.78rem; opacity: 0.7; }
/* How many players go before you - a plain count, not urgent-looking, since the glow above
 * is what actually signals "it's you" once that count hits zero. */
.active-table-badge {
  flex: 0 0 auto; min-width: 1.6rem; height: 1.6rem; padding: 0 0.4rem; border-radius: 999px;
  background: rgba(217,201,163,0.2); border: 1px solid rgba(217,201,163,0.5);
  display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.85rem;
}
.active-table-remove {
  flex: 0 0 auto; width: 1.8rem; height: 1.8rem; border-radius: 50%; border: none;
  background: transparent; color: var(--cream); opacity: 0.6; font-size: 1.1rem; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.active-table-remove:hover { opacity: 1; background: rgba(192,57,43,0.25); }

h1, h2, h3 { font-family: 'Palatino Linotype', Georgia, serif; }

/* ---------- Home / lobby / waiting room ---------- */
#home-screen, #lobby-screen, #room-screen, #join-request-screen {
  flex: 1 1 700px; text-align: center; padding: 1.5rem 1rem; position: relative;
}
}

/* Lake-life clutter tucked into a corner - purely decorative, never worth a tap, so it stays
   out of the way of anything a screen reader or a finger actually needs. Faint and a little
   tilted, like something left behind at the dock rather than a logo pinned to the page.
   The source crops aren't perfectly clean at their own edges (a hazard of slicing them out
   of a bigger illustrated sheet rather than each being drawn standalone) - masked to a soft
   oval that fades out well before the actual image bounds, so a corner artifact fades into
   the page instead of reading as a rectangle stuck on top of it. */
.lake-decor {
  position: absolute; opacity: 0.5; pointer-events: none; user-select: none;
  -webkit-user-drag: none; user-drag: none; z-index: 1;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, black 42%, transparent 72%);
  mask-image: radial-gradient(circle at 50% 50%, black 42%, transparent 72%);
}
.lake-decor.corner-tl { top: 0.5rem; left: 0.5rem; width: 46px; transform: rotate(-8deg); }
.lake-decor.corner-tr { top: 0.5rem; right: 0.5rem; width: 46px; transform: rotate(7deg); }
.lake-decor.corner-bl { bottom: 0.5rem; left: 0.5rem; width: 46px; transform: rotate(6deg); }
.lake-decor.corner-br { bottom: 0.5rem; right: 0.5rem; width: 46px; transform: rotate(-6deg); }
#join-request-screen .paused-card { margin-inline: auto; }
#home-screen h1 { margin: 0.1rem 0 0.4rem; }

/* Both of these are cut to their own outline with transparent surrounds, so the shadow has
   to be a drop-shadow filter - a box-shadow would trace the element's rectangle and put a
   hard edge back around art that no longer has one. */
#hero-art {
  width: 100%; max-width: 520px; height: auto; display: block; margin: 0 auto 0.5rem;
  filter: drop-shadow(0 8px 18px rgba(0,0,0,0.5));
}
/* The lockup is black line art, so it keeps a light card behind it - on the felt the
   wordmark and the tagline would disappear. Now that the art is cut to its own outline the
   card hugs the drawing instead of the original file's uneven white margin. */
/* Grown about 15% and dropped a little further down the page - it was reading small for the
   thing it is, which is the only signature on the whole app. */
#studio-logo {
  width: 100%; max-width: 300px; height: auto; display: block; margin: 2.9rem auto 0;
  border-radius: 12px; padding: 0.6rem 0.7rem;
  background: rgba(244,239,227,0.94);
  box-shadow: 0 4px 14px rgba(0,0,0,0.35);
}

/* A quiet text link, not a full button - "how to play" is worth surfacing on both the home
   screen and the tables lobby, but it shouldn't compete with the actual primary actions. */
.instructions-link {
  display: block; margin: 1rem auto 0; text-align: center; opacity: 0.85;
}
/* Both classes together beats .link-back's own font-size on specificity, not source order -
   source order alone left this stuck at .link-back's smaller size regardless of what was
   written here. */
.link-back.instructions-link { font-size: 1rem; }
#lobby-screen .instructions-link { margin: 0 0 0.6rem; text-align: left; }

/* On a phone this is one tall column, in DOM order, and that's correct - there's nowhere
   else to put it. A desktop or landscape tablet has width to spare that a single centered
   column never touches, so past this width the two groups sit side by side instead - each
   one still just stacks its own children top to bottom, sized off its own content, which is
   what keeps the hero image's height from dragging the profile row down with it (a shared
   grid row would force exactly that). */
@media (min-width: 860px) {
  #home-screen {
    display: flex; flex-direction: row; align-items: flex-start; justify-content: center;
    gap: 2.5rem; text-align: left; max-width: 960px; margin: 0 auto;
  }
  .home-branding { flex: 1 1 420px; min-width: 0; }
  .home-actions { flex: 1 1 340px; min-width: 0; text-align: center; }
  #studio-logo { margin-top: 1.4rem; }
}

/* The whole front door should land on one screenful of a phone - having to scroll to find
   the thing you came to tap is a bad first impression, and it's the screen every beta tester
   sees first. Everything here is a squeeze of the same layout rather than a different one:
   the hero and the studio lockup give up the most, being the two biggest blocks and the two
   that lose the least by being smaller. Placed after the rules it overrides, since they're
   id selectors and would otherwise win on document order alone. */
@media (max-width: 700px) {
  #home-screen { padding: 0.8rem 1rem 0.5rem; }
  /* A shade narrower than it was: the art is wider than it is tall, so twenty pixels off its
     width buys back more height than it costs presence - and it is what keeps the home screen
     clear of a scroll once Safari's own bar has taken its share of the viewport. */
  #hero-art { max-width: 296px; margin-bottom: 0.2rem; }
  #home-screen .tagline { margin-bottom: 0.8rem; font-size: 0.9rem; }
  #home-screen #mode-choice { gap: 0.45rem; }
  #home-screen .active-tables-btn { margin-top: 0.4rem; padding: 0.5rem 1rem; }
  /* Up about 17% from 168, and sat a little lower - it is the only signature on the app and
     was reading smaller than it deserves. */
  #studio-logo { max-width: 196px; margin-top: 1.15rem; padding: 0.4rem 0.5rem; }
}
/* A short phone - an SE, or anything held in landscape - has to give up more again. */
@media (max-width: 700px) and (max-height: 720px) {
  #home-screen { padding: 0.5rem 1rem 0.35rem; }
  #hero-art { max-width: 230px; }
  #home-screen .tagline { margin-bottom: 0.5rem; font-size: 0.84rem; }
  #home-screen .mode-btn { padding: 0.5rem 0.9rem; }
  #home-screen .mode-btn small { font-size: 0.72rem; }
  #studio-logo { max-width: 150px; margin-top: 0.6rem; }
}

.deck-label { font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.6; margin: 0 0 0.5rem; }

.profile-row { display: flex; gap: 0.5rem; align-items: center; max-width: 340px; margin: 0 auto; }
.profile-row input {
  flex: 1 1 auto; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--gold);
  background: #fff; color: #111; font-size: 1rem; min-width: 0;
}
#my-profile { margin-bottom: 1.6rem; }
#my-profile .avatar-picker { max-width: 340px; margin: 0.4rem auto 0; }
.profile-error { color: var(--warn); font-size: 0.8rem; min-height: 1em; margin: 0.35rem 0 0; }

#mode-choice { display: flex; flex-direction: column; gap: 0.7rem; max-width: 340px; margin: 0 auto; }
.mode-btn {
  display: flex; flex-direction: column; gap: 0.2rem; padding: 0.9rem 1rem; cursor: pointer;
  border: 1px solid var(--gold); border-radius: 10px; background: rgba(217,201,163,0.14);
  color: var(--cream); font-size: 1.02rem; font-weight: bold; font-family: inherit;
}
.mode-btn small { font-weight: normal; opacity: 0.7; font-size: 0.78rem; }
.mode-btn:hover { background: rgba(217,201,163,0.24); }

.link-back {
  background: none; border: none; color: var(--gold); cursor: pointer; font-size: 0.85rem;
  padding: 0; margin-bottom: 0.6rem; display: block; text-align: left;
}
.lobby-status { min-height: 1.2em; font-size: 0.85rem; opacity: 0.85; color: var(--warn); }
.empty-note { opacity: 0.6; font-size: 0.9rem; }

#open-tables-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.lobby-row {
  display: grid; grid-template-columns: 1fr auto; gap: 0.2rem 0.6rem; text-align: left;
  padding: 0.7rem 0.8rem; cursor: pointer; font-family: inherit;
  border: 1px solid var(--gold); border-radius: 10px; background: rgba(255,255,255,0.05); color: var(--cream);
}
.lobby-row:hover { background: rgba(255,255,255,0.11); }
.lobby-name { font-weight: bold; }
.lobby-meta { font-size: 0.78rem; opacity: 0.7; }

/* ---------- Games screen: My active games / Open tables tabs ---------- */
.games-tabs { display: flex; gap: 0.4rem; margin: 0.6rem 0 1rem; }
/* 1 1 0, not 1 1 auto: sized from nothing rather than from their own labels, so the two are
   always exactly half each. On auto they were sized in proportion to their text, which is one
   of the two reasons these jumped when you switched between them. */
.games-tab {
  flex: 1 1 0; min-width: 0; padding: 0.55rem 0.6rem; cursor: pointer; font-family: inherit;
  font-size: 0.88rem; font-weight: bold; color: var(--cream); opacity: 0.6;
  border: 1px solid rgba(217,201,163,0.4); border-radius: 10px; background: rgba(255,255,255,0.04);
}
.games-tab:hover { opacity: 0.85; }
.games-tab.is-active { opacity: 1; border-color: var(--gold); background: rgba(255,255,255,0.1); }

/* ---------- "List this table" public/private checkbox on table setup ---------- */
.public-table-toggle {
  display: flex; align-items: flex-start; gap: 0.55rem; cursor: pointer;
  margin: 0.8rem 0; font-size: 0.82rem; line-height: 1.35;
}
.public-table-toggle input[type=checkbox] {
  margin-top: 0.15rem; flex: 0 0 auto; -webkit-tap-highlight-color: transparent;
}
.public-table-toggle strong { color: var(--warn); }
.lobby-code {
  grid-row: 1 / span 2; align-self: center; font-family: monospace; font-size: 1.15rem;
  letter-spacing: 0.12em; color: var(--warn);
}

.table-name-label {
  display: block; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
  opacity: 0.6; margin: 0 0 0.35rem;
}
#new-table-name {
  width: 100%; padding: 0.55rem; margin-bottom: 0.8rem; border-radius: 8px;
  border: 1px solid var(--gold); background: #fff; color: #111; font-family: inherit;
  font-size: 0.95rem; box-sizing: border-box;
}
#btn-to-lobby { display: block; }

/* The two halves of the Tables screen - join somebody's table, or start your own. Boxed and
   headed so each reads as a whole answer rather than as the next field down a single form. */
/* The mark at the top of the Tables page, where the lantern used to hang. Deliberately small
   - the page is about the two controls under it. */
.page-wordmark {
  display: block; width: min(190px, 52%); height: auto; margin: 0.2rem auto 0.4rem;
  opacity: 0.95;
}
.lobby-option {
  padding: 0.85rem 0.9rem; border-radius: 12px;
  border: 1px solid rgba(217,201,163,0.3); background: rgba(0,0,0,0.16);
}
.lobby-option-head {
  margin: 0 0 0.45rem; font-size: 1rem; color: var(--gold); letter-spacing: 0.01em;
}
.lobby-option .join-form { margin-bottom: 0; }
.lobby-option .room-hint { margin-top: 0; }
/* A rule across the screen with "or" sitting in the gap - the one word that says these are
   alternatives and not steps. */
.lobby-split {
  display: flex; align-items: center; gap: 0.7rem;
  margin: 0.9rem 0; color: rgba(217,201,163,0.55);
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.16em;
}
.lobby-split::before, .lobby-split::after {
  content: ''; flex: 1 1 auto; height: 1px; background: rgba(217,201,163,0.25);
}
.join-form { display: flex; gap: 0.4rem; margin-bottom: 1rem; }
.join-form input {
  flex: 1 1 auto; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--gold);
  background: #fff; color: #111; text-transform: uppercase; letter-spacing: 0.18em;
  font-family: monospace; font-size: 1.05rem; text-align: center; min-width: 0;
}
.join-form button {
  padding: 0.6rem 1rem; border-radius: 8px; border: none; background: var(--gold);
  color: #1a1a1a; font-weight: bold; cursor: pointer;
}

/* The tip jar. Deliberately the quietest thing on the page - a saucer under the controls,
   not a banner over them. Warm rather than gold so it reads as an aside, and it never grows
   into a call to action: if it competes with "Start a new table" it has gone wrong. */
.tip-jar {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 1.6rem auto 0.4rem; padding: 0.55rem 0.8rem; max-width: 22rem;
  border: 1px dashed rgba(217,201,163,0.3); border-radius: 999px;
  background: rgba(217,201,163,0.05);
  color: inherit; text-decoration: none;
  transition: background 140ms ease, border-color 140ms ease;
}
.tip-jar:hover, .tip-jar:focus-visible {
  background: rgba(217,201,163,0.11); border-color: rgba(217,201,163,0.5);
}
.tip-jar-cup { font-size: 1.3rem; line-height: 1; flex: 0 0 auto; }
.tip-jar-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.tip-jar-text strong { font-size: 0.85rem; font-weight: 600; opacity: 0.9; }
.tip-jar-text small { font-size: 0.72rem; opacity: 0.6; }

/* The follow line, quieter again than the tip jar - it sits under an aside, so it has to be
   the smaller of the two or the page ends up with two competing asks under the one button
   that matters. No box, no border: just a line of type with a glyph. */
.follow-slot { margin: 0.15rem 0 0; text-align: center; }
.follow-line {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: 0.74rem; letter-spacing: 0.01em; opacity: 0.55;
  color: inherit; text-decoration: none;
}
.follow-glyph { flex: 0 0 auto; }
/* The two outward links as one aside rather than two. Wraps on a narrow phone so the pair
   stacks instead of overflowing, and the separator goes with the wrap. */
.follow-pair {
  display: inline-flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 0.1rem 0.45rem;
}
.follow-sep { opacity: 0.4; font-size: 0.7rem; }
/* Live: it can be tapped, so it is allowed to respond. */
a.follow-line:hover, a.follow-line:focus-visible { opacity: 0.9; text-decoration: underline; }
/* Not live yet: no pointer, no hover, nothing to tap. It reads as a caption rather than a
   promise, which is the honest thing to show while the account does not exist. */
.follow-line.is-soon { cursor: default; opacity: 0.42; }
.instructions-signoff .follow-line { margin-top: 0.5rem; }

/* Not an error - nothing has gone wrong and nothing the player did caused it. A caution,
   in the colour the app already uses for "worth knowing", saying what to do about it. */
.local-save-warning {
  margin: 0.3rem 0; padding: 0.45rem 0.65rem; border-radius: 8px;
  background: rgba(224,160,0,0.14); border: 1px solid rgba(224,160,0,0.45);
  color: var(--cream); font-size: 0.8rem; line-height: 1.35;
}

.room-code-line { font-size: 1.1rem; }
.room-code-line strong { font-family: monospace; font-size: 1.7rem; letter-spacing: 0.22em; color: var(--warn); }
.room-hint { font-size: 0.82rem; opacity: 0.7; margin-top: -0.4rem; }

.share-btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.6rem 1rem; margin: 0.2rem 0 0.6rem; cursor: pointer;
  border: 1px solid var(--gold); border-radius: 999px;
  background: rgba(255,255,255,0.06); color: var(--cream);
  font-family: inherit; font-size: 0.95rem;
}
.share-btn:hover { background: rgba(255,255,255,0.13); }
.share-icon { font-size: 1.05rem; }
#share-label { overflow-wrap: anywhere; text-align: left; }

/* ---------- Number-of-players pickers ---------- */
/* Nine buttons, thumb-sized, wrapping - never a typed number. */
/* A keypad, not a wrapping pile: 2-6 on top, 7-10 underneath. */
.seat-picker {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.35rem;
  max-width: 320px; margin: 0 auto;
}
.seat-choice {
  min-width: 2.6rem; padding: 0.5rem 0; cursor: pointer;
  border: 1px solid rgba(217,201,163,0.45); border-radius: 9px;
  background: rgba(255,255,255,0.05); color: var(--cream);
  font-family: inherit; font-size: 1.05rem; font-weight: bold; line-height: 1;
}
.seat-choice:hover { background: rgba(255,255,255,0.12); }
.seat-choice.chosen {
  border-color: var(--warn); background: rgba(224,160,0,0.18); color: var(--warn);
}
.deck-note { font-size: 0.8rem; opacity: 0.65; margin: 0.5rem 0 0.2rem; text-align: center; }
#host-seat-picker { margin-bottom: 0.9rem; }
#room-seats { display: flex; flex-direction: column; gap: 0.4rem; margin: 1rem 0; }
.room-seat {
  display: flex; align-items: center; gap: 0.5rem; text-align: left;
  border: 1px solid rgba(217,201,163,0.4); border-radius: 8px; padding: 0.45rem 0.6rem;
}
.room-seat.is-me { border-color: var(--warn); background: rgba(224,160,0,0.1); }
.you-tag { margin-left: auto; font-size: 0.72rem; color: var(--warn); }

/* ---------- Custom match: pick which rounds, and in what order ---------- */
.round-picker-wrap {
  text-align: left; margin: 0.8rem 0; border: 1px solid rgba(217,201,163,0.35);
  border-radius: 10px; padding: 0.15rem 0.8rem;
}
.round-picker-wrap[open] { padding-bottom: 0.7rem; }
.round-picker-wrap summary {
  cursor: pointer; padding: 0.6rem 0; font-weight: bold; font-size: 0.9rem;
  color: var(--gold); list-style: none;
}
.round-picker-wrap summary::-webkit-details-marker { display: none; }
.round-picker-wrap summary::before { content: '▸ '; }
.round-picker-wrap[open] summary::before { content: '▾ '; }
.round-picker-hint { font-size: 0.78rem; opacity: 0.65; margin: 0 0 0.6rem; }
.attentive-toggle {
  display: flex; align-items: flex-start; gap: 0.55rem; cursor: pointer;
  margin-top: 0.7rem; padding-top: 0.7rem; border-top: 1px solid rgba(217,201,163,0.2);
  font-size: 0.82rem; line-height: 1.35;
}
.attentive-toggle input[type=checkbox] {
  margin-top: 0.15rem; flex: 0 0 auto; -webkit-tap-highlight-color: transparent;
}
.attentive-toggle strong { color: var(--warn); }

/* The Jokers Wild explainer. It only exists while the box is ticked - somebody who leaves the
   setting alone should never have to read four paragraphs about a mode they didn't ask for.
   Indented under the toggle it belongs to so it reads as that toggle's small print rather than
   as the next setting down, and given the notice treatment (tinted, gold rule) so it's clearly
   the app talking and not another thing to configure. */
.jokers-note {
  margin: 0.5rem 0 0 1.9rem; padding: 0.55rem 0.7rem;
  border-left: 2px solid var(--gold); border-radius: 0 6px 6px 0;
  background: rgba(217,201,163,0.08);
  font-size: 0.78rem; line-height: 1.4;
}
.jokers-note p { margin: 0 0 0.45rem; }
.jokers-note p:last-child { margin-bottom: 0; }
.jokers-note strong { color: var(--gold); }

/* ---------- Turn timer: duration slider + off switch ---------- */
/* Shared by the setup form, the online room's form, and the live host-only control in the
   table menu - same markup, same look, wherever it appears. */
.timer-settings {
  margin-top: 0.7rem; padding-top: 0.7rem; border-top: 1px solid rgba(217,201,163,0.2);
}
.timer-settings-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 0.82rem; font-weight: bold; color: var(--gold); margin-bottom: 0.4rem;
}
.timer-off-switch {
  display: flex; align-items: center; gap: 0.3rem; font-weight: normal;
  font-size: 0.78rem; opacity: 0.85; cursor: pointer;
}
.timer-off-switch input[type=checkbox] { -webkit-tap-highlight-color: transparent; }
/* Sound on/off, sitting with the other table-menu controls. Sized to match the buttons above
   it rather than the small print inside the timer panel - it's a setting in its own right,
   not a modifier on something else. */
/* The table's own invite, kept as one block so the code and the share button read as a pair
   rather than two unrelated menu rows. */
.menu-share { display: flex; flex-direction: column; gap: 0.5rem; }
.menu-share-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  font-size: 0.8rem; opacity: 0.85;
}
/* Monospace and spaced out, because the usual reason this is on screen is somebody reading it
   down a phone to somebody else. */
.menu-share-head strong {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.1rem; letter-spacing: 0.12em; color: var(--gold);
}
.menu-share-btn {
  width: 100%; padding: 0.55rem 0.8rem; border: none; border-radius: 8px;
  background: var(--gold); color: #1a1a1a; font-family: inherit; font-weight: bold;
  font-size: 0.88rem; cursor: pointer;
}
.menu-share-btn:hover { filter: brightness(1.08); }
.sound-switch {
  display: flex; align-items: center; gap: 0.6rem; cursor: pointer;
  color: var(--cream); font-size: 0.92rem; min-height: 46px;
  -webkit-tap-highlight-color: transparent;
}
.sound-switch input[type=checkbox] {
  width: 1.05rem; height: 1.05rem; accent-color: var(--gold); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.timer-settings input[type=range] {
  width: 100%; accent-color: var(--gold); cursor: pointer;
}
.timer-settings input[type=range]:disabled { opacity: 0.4; cursor: not-allowed; }
#timer-limit-readout, #room-timer-limit-readout, #setup-timer-limit-readout {
  font-size: 0.76rem; opacity: 0.7; text-align: right; margin-top: 0.15rem;
}
/* The live control in the table menu sits inside a dark slide-over panel already, so it
   doesn't need its own divider/heading treatment on top of that. */
#table-menu-buttons .timer-settings { margin-top: 0.3rem; padding-top: 0; border-top: none; }
/* Leads the two "Customize the match" accordions (room + setup) - nothing sits above it
   there, so the usual divider line would just border the summary heading for no reason. */
summary + .timer-settings { margin-top: 0.3rem; padding-top: 0; border-top: none; }
.round-picker-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.2rem; border-bottom: 1px solid rgba(217,201,163,0.15);
}
.round-picker-row:last-child { border-bottom: none; }
.round-picker-row.excluded { opacity: 0.45; }
.round-picker-check {
  display: flex; align-items: center; gap: 0.5rem; flex: 1 1 auto; cursor: pointer; font-size: 0.9rem;
}
.round-picker-move { display: flex; gap: 0.3rem; flex: 0 0 auto; }
.round-picker-move button {
  width: 1.8rem; height: 1.8rem; border-radius: 6px; border: 1px solid rgba(217,201,163,0.35);
  background: rgba(255,255,255,0.04); color: var(--cream); font-family: inherit;
  cursor: pointer; -webkit-tap-highlight-color: transparent; outline: none;
}
.round-picker-move button:focus-visible { outline: 2px solid var(--warn); outline-offset: 1px; }
.round-picker-move button:disabled { opacity: 0.25; cursor: not-allowed; }
.round-picker-move button:not(:disabled):hover { background: rgba(255,255,255,0.1); }
.round-picker-check input[type=checkbox] { -webkit-tap-highlight-color: transparent; }

.your-turn { color: var(--warn); font-weight: bold; }

/* The old 2rem top pad was most of the dead space above the fold - the bar wants to sit as
   high as the notch allows, the same as it does on the lobby. Same gutter on both, so the
   two ways into a game line up with each other rather than each finding their own margin. */
#setup-screen { text-align: center; padding: 0.6rem 1rem 2rem; }
#lobby-screen { padding: 0.6rem 1rem 2rem; }
#setup-screen h1 { margin: 0.1rem 0 0.4rem; }
.studio-mark {
  margin: 0; font-size: 0.78rem; letter-spacing: 0.18em; text-transform: uppercase;
  opacity: 0.65; color: var(--gold);
}
.tagline { opacity: 0.8; margin-bottom: 2rem; }
#setup-form { display: flex; flex-direction: column; gap: 0.75rem; max-width: 360px; margin: 0 auto; }
#setup-form input[type="text"], #setup-form input[type="number"] {
  padding: 0.5rem; border-radius: 6px; border: 1px solid var(--gold); background: #fff; color: #111;
}
/* The seat picker brings its own look; this is for the name rows and "Deal it in". Also
   excludes .avatar-btn - each seat's avatar picker button is a <button> in here too, and
   without the exclusion this rule's own padding/background/border-radius beat .avatar-btn's
   (an ID-qualified selector always outranks a plain class one), which is what was squashing
   the per-seat avatars down to tiny art lost inside an oversized tan square instead of the
   same clean circle the home screen's profile avatar uses. */
#setup-form button:not(.seat-choice):not(.avatar-btn) {
  padding: 0.7rem; border-radius: 8px; border: none; background: var(--gold); color: #1a1a1a;
  font-weight: bold; cursor: pointer; font-size: 1rem;
}
#setup-form button:not(.seat-choice):not(.avatar-btn):hover { filter: brightness(1.08); }

/* ---------- Avatars ---------- */
.avatar-bubble {
  width: 26px; height: 26px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.12); border: 1px solid rgba(217,201,163,0.6);
  font-size: 0.95rem; object-fit: cover; vertical-align: middle;
}
.avatar-bubble.big { width: 44px; height: 44px; font-size: 1.5rem; }

.player-seat { display: flex; align-items: center; gap: 0.5rem; position: relative; flex-wrap: wrap; }
.player-seat input[type=text] { flex: 1 1 auto; min-width: 0; }
.avatar-btn {
  width: 42px; height: 42px; border-radius: 50%; flex: 0 0 auto; cursor: pointer;
  border: 1px solid var(--gold); background: rgba(255,255,255,0.1); font-size: 1.3rem;
  display: inline-flex; align-items: center; justify-content: center; overflow: hidden; padding: 0;
  position: relative;
}
/* Picking an avatar pops it big, then settles it back home - the moment should feel like it
   landed, not like the art just silently swapped underneath the button. Scaling the whole
   button (not just the image inside it) means the clipped-circle border pops up along with
   the art rather than the art overflowing its own frame. */
@keyframes avatarPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.7); }
  65%  { transform: scale(0.92); }
  100% { transform: scale(1); }
}
.avatar-btn.avatar-pop { animation: avatarPop 0.5s cubic-bezier(0.2,1,0.3,1) both; z-index: 20; }
@media (prefers-reduced-motion: reduce) {
  .avatar-btn.avatar-pop { animation: none; }
}
/* The click handler is on the button, never the image, so an inert click-through image never
   gives Android/desktop browsers anything to pop a native save/share menu over. */
.avatar-btn img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; -webkit-user-drag: none; user-drag: none; }
/* 21 avatars, 7 rows of 3 - a count picked specifically so this grid always divides evenly.
   Portraits fill their cell (not a fixed pixel size), so they read as big as the picker's own
   width allows - the fun art is the whole point of choosing one, and they only get smaller
   from here once a seat is holding it during play. */
.avatar-picker {
  flex: 1 1 100%; display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.4rem;
  background: rgba(0,0,0,0.35); border: 1px solid var(--gold); border-radius: 8px; padding: 0.5rem;
}
.avatar-choice {
  font-size: 1.2rem; background: none; border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; padding: 0.15rem;
}
.avatar-choice:hover { background: rgba(255,255,255,0.14); }
.avatar-choice.art { padding: 3px; border-radius: 12px; line-height: 0; }
.avatar-choice.art img {
  width: 100%; height: auto; aspect-ratio: 1; border-radius: 10px; display: block;
  pointer-events: none; -webkit-user-drag: none; user-drag: none;
}
.avatar-choice.art:hover { background: rgba(255,255,255,0.2); }

#reveal-guard {
  text-align: center; padding: 3rem 1rem; border: 2px dashed var(--gold); border-radius: 12px; margin-top: 2rem;
}
#reveal-guard-title { display: flex; align-items: center; justify-content: center; gap: 0.5rem; flex-wrap: wrap; }
#reveal-guard button {
  margin-top: 1rem; padding: 0.7rem 1.4rem; border-radius: 8px; border: none;
  background: var(--gold); color: #1a1a1a; font-weight: bold; cursor: pointer; font-size: 1.05rem;
}

/* Table view is exactly one viewport tall: the top section scrolls internally, the hand
   dock at the bottom never moves, so your hand never requires scrolling to reach. */
#table-view {
  display: flex; flex-direction: column;
  height: calc(100dvh - var(--app-pad) * 2); max-height: calc(100dvh - var(--app-pad) * 2);
}
/* The actual table, as an illustrated lake scene rather than a flat CSS green - the darkening
   overlay keeps the same contrast the felt color used to give cream text and cards, so nothing
   else needed retuning once real art replaced the color underneath it. */
#table-scroll {
  flex: 1 1 auto; overflow-y: auto; min-height: 0; padding-right: 2px;
  background-image: linear-gradient(rgba(6,26,18,0.55), rgba(6,26,18,0.55)), url('/assets/brand/table-canvas.png');
  background-size: cover; background-position: center; background-repeat: no-repeat;
  border-radius: 12px;
}
#hand-dock {
  flex: 0 0 auto; padding: 0.4rem 0 0.3rem;
  background: var(--felt-dark);
  /* Covers the gaps between cards and the track background too, not just the cards
     themselves - see the gesturestart/touchstart backstop in main.ts for why touch-action
     alone still isn't the whole story on iOS Safari. */
  touch-action: none;
}
/* Freed up from the big round header this used to sit under - the round/turn context that
   ate the top of the screen now lives right here, where you're already looking. */
/* Bumped up from 0.92rem now that it's the only place the round's requirement text shows -
   the lay-down panel below used to repeat it in its own heading (see .lay-down-handle). */
/* Three parts, spaced apart rather than run together: which level you're on hard left, what
   that level asks for dead centre (the bit actually read most often), whose turn it is hard
   right. Mixed numbers and words in one sentence read as a jumble. */
/* The outer two parts sit hard left and hard right by design, which on a phone put them
   flush against the glass - so the line gets a sliver of inset of its own. The cards below
   deliberately don't: the fan wants every pixel of width it can get. */
#hand-area h3 {
  margin: 0 0 0.25rem; padding-inline: 0.5rem;
  font-size: 1.02rem; font-weight: 600; opacity: 0.95;
  display: flex; align-items: baseline; gap: 0.5rem;
}
#hand-area h3 .ht-level { flex: 0 0 auto; opacity: 0.85; font-size: 0.86rem; font-weight: normal; }
#hand-area h3 .ht-req { flex: 1 1 auto; text-align: center; }
#hand-area h3 .ht-turn { flex: 0 0 auto; text-align: right; font-size: 0.9rem; }
#hand-area h3 .your-turn { color: var(--warn); font-weight: bold; }
#hand-area h3 .next-up-tag { color: var(--gold); opacity: 0.8; }
@media (max-width: 420px) {
  #hand-area h3 { font-size: 0.94rem; gap: 0.35rem; }
  #hand-area h3 .ht-level { font-size: 0.76rem; }
}

/* A real bar across the top of the table rather than controls floated over the felt. Keeping
   them off the table is what lets the placemats below use the full width - the old layout
   reserved a strip of dead space down the right for a fixed-position chat button. */
#table-topbar {
  display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.4rem;
  /* Even padding all round now. It used to reserve 2.6rem on the right for a fixed
     hard-refresh circle that lived in that corner; that button moved into this very bar long
     ago, and the reserve was just leaving the chat bubble stranded short of the edge while
     everything else sat flush. */
  padding: 0.25rem 0.4rem;
  background: rgba(4,20,14,0.55); border-radius: 10px;
  border: 1px solid rgba(217,201,163,0.16);
}
.topbar-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem;
  min-width: 2rem; height: 1.9rem; padding: 0 0.5rem; flex: 0 0 auto;
  background: rgba(217,201,163,0.12); border: 1px solid rgba(217,201,163,0.45);
  border-radius: 8px; color: var(--cream); font-family: inherit;
  font-size: 0.9rem; line-height: 1; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.topbar-btn:hover { background: rgba(217,201,163,0.24); }
.topbar-btn:focus-visible { outline: 2px solid var(--warn); outline-offset: 2px; }
.topbar-btn-wide { font-size: 0.76rem; font-weight: bold; }
/* The masthead: home on the far left, the game's name beside it, then everything you might
   want to open pushed to the other end - scoreboard, and the chat bubble hard right. The
   spacer is the wordmark's own margin, so the two buttons stay welded to the right edge
   whatever the name's width turns out to be. */
.table-wordmark {
  flex: 0 1 auto; margin-right: auto; min-width: 0;
  font-size: 1.02rem; font-weight: bold; letter-spacing: 0.03em;
  color: var(--gold); text-shadow: 0 1px 2px rgba(0,0,0,0.5);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-left: 0.15rem;
}
/* Ready for the brush-lettered wordmark the moment there's a spelling-correct file to point
   at: an <img> in this slot sizes itself to the bar's height and keeps its own aspect. */
.table-wordmark img {
  display: block; height: 1.5rem; width: auto; max-width: 100%; object-fit: contain;
}
@media (max-width: 380px) {
  .table-wordmark { font-size: 0.9rem; }
  .table-wordmark img { height: 1.25rem; }
}

/* Sits on the seam between the scrolling table above and the fixed hand dock below, thin
   since each seat's own turn highlight now carries most of "whose go is it" on its own. */
#timer-bar-wrap {
  position: relative; height: 4px; background: rgba(255,255,255,0.15); flex: 0 0 auto;
}
#timer-bar {
  height: 100%; width: 100%; background: #cfe8dd;
  transition: width 0.25s linear, background 0.5s ease;
}
#timer-bar.warming { background: #e8912b; }   /* 40s left: you're on the clock now */
#timer-bar.urgent  { background: #cc2f22; }   /* 15s left: the loon is going under */
/* No clock at all: a plain, unmoving line rather than a bar that just stopped shrinking -
   deliberately calm and a different shade from "full time left" so it reads as parked on
   purpose, not mid-countdown. The canoe has nothing to ride along, so it's hidden outright. */
#timer-bar-wrap.timer-off #timer-bar { background: rgba(217,201,163,0.4); }
#timer-bar-wrap.timer-off #timer-marker { display: none; }
/* Rides the boundary between elapsed and remaining time, same position as the bar's own
   trailing edge - a little canoe paddling along the line rather than just a bar shrinking. */
#timer-marker {
  position: absolute; top: 50%; --marker-half: 13px;
  width: calc(var(--marker-half) * 2); height: auto;
  transform: translate(-50%, -50%); pointer-events: none;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6));
  transition: left 0.25s linear;
}
/* An actual number, because a shrinking bar is easy to lose track of when you're
   concentrating on your cards - and then it's suddenly nearly gone. */
#turn-clock {
  position: absolute; right: 0.4rem; top: 100%;
  font-size: 0.68rem; font-weight: bold; color: var(--warn);
  font-variant-numeric: tabular-nums; line-height: 1.3;
  background: var(--felt-dark); padding: 0 0.3rem; border-radius: 0 0 5px 5px;
}
#turn-clock[hidden] { display: none; }

/* A forced choice, not a passive notice - covers the hand itself (JS sizes it to #hand-area's
   own rect) so idly reorganizing cards can't stand in for actually deciding. The action bar
   below stays uncovered on purpose, so an already-selected discard can still go through. */
.more-time-offer {
  position: fixed; left: 0; right: 0;
  /* top/height are set in JS to match #hand-area exactly. */
  z-index: 88; display: flex; align-items: center; justify-content: center;
  background: rgba(4,20,14,0.93); backdrop-filter: blur(3px);
  opacity: 0; transition: opacity 0.2s ease;
}
.more-time-offer.showing { opacity: 1; }
.more-time-card {
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  padding: 1rem 1.2rem; max-width: 90vw;
  background: rgba(8,30,24,0.97); border: 1px solid var(--warn); border-radius: 14px;
  box-shadow: 0 8px 22px rgba(0,0,0,0.5);
}
.more-time-avatar {
  width: 44px; height: 44px; border-radius: 50%; object-fit: cover;
  border: 1px solid rgba(217,201,163,0.6);
}
.more-time-avatar.emoji {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.6rem; background: rgba(255,255,255,0.12);
}
.more-time-text { font-size: 0.92rem; color: var(--cream); text-align: center; }
.more-time-btn {
  border: none; border-radius: 999px; cursor: pointer; font-family: inherit;
  padding: 0.6rem 1.1rem; font-size: 0.9rem; font-weight: bold; color: #1a1a1a;
  background: linear-gradient(160deg, #f6d98c 0%, var(--gold) 60%, #c99b2e 100%);
}
.more-time-btn:hover { filter: brightness(1.07); }

/* One box per seat - name, stats, and (once they've laid down) their melds together, so the
   strip reads like the actual arrangement at the table instead of two separate accountings
   of the same players. */
#table-seats {
  display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.5rem;
}
/* The ring goes round every player at the table, in the order play travels through them - one
   unbroken run, wrapping to a second row past two of them. The viewer has a placemat in it
   like anybody else, as near the middle of the near row as the columns allow, so at ten
   players the table is two rows of five with you dead centre of the near side. Where you sit
   is said by the two gold arrows either side of your own mat (see .arrow-mine), which cost no
   width because every turn arrow rides on a seat. Only the smallest tables - one or two
   opponents, no near row to seat you in - still mark your place with the seam below instead
   (see .you-marker). */
/* Both rows live in one horizontally scrolling strip, so the two halves of the table can
   never drift out of step with each other - and .ring-body being max-content wide is what
   makes them scroll as one piece rather than each finding its own offset. Past five players
   there is simply more table than a phone is wide, and the answer is to let the cards be big
   enough to read and let the table run off the edge, not to shrink everyone to fit. */
.ring-scroll {
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.ring-scroll::-webkit-scrollbar { display: none; }
.ring-body {
  position: relative;
  width: max-content; min-width: 100%;
  display: flex; flex-direction: column; gap: var(--ring-row-gap, 0.3rem);
  /* Room for the badges hanging off the outermost avatar's left edge - without it the strip's
     own overflow clips the card count of whoever happens to be first in the row. Only the left
     needs it; nothing overhangs a seat's right.
     Top and bottom are for the active player's avatar, which swells out past the rim of its
     own row (see .seat.current-turn .avatar-bubble). The strip has to clip on one axis to
     scroll on the other - overflow-y: visible isn't a thing next to overflow-x: auto - so the
     only way that avatar keeps its round bottom edge is to give the strip somewhere to put
     it. Kept to exactly the overhang; any more is table height spent on nothing. */
  padding-left: 0.65rem; padding-right: 0.2rem;
  padding-top: 8px; padding-bottom: 8px;
}
.ring-far, .ring-near {
  display: flex; align-items: flex-start; justify-content: center; gap: 0.5rem;
}
/* One row defines the resting places, or the two rows' snap points fight each other. Centred
   rather than leading-edge: a table you are meant to be in the middle of should settle with
   somebody in the middle of the screen. */
.ring-far .seat { scroll-snap-align: center; }
/* Everyone opposite has their cards laid out in front of them, pointing back at the middle -
   so the near row turns round: cards toward the centre, name and face out on the near rim.
   column-reverse is the whole trick; the seat is already a column, so nothing but the order
   changes and no seat resizes. */
.ring-near .seat { flex-direction: column-reverse; }
/* The two turns of the ring, in the table's own far corners where the near row and the far
   row join: up the left-hand end, down the right-hand end. Vertically centred across both
   rows, which is exactly the seam between them.

   Printed on the felt rather than given room of their own - they lie UNDER the place settings
   (hence the z-index pair below, and their being rendered before the rows), so they cost the
   cards not one pixel of width. Every hand opens with nothing laid down, which is exactly
   when somebody is getting their bearings and when these are wholly visible; by the time
   cards cover a corner of one, the shape of the table has already been learned. A touch
   bigger and softer than the between-seat arrows to read as part of the table rather than as
   another label on it. They scroll with the table, because they belong to its corners and not
   to the edge of the view. */
/* The frame the strip scrolls inside. It exists so the two end turns can be pinned to the
   edges of the view rather than to the ends of the table's own content - see renderRing. */
.ring-frame { position: relative; }
.ring-end {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 0;
  font-size: 1.3rem; line-height: 1; pointer-events: none;
  color: rgba(217,201,163,0.3);
}
/* Everything dealt onto the table lies on top of what's printed on it. */
#table-seats .seat { z-index: 1; }
.ring-end-left { left: 0; }
.ring-end-right { right: 0; }

/* Where you fit in, on the two smallest tables only - one or two opponents means a single far
   row and no near row to seat you in, so your place is a marked seam directly above your own
   piles and hand. Play arrives from the player on your right, then leaves again to the player
   on your left; both are named in the tooltips rather than drawn, because the seam is a fact
   about you and not about where they happen to be sitting. From four players up you have a
   real placemat instead and this isn't rendered at all. */
.you-seam {
  display: flex; align-items: center; justify-content: center; gap: 0.3rem;
  margin-top: 0.15rem;
  /* Sized to its own content and centred, rather than stretched across the strip: a
     full-width box hangs off the end and gives the whole page a horizontal scrollbar. */
  align-self: center; width: max-content;
}
.you-seam-label {
  font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: rgba(217,201,163,0.28);
}
/* Lit only while you're actually looking at your own place. The markers point at the two
   chairs either side of you; scrolled somewhere else on the table they'd be pointing at
   whoever happens to be on screen, which is a lie - so they go quiet and light back up the
   moment you're seated between the right two people again. Toggled live as the strip moves,
   not just on render (see markSeamAtHome). */
.you-marker, .you-seam-label {
  transition: color 0.25s ease, text-shadow 0.25s ease, opacity 0.25s ease;
}
.you-marker {
  flex: 0 0 auto; font-size: 1.15rem; line-height: 1;
  color: rgba(217,201,163,0.3); text-shadow: none;
}
.you-seam.at-home .you-marker {
  color: rgba(224,160,0,0.9); text-shadow: 0 0 9px rgba(224,160,0,0.45);
}
.you-seam.at-home .you-seam-label { color: rgba(224,160,0,0.75); }

/* Which way play moves round the table, said with a glance rather than a label. Faint on
   purpose - a hint you'd notice if you looked, not something competing with the seats. It
   rides ON the seat rather than standing between two of them, so direction costs no width at
   all: at nine players on a phone, three arrows abreast were the difference between every
   place setting being readable and every place setting being 15px narrower. It parks at the
   far end of the name, level with the top of the avatar - and on the near row, whose seats
   are turned round, that lands it under the name instead, which is the same place relative
   to the person. */
.turn-arrow {
  position: absolute; z-index: 2; pointer-events: none;
  font-size: 1.05rem; line-height: 1; color: rgba(217,201,163,0.4);
}
/* Both directions hang off the same edge - the right-hand one of the left-hand column in a
   pair - so the top row's arrow and the bottom row's arrow for the same gap land in the same
   vertical plane, one above the other, pointing opposite ways. Only the glyph differs. */
.turn-arrow { right: 0.05rem; }
.ring-far .turn-arrow { top: 0.42rem; }
.ring-near .turn-arrow { bottom: 0.42rem; }
/* Only the seats that actually carry one pay for the room. */
.seat.has-arrow .seat-head { padding-right: 0.85rem; }
#table-seats.seats-lg .turn-arrow { font-size: 1.3rem; }
#table-seats.seats-xl .turn-arrow { font-size: 1.5rem; }
/* The two hops that touch your own placemat - play arriving from the player on your right,
   then leaving again to the player on your left. Picked out in gold so where you're sitting
   reads at a glance from anywhere on the strip, without a separate marker taking up room.
   They're ordinary turn arrows in every other respect, which is what keeps them honest: they
   come straight off the ring path, so they can only ever point where play really goes. */
.turn-arrow.arrow-mine {
  color: rgba(224,160,0,0.9); text-shadow: 0 0 9px rgba(224,160,0,0.4);
}
/* Your own place at the table. Drawn exactly like everybody else's - same avatar, same name,
   same badges, same melds once you're down - with only a whisper of a rim to find it by, so
   it reads as one of the seats rather than as a control panel. */
.seat.is-you { border-color: rgba(224,160,0,0.22); }
.seat.is-you.current-turn { border-color: rgba(224,160,0,0.75); }
/* No box around a place at the table - cards just lie on the felt, the way they do in life.
   Dropping the border/fill/padding is what buys the cards the room to be bigger, and whose
   turn it is is said by a spotlight instead (see .seat.current-turn). */
.seat {
  background: none; border: 1px solid transparent; border-radius: 10px;
  padding: 0.15rem 0.2rem; font-size: 0.78rem; cursor: default;
  display: flex; flex-direction: column; gap: 0.1rem; min-width: 0;
  position: relative; flex: 0 0 auto;
}
.seat.has-melds { cursor: zoom-in; }
/* Ruled lines on the felt, the way a real table has seams in it. Barely there on purpose -
   they only need to say "these are the places, and this is where one ends and the next
   begins", which is the thing a wide desktop window loses when the same seats float in a lot
   of empty green. Cards landing on top of one is fine; a placemat's melds sit where they sit.

   Drawn as a rule in the GAP after each seat, not as a border on the seat, so they cost no
   width and can never nudge a column. The last seat in a row gets none - a line out there
   would run past the end of the table. Inset top and bottom so no line reaches as far as the
   end turns printed at the rim (see .ring-end), which are the only marks out there meant to
   carry any weight. */
.ring-far .seat:not(:last-child)::before,
.ring-near .seat:not(:last-child)::before {
  content: ''; position: absolute; z-index: 0; pointer-events: none;
  top: 12%; bottom: 12%; width: 1px;
  right: calc(var(--ring-gap, 8px) / -2);
  background: rgba(217,201,163,0.09);
}
/* And one along the seam where the two rows meet, stopping well short of both ends for the
   same reason. */
.ring-near { position: relative; }
.ring-near::before {
  content: ''; position: absolute; z-index: 0; pointer-events: none;
  left: 6%; right: 6%; top: calc(var(--ring-row-gap, 0.3rem) / -2); height: 1px;
  background: rgba(217,201,163,0.09);
}
/* A place setting is exactly as wide as the cards in front of it - never as wide as whatever
   sentence happens to be describing the last move. Without this the longest last-action text
   at the table silently sets every seat's width, and the ring stops fitting.
   The clipping lives on the TEXT, not on the header row: overflow:hidden on the row itself
   also cropped the active player's grown avatar flat top and bottom, which is a lot of
   collateral for a job only the name needs doing. */
.seat > .last-action {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.seat > .seat-head { max-width: 100%; white-space: nowrap; }
.seat > .seat-head strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* A spotlight falling on whoever's up, rather than a box being drawn round them: a warm pool
   of light under their whole place - avatar, name and the cards in front of them - so it reads
   from across the room without costing a single pixel of layout.

   Carried on a pseudo-element rather than the seat's own background so it can be FADED. The
   turn moving on is the one moment the table has something to say, and a light that snaps off
   one mat and onto another says it far less well than one that dims and another that warms up
   (see .turn-leaving / .turn-arriving). z-index sits it under the seat's own contents but
   above the felt, so the cards always read on top of the light, never through it. */
.seat::after {
  content: ''; position: absolute; inset: 0; border-radius: 10px; z-index: -1;
  pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse at 30% 20%, rgba(224,160,0,0.30), rgba(224,160,0,0.10) 65%, transparent 85%);
  box-shadow: 0 0 22px rgba(224,160,0,0.28);
}
.seat.current-turn::after { opacity: 1; }
.seat.current-turn .seat-head strong { color: var(--warn); }
.seat.current-turn .avatar-bubble {
  box-shadow: 0 0 0 2px var(--warn), 0 0 14px rgba(224,160,0,0.7);
}
/* The one avatar actually worth looking at right now gets to be bigger than the rest -
   avatars shrink down to almost nothing once several seats are competing for the same row,
   so whoever's turn it is (the one moment their portrait matters most) is the moment it
   grows back instead of staying exactly seat-sized.
   A transform, not width/height: growing the box made the header row taller, which made the
   whole place setting taller than its neighbours and shoved everything below it around. A
   scale changes nothing in layout at all - the melds never move by so much as a pixel. */
#table-seats {
  --turn-grow: 1.3;
  /* How long the hand-over takes. Named so the four animations that make it up can't drift
     apart, and so TURN_HANDOFF_MS in main.ts has one number to match rather than four.
     Unhurried on purpose: at half this it read as a page being flicked rather than a turn
     being passed, and the whole point of animating it at all is that you notice. */
  --turn-anim: 780ms;
  --turn-anim-out: 660ms;
}
/* The wrapper grows, not the portrait inside it. The card count and the dealer chip are
   pinned to the wrapper's edges, so scaling only the portrait left them sitting where the
   small avatar used to end - which on the seat whose turn it is meant the avatar swelled out
   and swallowed them both. Scaling the wrapper carries them out along with it, so they stay
   tucked on the rim at exactly the same overlap whatever size the portrait is. */
.seat.current-turn .seat-avatar-wrap { transform: scale(var(--turn-grow)); }
/* The badges ride out with the wrapper but keep their own size. Scaling the wrapper is what
   carries them along to the rim; cancelling that scale on the badges themselves is what stops
   them growing into the portrait. Same trick either way round: divide by what the parent
   multiplied by, from the corner each badge is pinned to, so it lands on the enlarged rim at
   exactly the size it always was. */
.seat.current-turn .seat-avatar-wrap .seat-card-count,
.seat.current-turn .seat-avatar-wrap .dealer-chip {
  transform: scale(calc(1 / var(--turn-grow)));
}
/* About their own centres, so each badge's middle lands exactly on the enlarged rim - the
   same half-in, half-out straddle an idle seat has. Anchoring by a corner instead slid them
   outboard and left only a third of the badge over the portrait. */
.seat.current-turn .seat-avatar-wrap .seat-card-count,
.seat.current-turn .seat-avatar-wrap .dealer-chip { transform-origin: 50% 50%; }
/* Which way it grows: away from the cards, always. The far row keeps its melds below the
   name, so its avatar swells upward, out over the rim of the table; the near row is turned
   round and keeps its melds above, so its avatar swells downward instead. Either way the
   extra size lands on empty felt rather than over the one thing at a place setting that
   actually has to stay readable. */
/* Sideways it always grows outward too, off the seat's leading edge rather than into the
   name: the portrait leans out of its place at the table instead of swallowing the first
   letter of whoever it belongs to. The few pixels it gains land in the strip's own left
   padding, or at worst on the very edge of the next mat along, which costs that mat nothing
   anybody was reading. */
.ring-far .seat .seat-avatar-wrap { transform-origin: 100% 100%; }
.ring-near .seat .seat-avatar-wrap { transform-origin: 100% 0%; }

/* ---------- The turn changing hands ---------- */
/* The one event at this table worth animating. Play moving on is otherwise a silent swap -
   one mat dark, another lit, between two frames - and on a ten-player table that is very easy
   to miss entirely. So it's staged: the player who just went shrinks back down and their light
   fades out, the next player's warms up and their portrait swells, and the strip slides along
   underneath to bring the new pair to the middle (see followActiveSeat).

   Keyframes rather than transitions, because renderTable rebuilds every seat from innerHTML:
   a fresh element has no previous value to transition FROM, but an animation plays on it the
   moment it lands. --turn-anim-delay is set to negative elapsed time by the same code, so a
   repaint landing mid-animation resumes it where it was instead of starting it over. */
@keyframes turnAvatarGrow {
  from { transform: scale(1); }
  /* A hair past its resting size and back - the overshoot is what makes it read as somebody
     leaning in rather than a box being resized. */
  70%  { transform: scale(calc(var(--turn-grow) * 1.07)); }
  to   { transform: scale(var(--turn-grow)); }
}
@keyframes turnAvatarShrink {
  from { transform: scale(var(--turn-grow)); }
  to   { transform: scale(1); }
}
/* The light doesn't creep up alongside the portrait - it waits, and pops the instant the
   portrait lands. 70% is where turnAvatarGrow reaches full size (its overshoot peak), and
   both run for --turn-anim, so these percentages line up whatever that is set to: dark all
   the way in, then lit on arrival, like somebody sitting down under a lamp. Fading the two
   together made the arrival mushy - there was no single moment where the turn had visibly
   become theirs. */
@keyframes turnGlowIn  { 0%, 64% { opacity: 0; } 76%, 100% { opacity: 1; } }
@keyframes turnGlowOut { from { opacity: 1; } to { opacity: 0; } }

.seat.turn-arriving .seat-avatar-wrap {
  animation: turnAvatarGrow var(--turn-anim) cubic-bezier(0.22, 0.9, 0.3, 1) both;
  animation-delay: var(--turn-anim-delay, 0ms);
}
.seat.turn-arriving::after {
  animation: turnGlowIn var(--turn-anim) ease both;
  animation-delay: var(--turn-anim-delay, 0ms);
}
/* The seat that just played. It carries no .current-turn any more, so its resting state is
   already small and dark - the animation's job is purely to get it there gracefully. */
.seat.turn-leaving .seat-avatar-wrap {
  animation: turnAvatarShrink var(--turn-anim-out) ease both;
  animation-delay: var(--turn-anim-delay, 0ms);
}
.seat.turn-leaving::after {
  animation: turnGlowOut var(--turn-anim-out) ease both;
  animation-delay: var(--turn-anim-delay, 0ms);
}
@media (prefers-reduced-motion: reduce) {
  .seat.turn-arriving .seat-avatar-wrap, .seat.turn-arriving::after,
  .seat.turn-leaving .seat-avatar-wrap, .seat.turn-leaving::after { animation: none; }
}

/* Pass-and-play only: the beat between somebody finishing their turn and the cover screen
   coming down, so whoever is holding the device actually sees the handover happen before the
   blinds drop. The table stays up; the hand doesn't, because it isn't theirs any more. */
#table-view.handing-over #hand-dock { visibility: hidden; }
/* Deaf as well as handless for that half second. The turn has genuinely moved on already -
   pass-and-play draws the viewer from whoever is on turn - so a stray tap on a pile here
   would be the player who just finished taking the next player's draw for them. */
#table-view.handing-over #table-scroll { pointer-events: none; }
.seat.is-afk { opacity: 0.62; border-style: dashed; }
.seat.is-dnf { opacity: 0.5; border-style: dashed; border-color: rgba(192,57,43,0.5); cursor: default; }
.seat.is-dnf .seat-line { opacity: 0.85; }
.afk-tag { font-size: 0.8rem; margin-left: auto; }
/* Exactly as many empty, dashed meld rows as this seat's own level requires, reserving the
   footprint the real cards will take the instant they lay down - lay-down is atomic (every
   required meld at once), so a seat only ever goes empty straight to full, and reserving the
   whole stack up front is what removes that jump entirely. Built from the same
   .meld-group/.meld-scale shapes the real melds use, so it inherits every seat tier's sizing
   automatically instead of duplicating those numbers here. */
.meld-slot-empty {
  background: transparent; border: 1px dashed rgba(217,201,163,0.3); border-radius: 4px;
}
.meld-slot-empty .meld-owner { visibility: hidden; }
.seat-melds-placeholder { position: relative; }
.seat-melds-placeholder::before {
  content: 'melds land here'; position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; pointer-events: none;
  font-size: 0.62rem; letter-spacing: 0.02em; opacity: 0.4; text-transform: uppercase;
  white-space: nowrap;
}
/* A little poker-style chip, not a word - "dealer" said the same way whoever's cards it is
   would recognize at a real table. Rides on the avatar's own corner like a notification
   badge rather than taking a place in the header row: the meld area below needs that width
   more than the header does. */
.dealer-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; border-radius: 50%; flex: 0 0 auto;
  background: var(--cream); color: #1a1a1a; font-size: 0.62rem; font-weight: bold;
  border: 1px solid rgba(0,0,0,0.35); box-shadow: 0 1px 2px rgba(0,0,0,0.4);
}
/* Both counters hang off the avatar the way a notification badge hangs off an app icon -
   cards left on the top-left, the dealer's chip on the bottom-left. With no box around the
   seat any more they'd otherwise be floating in open felt with nothing to belong to. */
/* Both badges hang off the avatar the way a notification badge hangs off an app icon - but
   off it, not on it. They started life stacked on the same left-hand side and sitting far
   enough in that they covered the character's face, which is the bit worth looking at. Now
   they take opposite corners and sit outside the disc: cards left on the top RIGHT, the
   dealer's chip on the bottom left. The margin on the wrap is what gives them somewhere to
   hang without being clipped by the seat's own edge. */
/* Both badges hang down the avatar's left-hand edge - cards left at its top corner, the
   dealer's chip at its bottom one - each just barely biting into the circle so the character
   inside stays the thing you look at. Neither is allowed to sit above or below the avatar's
   own height: overhanging vertically made every seat in the row taller for two little discs.
   Going left costs a few pixels off a very long name, and the dealer chip no longer needing
   its own bottom margin gives most of that straight back. */
.seat-avatar-wrap {
  position: relative; display: inline-flex; flex: 0 0 auto;
  margin-left: 0.55rem; margin-right: 0.1rem;
}
.seat-avatar-wrap .dealer-chip { position: absolute; bottom: 0; left: -0.55rem; z-index: 3; }
.seat-card-count {
  position: absolute; top: 0; left: -0.55rem; z-index: 3;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.15rem; height: 1.15rem; padding: 0 0.22rem; border-radius: 999px;
  background: rgba(6,26,18,0.95); border: 1px solid var(--gold);
  color: var(--cream); font-size: 0.68rem; font-weight: bold; line-height: 1;
  pointer-events: none;
}
/* The ring turns gold; the number does not. Recolouring the digit as well made the count
   read as a warning about itself rather than as whose turn it is. */
.seat.current-turn .seat-card-count { border-color: var(--warn); }

/* A table with only a few people at it has room to spare - scaling seats up to fill it
   reads better for old eyes than leaving them at the size that only really earns its keep
   once seven or more players are competing for the same rows. Only the size changes between
   these tiers now; the shape is the same ring at every count. */
#table-seats.seats-lg .ring-far, #table-seats.seats-lg .ring-near { gap: 0.25rem; }
/* flex-grow stays 0 so a seat can never stretch past its siblings just because its group
   happens to have room going spare - every place setting is the same size or the table stops
   reading as a table. */
#table-seats.seats-lg .seat { flex: 0 0 auto; width: 176px; padding: 0.2rem 0.25rem; font-size: 0.88rem; }
#table-seats.seats-lg .seat-head strong { font-size: 1.02rem; }
#table-seats.seats-lg .seat-line { font-size: 0.84rem; }
#table-seats.seats-lg .avatar-bubble { width: 34px; height: 34px; font-size: 1.15rem; }
#table-seats.seats-lg .dealer-chip { width: 1rem; height: 1rem; font-size: 0.6rem; }
#table-seats.seats-lg .seat-card-count { min-width: 1rem; height: 1rem; font-size: 0.6rem; padding: 0 0.15rem; }
#table-seats.seats-lg { --meld-cards-scale: 0.5125; }
#table-seats.seats-lg .meld-scale { height: 66px; }
#table-seats.seats-lg .meld-cards { transform: scale(0.5125); }
/* Tight enough that two tiles clear the seat's width with room to spare - at a hair's
   breadth they round-trip to a wrap, which doubles every seat's height at a stroke. */
#table-seats.seats-lg .seat-melds, #table-seats.seats-lg .seat-melds-placeholder { gap: 0.1rem; }

#table-seats.seats-xl .ring-far, #table-seats.seats-xl .ring-near { gap: 0.85rem; }
#table-seats.seats-xl .seat { flex: 0 0 auto; width: 336px; padding: 0.3rem 0.4rem; font-size: 0.98rem; }
#table-seats.seats-xl .seat-head strong { font-size: 1.18rem; }
#table-seats.seats-xl .seat-line { font-size: 0.92rem; }
#table-seats.seats-xl .avatar-bubble { width: 44px; height: 44px; font-size: 1.4rem; }
#table-seats.seats-xl .dealer-chip { width: 1.25rem; height: 1.25rem; font-size: 0.7rem; }
#table-seats.seats-xl .seat-card-count { min-width: 1.25rem; height: 1.25rem; font-size: 0.7rem; }
#table-seats.seats-xl { --meld-cards-scale: 1; }
#table-seats.seats-xl .meld-scale { height: 130px; }
#table-seats.seats-xl .meld-cards { transform: scale(1); }
/* Two players, and only two players. One opponent means one place setting on a screen built
   to hold four, so their melds go side by side across all that spare width instead of
   stacking down it - and at that size they are already big enough to throw a card at
   directly, which is why handDropZones registers them without the seat having to be opened
   first. Every other count is too tight for that to be anything but a missed drop. */
#table-seats.seats-xl .seat-melds,
#table-seats.seats-xl .seat-melds-placeholder {
  flex-direction: row; flex-wrap: nowrap; align-items: flex-start; gap: 0.5rem;
}
#table-seats.seats-xl .seat-melds .meld-group { min-width: 0; }
/* The lay-off target reads as one, rather than looking identical to a meld you can only
   admire. Faint until you actually pick a card up (see .laying-off below). */
#table-seats.seats-xl .seat-melds .meld-group {
  border: 1px dashed rgba(217,201,163,0.22); border-radius: 8px; padding: 0.15rem;
}
#table-seats.seats-xl .seat-melds .meld-group.drop-target-active {
  border-style: solid;
}

/* Six players and up. This tier used to shrink each place setting to whatever four abreast
   left over - about 90px - and a meld card at that size is a smear, so it showed pips instead
   and made you tap to read anything. With the strip scrolling again there is no reason for
   either compromise: the cards here are the biggest of any tier, and the table simply runs
   wider than the phone, which is what a nine-player table does in life too. */
/* ---------- Five players and up: the seating grid ----------
   Two rows by C columns, strictly aligned - column n of the top row sits directly above
   column n of the bottom row, no stagger. The window shows exactly two columns, so a column
   is measured as half of it rather than picked from a table of magic numbers per player
   count: the phone decides how wide a place setting is and the cards inside scale to fit
   what's left. sizeRingColumns in main.ts writes the four values below before the seats are
   built, so the first paint is already right. */
#table-seats.ring-grid {
  --ring-gap: 8px;        /* between columns */
  --ring-pad-left: 10px;  /* room for the badges hanging off the first avatar */
  --ring-pad-right: 4px;
  --ring-pad: 14px;       /* the two together, for the column-width maths */
  --seat-pad: 10px;  /* a place setting's inner padding */
  --meld-gap: 3px;        /* between the two meld tiles inside one */
  --meld-chrome: 6px;     /* the tiles' own borders, plus a hair of slack against wrapping */
  --seat-col-max: 250px;  /* past this, a wider screen shows more of the table, not bigger seats */
}
/* Between the two rows. Named because the seam rule ruled on the felt has to land exactly
   halfway down it (see .ring-near::before). */
#table-seats { --ring-row-gap: 0.3rem; }
#table-seats.ring-grid .ring-body {
  padding-left: var(--ring-pad-left); padding-right: var(--ring-pad-right);
  gap: var(--ring-row-gap);
}
#table-seats.ring-grid .ring-far,
#table-seats.ring-grid .ring-near { gap: var(--ring-gap); justify-content: flex-start; }
#table-seats.ring-grid .seat {
  flex: 0 0 auto; width: var(--seat-col); padding: 0.2rem 5px; font-size: 0.86rem;
}
#table-seats.ring-grid .seat-head strong { font-size: 0.98rem; }
#table-seats.ring-grid .avatar-bubble { width: 32px; height: 32px; font-size: 1.1rem; }
#table-seats.ring-grid { --meld-cards-scale: var(--meld-scale); }
#table-seats.ring-grid .meld-scale { height: var(--meld-tile-h); }
#table-seats.ring-grid .meld-cards { transform: scale(var(--meld-scale)); }
#table-seats.ring-grid .seat-melds,
#table-seats.ring-grid .seat-melds-placeholder { gap: var(--meld-gap); }
#table-seats.ring-grid .seat-avatar-wrap { margin-left: 0.5rem; margin-right: 0.08rem; }
#table-seats.ring-grid .seat-card-count {
  min-width: 1rem; height: 1rem; font-size: 0.6rem; padding: 0 0.15rem; left: -0.5rem;
}
#table-seats.ring-grid .dealer-chip { width: 1rem; height: 1rem; font-size: 0.6rem; left: -0.5rem; }
/* Column-aligned rest positions. Mandatory, not proximity: the spec is explicit that the
   scroll never comes to rest between columns, and with every column exactly half the window
   wide, start-aligning one always puts a whole second column beside it. */
#table-seats.ring-grid .ring-scroll { scroll-snap-type: x mandatory; }
/* ...except while the strip is travelling to a new active pair under its own steam. Mandatory
   snapping re-snaps after every write, so a hand-rolled slide would arrive on its first frame
   and none of the easing would ever be seen. It comes back on the moment the slide lands,
   which is also the moment snapping starts mattering again. */
#table-seats .ring-scroll.sliding { scroll-snap-type: none; }
#table-seats.ring-grid .ring-far .seat,
#table-seats.ring-grid .ring-near .seat { scroll-snap-align: start; }

/* The one vacant place, on the counts that don't divide evenly. Always the bottom row's last
   column. Somebody's empty chair with a mug left on it - never a token, never a scroll
   target, and deliberately quiet enough that you don't mistake it for a player. */
.seat-empty {
  display: flex; align-items: center; justify-content: center;
  opacity: 0.22; pointer-events: none; align-self: stretch;
}
.seat-empty .empty-mark { font-size: 1.6rem; filter: grayscale(0.6); }

#table-seats.seats-sm .ring-far, #table-seats.seats-sm .ring-near { gap: 0.4rem; }
#table-seats.seats-sm .seat { flex: 0 0 auto; width: 212px; padding: 0.2rem 0.25rem; font-size: 0.86rem; }
#table-seats.seats-sm .seat-head strong { font-size: 0.98rem; }
#table-seats.seats-sm .avatar-bubble { width: 32px; height: 32px; font-size: 1.1rem; }
/* Two tiles plus the gap have to clear the seat's inner width with room to spare - at a
   hair's breadth they wrap, and every seat in the row doubles in height at a stroke. */
#table-seats.seats-sm { --meld-cards-scale: 0.6; }
#table-seats.seats-sm .meld-scale { height: 78px; }
#table-seats.seats-sm .meld-cards { transform: scale(0.6); }
#table-seats.seats-sm .seat-avatar-wrap { margin-left: 0.5rem; margin-right: 0.08rem; }
#table-seats.seats-sm .seat-card-count {
  min-width: 1rem; height: 1rem; font-size: 0.6rem; padding: 0 0.15rem; left: -0.5rem;
}
#table-seats.seats-sm .dealer-chip { width: 1rem; height: 1rem; font-size: 0.6rem; left: -0.5rem; }

/* ---------- Tablet and up: same layout, sized to use the extra room ----------
   #table-view already fills the real viewport height on any device (see its own
   height: calc(100dvh - 2rem)), so nothing here is about avoiding a scrollbar - it's
   purely "make good use of space that a phone-sized layout would otherwise leave
   spare," on anything from a small tablet up to a desktop window wide enough to fit it. */
/* Desktop only: when the window is too short for the whole game, the TABLE is what gives way,
   not the cards you have to play with. A browser window dragged shorter used to push the draw
   pile, the discard and the lay-down drawer below the fold together, leaving you hunting
   behind a scrollbar for the pile you were about to draw from. Now the strip of placemats is
   the scroll region: the far side of the table goes under the fold first, and only once the
   strip has shrunk as far as it's allowed does the whole area start scrolling as a fallback -
   because something being unreachable is worse than something needing a scroll.
   Deliberately not on phones. There, the seat strip is already sized to the screen, and two
   or three players get a big deliberately-generous meld area that squeezing would wreck. */
@media (min-width: 700px) {
  #table-scroll { display: flex; flex-direction: column; }
  #table-seats {
    flex: 1 1 auto; overflow-y: auto; overflow-x: hidden;
    /* No bar of its own. This is a nested scroller inside #table-scroll, which keeps its own
       bar as the last-resort fallback - and when a window is short enough for both to engage
       you got two vertical scrollbars side by side down the edge of the felt, which looks
       exactly as broken as it sounds. The table still scrolls by wheel, trackpad and touch;
       it just doesn't draw furniture on the tablecloth to say so. */
    scrollbar-width: none;
    /* Low enough that the strip really can absorb the whole shortfall before the outer
       area has to scroll at all - a high floor just moves the scrollbar back out again -
       and proportional so a very short window still gives the table what it can spare. */
    min-height: min(230px, 36vh);
  }
  #table-seats::-webkit-scrollbar { display: none; }
  /* Fixed-size things that have to stay whole and reachable. */
  #table-topbar, #piles, #lay-down-panel { flex: 0 0 auto; }

  #app { max-width: 1300px; }
  #piles { --pile-card-w: 108px; --pile-card-h: 152px; }
  #table-seats.seats-lg .avatar-bubble { width: 40px; height: 40px; font-size: 1.3rem; }
  #table-seats.seats-xl .avatar-bubble { width: 52px; height: 52px; font-size: 1.6rem; }
  #table-seats.seats-xl .meld-scale { width: 160px; height: 130px; }
  .my-space-melds .meld-scale, .my-space-melds.always-open .meld-scale { width: 176px; height: 143px; }
}
/* The phone sizes are what they are because two place settings have to share 390px. Given a
   tablet's width there's no reason to keep them that small - same ring, same everything, just
   using the room. Gated on height as well as width on purpose: a tablet held landscape has
   the width for bigger place settings and nowhere near the height, and the ring is two rows
   deep from four players on. */
/* Capped in width as well as floored, or it fires on a tall DESKTOP window too, where it
   inflates the place settings past the height the strip has been given and puts the near row
   under the fold. This rule is about a tablet held portrait, and now says so. */
/* Excluded on the ring grid, where sizeRingColumns has already measured the window and
   written --seat-col to fit the whole table. This block sits later in the file at equal
   specificity, so without the exclusion it quietly won - pinning every place setting to 236px
   whatever the ring needed, overflowing the strip, and leaving the left-hand seats parked off
   the edge looking like the table had been cut off rather than resized. */
@media (min-width: 700px) and (max-width: 1100px) and (min-height: 1000px) {
  #table-seats.seats-lg:not(.ring-grid) .seat { width: 236px; }
  #table-seats.seats-lg:not(.ring-grid) .meld-scale { width: 112px; height: 91px; }
  #table-seats.seats-lg:not(.ring-grid) .meld-cards { transform: scale(0.7); }
}
@media (min-width: 1024px) {
  #app { max-width: 1500px; }
  #piles { --pile-card-w: 124px; --pile-card-h: 174px; }
}

/* Say you've stepped away and your turns play themselves until you say otherwise. */


.menu-row.resuming {
  background: rgba(224,160,0,0.2); border-color: var(--warn); color: var(--warn); font-weight: bold;
}

/* Parked. Deliberately covers everything - the point of a hold is that the game is
   genuinely stopped, not merely quiet. */
#paused-overlay, #table-gone-overlay {
  position: fixed; inset: 0; z-index: 86; display: flex;
  align-items: center; justify-content: center; padding: 1rem;
  background: rgba(4,20,14,0.86); backdrop-filter: blur(6px);
}
#table-gone-overlay { z-index: 96; }   /* nothing sits over "this game is gone" */
#paused-overlay[hidden], #table-gone-overlay[hidden] { display: none !important; }
#btn-table-gone-ok {
  margin-top: 1rem; padding: 0.6rem 1.2rem; border: none; border-radius: 9px;
  background: var(--gold); color: #1a1a1a; font-family: inherit; font-weight: bold;
  font-size: 1rem; cursor: pointer;
}
.paused-card {
  max-width: 420px; text-align: center; padding: 1.6rem 1.4rem;
  border: 2px solid var(--gold); border-radius: 14px; background: rgba(10,44,31,0.97);
  box-shadow: 0 14px 40px rgba(0,0,0,0.55);
}
.paused-card h2 { margin: 0 0 0.5rem; color: var(--warn); font-size: 1.8rem; }
.paused-card p { margin: 0.4rem 0; font-size: 0.95rem; }
.paused-sub { opacity: 0.7; font-size: 0.85rem !important; }
.menu-row.away {
  background: rgba(224,160,0,0.2); border-color: var(--warn); color: var(--warn); font-weight: bold;
}
.seat .seat-head { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
.seat .seat-head strong { font-size: 0.82rem; }
.seat .seat-line { opacity: 0.85; font-size: 0.74rem; }
.breakdown-who { display: inline-flex; align-items: center; gap: 0.4rem; }

/* Their last move is a glance, not a panel: it appears over the chip when you poke it and
   fades out again on its own, so it isn't taking up room the other 99% of the time. */
.seat .last-action {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 4px);
  background: rgba(8,30,24,0.96); border: 1px solid var(--gold); border-radius: 7px;
  padding: 0.3rem 0.45rem; font-size: 0.68rem; font-style: italic; text-align: center;
  opacity: 0; transform: translateY(4px); pointer-events: none; z-index: 6;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.seat.showing-action .last-action { opacity: 1; transform: translateY(0); }
/* From the third look it stops being helpful and starts being personal. */
.seat .last-action.nagging { border-color: var(--warn); color: var(--warn); }

/* Down to one card. Loud on purpose, and gone again in a couple of seconds - it's a shout,
   not a label, since the chip already says how many cards they hold. In-flow (not floated
   like .last-action) so it can't collide with that bubble if someone pokes the chip while
   the shout is still up. */
.seat .loono-callout {
  align-self: flex-start; margin: 0.1rem 0;
  background: rgba(8,30,24,0.96); border: 1px solid var(--gold); border-radius: 6px;
  padding: 0.05rem 0.35rem; font-size: 0.7rem; font-weight: bold; letter-spacing: 0.06em;
  color: var(--gold); text-shadow: 0 0 6px rgba(217,201,163,0.5);
}
/* Seats rebuild from scratch on every render, so without this the pop would replay from the
   top on every unrelated render for the whole 2.6s the shout is up. `.pop` only appears in
   the template for the first 400ms of that window, so it plays exactly once. */
.seat .loono-callout.pop { animation: loonoPop 0.32s cubic-bezier(0.2,1,0.3,1) both; }
@keyframes loonoPop {
  from { transform: scale(0.6) rotate(-8deg); opacity: 0; }
  60%  { transform: scale(1.18) rotate(3deg); opacity: 1; }
  to   { transform: scale(1) rotate(0deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .seat .loono-callout.pop { animation: none; }
}

/* A seat's melds, small enough that a full table of them still fits on screen. Sitting right
   under that seat's name is the whole point of the merge - no separate "who owns this lane"
   bookkeeping needed. Nothing shown here at all just means that seat hasn't laid down yet -
   the old "Down / Not down" text was saying exactly what this now shows for free. */
/* Side by side, wrapping - a place setting is two meld tiles wide, so a round needing two
   melds lays them out in front of the player rather than stacking them and doubling how tall
   every seat at the table has to be. Three wraps onto a second row, which is the one level
   that needs it. */
.seat-melds {
  display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.18rem; align-items: flex-start;
  margin-top: 0.15rem;
}
/* On a phone a full seat is wider than the screen, so let its melds stack rather than push
   the page sideways. */
@media (max-width: 560px) {
  .seat { max-width: 100%; }
}
/* The seat you're currently looking at, marked only - it does not change size, ever. Its
   blown-up melds live in #meld-focus, floating over the table. */
.seat.expanded { border-color: var(--warn); box-shadow: 0 0 0 1px var(--warn) inset; }
.meld-group {
  background: rgba(255,255,255,0.06); border: 1px solid var(--gold); border-radius: 8px;
  padding: 0.25rem; display: flex; flex-direction: column; gap: 0.15rem; align-items: center;
  position: relative;
}
/* On a placemat you're just looking at cards lying there - no box around each meld, no
   "Set"/"Run" caption, nothing but the cards. Both come back in the focus overlay, which is
   the only place they're worth the room: when you're about to aim something at them. */
#table-seats .meld-group {
  background: none; border: none; padding: 0; border-radius: 0;
}
#table-seats .meld-owner { display: none; }
.meld-group .meld-owner { font-size: 0.66rem; opacity: 0.75; }
.meld-group .meld-cards {
  /* 160 is what a fanned meld actually spans (MELD_TRACK_WIDTH in main.ts) - the box used to
     be 190, so every tile carried 30px of empty space that the cards were scaled down to fit
     around. Sizing it to the real extent buys ~19% on every card at every table size. */
  position: relative; height: 130px; width: 160px;
  transform-origin: top left; transition: transform 0.16s ease;
}
.meld-group button { font-size: 0.72rem; padding: 0.2rem 0.5rem; border-radius: 6px; border: none; cursor: pointer; }
/* The lay-off button isn't hidden here any more - it simply isn't built at all except on the
   seat you've actually opened (see buildMeldGroupEl's allowAddButton), so there's nothing
   left in the DOM for a stray tap to find during an ordinary glance at the table. */

/* The wrapper holds the footprint; the cards inside are what scales down into it. A seat's
   own copy is fixed at its tier's size and never changes - reading them properly happens in
   #meld-focus instead. */
.meld-scale { width: 104px; height: 85px; }
#table-seats .meld-cards { transform: scale(0.65); }
.meld-hint { font-size: 0.7rem; opacity: 0.55; margin: 0.1rem 0 0; }

/* ---------- One opponent's melds, blown up over the table ----------
   Floating rather than grown-in-place on purpose: the placemats underneath keep their exact
   size and position whatever is open, which is the whole reason the table stopped lurching
   around. Fixed to the viewport so the 6-9-player strip's own horizontal scroll can't clip
   it. */
#meld-focus {
  position: fixed; left: 50%; top: 12vh; transform: translateX(-50%); z-index: 40;
  max-width: min(94vw, 460px); max-height: 66vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 0.5rem;
  padding: 0.7rem; border-radius: 14px;
  background: rgba(8,32,25,0.98); border: 1px solid var(--gold);
  box-shadow: 0 18px 44px rgba(0,0,0,0.7);
}
#meld-focus[hidden] { display: none; }
.meld-focus-head .avatar-bubble { width: 56px; height: 56px; font-size: 1.8rem; flex: 0 0 auto; }
.meld-focus-head strong { font-size: 1.15rem; }
.meld-focus-head {
  flex-wrap: wrap;
  display: flex; align-items: center; gap: 0.45rem; font-size: 0.95rem;
}
/* The card count is on the placemat too, as a badge the size of a pea - here, where the whole
   point is being able to actually read what's in front of somebody, it gets said in words. */
.meld-focus-count {
  font-size: 0.75rem; opacity: 0.8; padding: 0.1rem 0.4rem; border-radius: 999px;
  background: rgba(0,0,0,0.3); border: 1px solid rgba(217,201,163,0.4);
}
/* The avatar got a lot bigger, so on a phone the two labels beside it have nowhere to go but
   down - let the hint drop onto its own line whole rather than squeeze into three words on
   three lines. */
.meld-focus-hint {
  margin-left: auto; font-size: 0.68rem; opacity: 0.6; font-style: italic; white-space: nowrap;
}
.meld-focus-count { white-space: nowrap; }
.meld-focus-rows { display: flex; flex-direction: column; gap: 0.5rem; align-items: center; }
/* Full size here, genuinely - not a transform. The box really is this big, so it's a real
   drop target for a thrown card, not just something easier to read. */
#meld-focus { --meld-cards-scale: 1; }
#meld-focus .meld-scale { height: 130px; }
#meld-focus .meld-cards { transform: scale(1); }
#meld-focus .meld-group { width: 100%; align-items: flex-start; }

/* Drag a card straight onto a meld group to lay it off - lights up exactly like the discard
   pile does, an honest signal that releasing here will actually land. */
.meld-group.drop-target-active {
  outline: 3px solid var(--warn); box-shadow: 0 0 20px 6px rgba(224,160,0,0.75);
}

#piles {
  /* A tighter gap than the piles alone would want, so the commit control beside the discard
     pile (see #my-space) has real room on a phone-width screen instead of being squeezed
     into a sliver. */
  display: flex; gap: 0.6rem; align-items: stretch; justify-content: space-between; margin-bottom: 0.75rem;
  padding: 0.5rem; border-radius: 12px; transition: background 0.2s ease, box-shadow 0.2s ease;
}
/* The middle of the table, kept in the middle of it. On a phone this row already fills the
   width so space-between and centring look identical; give it a wide screen and the piles
   would otherwise sit hard left while the ring above them is centred on the viewer's own
   placemat - which is precisely the thing that has to line up. */
@media (min-width: 700px) {
  #piles { justify-content: center; gap: 2.5rem; max-width: 720px; margin-inline: auto; }
}
/* A desktop window has room the phone layout never assumed. Past this width the piles and the
   loon-call column stop huddling in the middle of a large empty table and take some of it -
   the cap still exists so they don't drift to opposite edges of an ultrawide and stop reading
   as one group. */
@media (min-width: 1280px) {
  #piles { gap: 5rem; max-width: 960px; }
}
/* The room that used to sit empty next to the discard pile - the viewer's own spot, not
   another opponent's. flex-grow lets it soak up whatever's left rather than a fixed width
   that might not exist on a narrow phone. */
/* Same touch-action guard as #hand-dock (see the gesturestart/touchstart backstop in
   main.ts) - this is the other place a two-finger touch mid-drag could trigger the
   browser's own pinch-zoom instead of landing a lay-off on your own meld. */
/* The piles and their own caption, as one block: the hint sits directly over the two cards
   it's talking about. Parked in the column to their right it read as a label for whatever
   was in that column instead - which, once your own melds moved up to your placemat at the
   table, was usually nothing at all. */
/* Grid rather than a flex column so the hint can't set the width: the track is sized to the
   piles' own min-content (the two cards, which don't shrink), and a long line of status text
   wraps to that instead of stretching the whole row and squeezing the control to its right. */
.pile-pair {
  flex: 0 0 auto; display: grid; grid-template-columns: min-content; justify-items: center;
}
/* align-items: center is the important one - the discard's stack box is taller than the stock
   card (it carries 16px of slop on every side so a badly-thrown card can hang off without
   covering anything), so aligning their tops leaves the two cards themselves visibly out of
   line. Centred, they read as two cards side by side on the same plane, which is what they
   are. margin-top is a little air under the caption, so the pair sits down on the felt rather
   than crowding the near row of placemats straight above. */
.pile-row { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.5rem; }
/* The viewer's own column: their status readout while there's still a lay-down to commit. */
/* Full height of the pile row, so the drawer button can sit at its foot - level with the
   bottom edge of the pile cards, which is where the eye already is - and everything above it
   is free for loon calls. */
#my-column {
  flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
  align-items: stretch; justify-content: space-between; gap: 0.35rem;
}
/* Two cards side by side are as narrow as the piles will ever go, so on the smallest phones
   the column beside them is left with about fifty pixels - fine for a "▾" button, nowhere
   near enough for somebody's name and what they said. Rather than shrink the bubble into
   something unreadable, or let it spill sideways over the discard pile, the column drops to
   its own full-width row underneath. It is still below the table and still above the meld
   drawer's button, which is what the spot was for. */
@media (max-width: 380px) {
  #piles { flex-wrap: wrap; }
  #my-column { flex: 1 0 100%; align-items: stretch; }
  #my-space { justify-content: center; }
}
#my-space { width: 100%; display: flex; justify-content: flex-end; min-width: 0; touch-action: none; }

/* The commit control itself - merged with the status readout that used to sit here on its
   own. Laying down is irreversible and hands everyone else somewhere to dump their cards,
   so once it's actually legal and actually your turn, this needs to look like the loudest
   control on the table, same as the old full-width bar at the foot of the lay-down panel
   did - just sized to live beside the discard pile instead of off the bottom of the screen. */
.my-space-status {
  /* Stacked rather than side-by-side - the room beside the discard pile is tall before it's
     wide, so the icon rides above the text instead of eating into the same narrow row. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.2rem; min-height: 44px; min-width: 84px;
  font-family: inherit; padding: 0.55rem 0.6rem; border-radius: 12px;
  cursor: pointer; text-align: center; color: var(--cream);
  border: 2px dashed rgba(217,201,163,0.45); background: #123a2a; max-width: 100%;
  transition: transform 0.15s ease, box-shadow 0.25s ease, background 0.25s ease;
}
.my-space-status:hover { filter: brightness(1.12); }
.my-space-mark {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  background: rgba(255,255,255,0.09); font-size: 1.05rem; line-height: 1;
}
/* The arrow means "tap to go down to your melds" - a quiet vertical nudge sells that
   without needing a caption, same idea as a scroll-cue chevron. Only while it's still
   just an arrow; once it flips to the checkmark the pulsing border already carries urgency. */
.my-space-status:not(.can-commit) .my-space-mark {
  animation: mySpaceArrowNudge 1.6s ease-in-out infinite;
}
@keyframes mySpaceArrowNudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .my-space-status:not(.can-commit) .my-space-mark { animation: none; }
}
.my-space-text { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; min-width: 0; }
.my-space-text strong { font-size: 0.78rem; line-height: 1.2; }
.my-space-text small { font-size: 0.64rem; opacity: 0.72; }

.my-space-status.ready { border-style: solid; border-color: var(--warn); }
.my-space-status.ready .my-space-mark { background: rgba(224,160,0,0.18); }

.my-space-status.can-commit {
  border-style: solid; border-color: #f0d089; color: #1a1a1a;
  background: linear-gradient(160deg, #f6d98c 0%, var(--gold) 55%, #c99b2e 100%);
  box-shadow: 0 6px 18px rgba(0,0,0,0.4), 0 0 0 0 rgba(224,160,0,0.55);
  animation: layDownReady 2.2s ease-in-out infinite;
}
.my-space-status.can-commit .my-space-mark { background: rgba(0,0,0,0.16); }
.my-space-status.can-commit small { opacity: 0.78; }
.my-space-status.can-commit:hover { filter: brightness(1.06); }
.my-space-status.can-commit:active { transform: scale(0.985); }
@media (prefers-reduced-motion: reduce) {
  .my-space-status.can-commit { animation: none; }
}

/* The gap between "I tapped it" and "the table has it" - same idea as the old panel's
   pending readout, just living in the one spot that now owns the whole commit gesture. */
/* What replaces the commit button the moment you lay down. Two controls disappear at once
   there - this button and the whole drawer below - and a quiet gap is not an explanation, so
   the space says what you can now do instead of just emptying. Deliberately calm: a note, not
   a control, since there is nothing here left to press. */
.my-space-note {
  margin: 0; max-width: 100%; padding: 0.45rem 0.55rem; border-radius: 10px;
  font-size: 0.7rem; line-height: 1.3; text-align: center;
  border: 1px dashed rgba(217,201,163,0.35); background: rgba(0,0,0,0.18);
  color: var(--cream); opacity: 0.9;
}
.my-space-note strong { color: var(--warn); }

.my-space-pending {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem; max-width: 100%;
  min-width: 84px; padding: 0.55rem 0.6rem; border-radius: 12px; font-size: 0.72rem; font-weight: bold;
  border: 2px solid rgba(240,208,137,0.5); background: rgba(224,160,0,0.12); color: var(--cream);
  text-align: center;
}
.my-space-pending::before {
  content: ''; flex: 0 0 auto; width: 1rem; height: 1rem; border-radius: 50%;
  border: 2px solid rgba(240,208,137,0.35); border-top-color: #f0d089;
  animation: layDownSpin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .my-space-pending::before { animation: none; }
}

/* Your own melds get whatever height they need on a tall phone, and scroll inside their own
   box on a short one rather than pushing the table up or the hand down. The table above is
   the thing that must not shrink - it's everyone else's cards, and there's no second way to
   see those - whereas your own melds are already the one thing on screen you know by heart. */
.my-space-melds {
  display: flex; gap: 0.3rem; flex-wrap: wrap; justify-content: flex-end;
  cursor: zoom-in; touch-action: none;
  max-height: 42vh; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin;
}
/* Blown up, it's a deliberate look-at-it-now state and gets the room to be looked at. */
.my-space-melds.expanded { max-height: none; overflow: visible; }
.my-space-melds.expanded { z-index: 5; cursor: zoom-out; }
.my-space-melds .meld-scale { width: 104px; height: 85px; }
.my-space-melds:not(.expanded) .meld-cards { transform: scale(0.5625); }
.my-space-melds.expanded .meld-cards { transform: scale(1); filter: drop-shadow(0 10px 22px rgba(0,0,0,0.65)); }
.my-space-melds.expanded .meld-group { background: rgba(10,38,30,0.97); box-shadow: 0 10px 26px rgba(0,0,0,0.5); }
.my-space-melds.expanded .meld-group button { display: block; }
.my-space-melds:not(.expanded) .meld-group button { display: none; }
/* Same grown-target treatment #table-seats.seats-sm-unused gives opponents' melds - your own
   already-laid meld is a legal lay-off target too, and it can't stay a 90x62 sliver for the
   one gesture that actually needs to land on it. */
.my-space-melds.laying-off .meld-scale { width: 160px; height: 130px; }
.my-space-melds.laying-off .meld-cards { transform: scale(1); }
.my-space-melds.laying-off .meld-group { background: rgba(10,38,30,0.97); box-shadow: 0 10px 26px rgba(0,0,0,0.5); }
.my-space-melds.laying-off .meld-group button { display: block; }
/* 2-3 players only (see bumpRequired in main.ts) - the same grown treatment, but permanent
   rather than gesture-triggered, since there's nothing here that needs collapsing back down. */
.my-space-melds.always-open { cursor: default; }
.my-space-melds.always-open .meld-scale { width: 160px; height: 130px; }
.my-space-melds.always-open .meld-cards { transform: scale(1); }
.my-space-melds.always-open .meld-group { background: rgba(10,38,30,0.97); box-shadow: 0 10px 26px rgba(0,0,0,0.5); }
.my-space-melds.always-open .meld-group button { display: block; }
/* A choice is owed here, the moment it's your turn to draw - both piles light up as one lit
   surface, not just a thin ring on each card, so it can't be missed at a glance. */
#piles.can-draw {
  background: rgba(224,160,0,0.16); box-shadow: 0 0 22px rgba(224,160,0,0.35);
}
.pile { position: relative; display: flex; flex-direction: column; align-items: center; gap: 0.3rem; perspective: 400px; }
/* Both piles read the same two variables the discard's stack box is built from, so a card
   that grows at a breakpoint carries its stack with it. */
.pile .card { width: var(--pile-card-w); height: var(--pile-card-h); }
/* A badge on the pile's own corner rather than a caption under it - the row of text beneath
   the piles was costing the table a line of height for two numbers nobody reads mid-turn.
   (The discard's count and how askew it is are gone entirely; the game still tracks both for
   the tidy bonus - see renderDiscardPile.) */
.pile .pile-count {
  position: absolute; bottom: 0.15rem; left: 50%; transform: translateX(-50%);
  font-size: 0.62rem; opacity: 0.8; padding: 0.05rem 0.35rem; border-radius: 999px;
  background: rgba(6,26,18,0.8); pointer-events: none; white-space: nowrap;
}
.pile.drawable { cursor: pointer; }
.pile.drawable .card { box-shadow: 0 0 0 2px var(--warn), 1px 2px 4px rgba(0,0,0,0.5); }
.pile.drawable:hover .card { filter: brightness(1.08); }
/* backface-visibility/will-change are a defensive hint, not decoration - iOS Safari has a
   known glitch where a 3D-transformed element (rotateY, here) briefly fails to paint
   mid-animation without them, which is exactly the "pile flickers invisible" bug reported
   from a phone browser. */
/* On the pile itself, NOT on the cards inside it. Every render rebuilds the pile's contents
   from innerHTML, so a card-level animation was destroyed and restarted from zero on each
   one - and with a turn clock ticking, renders come round faster than the 0.4s the animation
   needs. It never reached its end, animationend never fired, the class was never taken off,
   and the pile span forever and stopped answering taps. The pile element itself survives
   every render, so the animation here runs once and finishes. */
.pile.pile-flip {
  animation: pileFlip 0.4s ease;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  will-change: transform;
}
@keyframes pileFlip {
  0% { transform: rotateY(0deg) scale(1); }
  50% { transform: rotateY(90deg) scale(1.08); }
  100% { transform: rotateY(0deg) scale(1); }
}
/* Only shown when the throw is genuinely armed and releasing WILL discard. */
.pile.drop-target-active .card {
  outline: 4px solid var(--warn); transform: scale(1.1);
  box-shadow: 0 0 26px 8px rgba(224,160,0,0.85);
}
.pile.drop-target-active::after {
  content: 'release to discard';
  position: absolute; left: 50%; top: -1.5rem; transform: translateX(-50%);
  background: var(--warn); color: #1a1a1a; font-weight: bold; font-size: 0.7rem;
  padding: 0.15rem 0.5rem; border-radius: 999px; white-space: nowrap; pointer-events: none;
}
#discard-pile { position: relative; }
.hint-text { font-size: 0.8rem; opacity: 0.75; align-self: center; }
/* Right above the piles it's actually talking about, and no wider than they are, so a long
   "so-and-so is playing - you can still sort your hand" wraps over the cards instead of
   stretching the row and shoving the status control off the edge of a phone. */
.pile-hint {
  margin: 0; text-align: center; font-size: 0.72rem; line-height: 1.25;
  max-width: 100%; opacity: 0.8;
}

/* ---------- The sloppy discard pile ---------- */
/* The stack is the card plus a margin of slop on every side, so a badly-thrown card can hang
   off it without covering anything. Both dimensions are derived, never typed: they were once
   hardcoded to 124x162 - correct for the 92x130 phone card - and stayed there when the card
   grew to 124x174 on a desktop. The stack ended up SHORTER than the card inside it, and since
   .pile-row centres the two piles against each other, that mismatch pushed the discard 22px
   below the stock. --discard-slop must equal DISCARD_SLOP_PAD in main.ts, which positions the
   cards within it. */
.discard-stack {
  position: relative;
  width: calc(var(--pile-card-w) + 2 * var(--discard-slop));
  height: calc(var(--pile-card-h) + 2 * var(--discard-slop));
  touch-action: none; user-select: none;
}
.discard-stack .card { transition: left 0.25s ease, top 0.25s ease, rotate 0.25s ease; }
#discard-pile { cursor: grab; }
#discard-pile.messy .pile-count { color: var(--warn); }

.tidy-kudos { color: #9fd8c0; font-size: 0.7rem; opacity: 0.8; }
.tidy-reveal {
  background: rgba(159,216,192,0.12); border: 1px solid rgba(159,216,192,0.5);
  border-radius: 8px; padding: 0.5rem 0.75rem; display: inline-block;
}

/* Sits over the table area, never over the hand the player is trying to read. */
#loon-toast {
  position: fixed; left: 50%; top: 11%; transform: translateX(-50%) translateY(-10px);
  background: rgba(10,44,31,0.96); border: 1px solid var(--gold); color: var(--cream);
  padding: 0.6rem 0.9rem; border-radius: 10px; max-width: 82vw; text-align: center;
  font-size: 0.85rem; z-index: 60; pointer-events: none;
  opacity: 0; transition: opacity 0.25s ease, transform 0.25s ease;
  box-shadow: 0 6px 20px rgba(0,0,0,0.5);
}
#loon-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
#loon-toast[hidden] { display: none; }

.card {
  width: 92px; height: 130px; border-radius: 9px; position: relative;
  box-shadow: 1px 2px 4px rgba(0,0,0,0.5);
  flex: 0 0 auto;
}
.card.face { background: var(--cream); color: #111; border: 1px solid #999; }
.card.face.red { color: var(--danger); }
.card.face.black { color: #111; }
.card.face .corner { position: absolute; font-size: 1.35rem; line-height: 1.05; font-weight: bold; }
.card.face .corner.top { top: 5px; left: 6px; }
.card.face .corner.bottom { bottom: 5px; right: 6px; transform: rotate(180deg); }
.card.face .pip { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 2.3rem; }
/* The loon illustration for J/Q/K - contained, centered, and sized to actually read as a
   real illustration rather than a postage stamp. No plaque behind the corners needed (and
   none wanted - it was covering the crown, not protecting anything): the portrait is narrow
   at the head/crown and only widens into the full-width robe/shoulders well below where the
   corner index sits, so the two never actually overlap at this size. Keep the art this
   generously sized rather than shrinking it back down defensively - the shape itself is what
   keeps the corners clear, not the box being small. */
.card.face .face-art {
  position: absolute; top: 15px; bottom: 15px; left: 6px; right: 6px;
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
/* A faint Two Loons stamp behind the plain pip glyph on every number/ace card - texture and
   branding, not a replacement for the rank/suit anyone is actually reading mid-game. Nudged
   up slightly and sized a touch bigger than a flat 50/50 center would put it - the artwork's
   own loons sit low within the roundel's circle, so centering the image's bounding box alone
   still reads as visually low; this brings the loons themselves back toward true center. */
.card.face .watermark {
  position: absolute; inset: 0; opacity: 0.16; pointer-events: none;
  background-size: 96% auto; background-position: 50% 46%; background-repeat: no-repeat;
}
.card.back img { width: 100%; height: 100%; border-radius: 9px; display: block; }
.card.selected { outline: 3px solid var(--warn); transform: translateY(-14px); }
.card.clickable { cursor: pointer; }

/* ---------- Freeform multi-row hand ---------- */
/* relative so the meld count can be pinned to its bottom-right corner - see .meld-card-count,
   which sits opposite the hint line rather than in the flow that lays the hand out. */
#hand-area { margin-bottom: 0.4rem; position: relative; }
/* touch-action: none covers the whole hand area, not just the cards themselves - a touch
   that starts on a gap between cards, or the bit of track background past the last card,
   still needs to be denied to the browser's own pan/zoom gesture recognizer, or a rapid
   tap-tap while organizing can still read as a double-tap-to-zoom despite every individual
   card already saying no. Our own drag code handles all the movement in here; the browser
   has nothing left to do natively. */
#hand-cards { min-height: 1px; touch-action: none; }
.hand-rows { display: flex; flex-direction: column; }
.hand-row { display: flex; align-items: flex-start; }
/* Rows are shorter than a card is tall, so each row tucks under the one above it - only the
   top corner of a covered card matters, and it buys back vertical room on a phone. */
.row-track {
  position: relative; flex: 1 1 auto; height: 78px;
  overflow: visible;
}
.hand-row:last-of-type .row-track { height: 142px; } /* bottom row shows its cards in full */
.row-track [data-slot-el] {
  touch-action: none; user-select: none; -webkit-touch-callout: none;
}
.spacer-slot {
  width: 26px; background: transparent; border: none;
  box-shadow: none;
}
.long-pressing { transition: filter 0.45s ease; filter: brightness(1.25); }
/* A fixed-height slot, occupied whether or not anything is selected - the hand is docked to
   the bottom of the viewport, so anything here that comes and goes pushes the whole fan up
   and down as you tap cards. One line, one height, contents swapped in place. Many selected
   cards scroll sideways rather than wrapping onto a second line, for the same reason. */
.hand-hint {
  font-size: 0.7rem; opacity: 0.55; margin: 0.15rem 0 0; padding-inline: 0.5rem;
  /* The corner belongs to the meld count, which is pinned there (see .meld-card-count).
     Margin rather than padding: this line is a nowrap horizontal scroller, and padding only
     holds the gap open at the far end of the scroll - the text would still slide underneath
     the count at rest, which on a phone is exactly where it sat. Shortening the box keeps
     them apart at every scroll position. */
  margin-right: var(--meld-count-gutter, 4rem);
  height: 1.35rem; line-height: 1.35rem;
  display: flex; align-items: center; gap: 0.3rem;
  white-space: nowrap; overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
}
.hand-hint::-webkit-scrollbar { display: none; }
.hand-hint.has-selection { opacity: 0.9; font-size: 0.75rem; }
.hand-hint-label { flex: 0 0 auto; opacity: 0.7; }
.selected-chip {
  display: inline-block; flex: 0 0 auto;
  font-weight: bold; background: rgba(255,255,255,0.92);
  border-radius: 4px; padding: 0.05rem 0.35rem; line-height: 1.4;
}
.selected-chip.red { color: var(--danger); }
.selected-chip.black { color: #111; }

.dragging-original { opacity: 0.3; }
/* The card you're dragging is mostly see-through so you can read the drop target underneath
   your own thumb - but the rank/suit corner stays fully opaque and legible. */
.drag-ghost {
  box-shadow: 0 14px 28px rgba(0,0,0,0.45); transform: scale(1.12);
  background: transparent !important; border-color: rgba(255,255,255,0.55) !important;
}
/* Opaque enough to track as a card, sheer enough to read the target underneath. */
.drag-ghost.face { background: rgba(244,239,227,0.62) !important; }
.drag-ghost .pip, .drag-ghost .face-art { opacity: 0.4; }
.drag-ghost .corner.bottom { opacity: 0.5; }
.drag-ghost .corner.top {
  opacity: 1; font-size: 1.5rem;
  text-shadow: 0 1px 3px rgba(255,255,255,0.95), 0 0 8px rgba(255,255,255,0.8);
}
/* Carrying a whole selected group: the cards travel together as one block. */
.drag-ghost-group { width: 0; height: 0; }
.drag-ghost-group .drag-ghost { transform: scale(1.06); }

.insertion-marker {
  position: fixed; background: var(--warn); border-radius: 3px; pointer-events: none; z-index: 999;
  box-shadow: 0 0 10px 2px rgba(224,160,0,0.8);
}
.insertion-marker.new-row-marker { height: 5px; }

#lay-down-panel { background: rgba(0,0,0,0.2); border-radius: 10px; margin-bottom: 0.5rem; overflow: hidden; }
#lay-down-panel:empty { display: none; margin: 0; padding: 0; }
/* The pull-tab for the drawer below - the round's own requirement text already lives up in
   #hand-title (bigger, shown once), so this only needs a short label and an open/close cue.
   touch-action:none keeps a vertical drag here from also scrolling the page underneath it. */
.lay-down-handle {
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  font-family: inherit; font-size: 0.85rem; font-weight: bold; color: var(--cream);
  background: none; border: none; padding: 0.6rem 0.75rem; cursor: pointer;
  touch-action: none;
}
.lay-down-handle-arrow {
  display: inline-block; transition: transform 0.2s ease; opacity: 0.85;
}
.lay-down-drawer.open .lay-down-handle-arrow { transform: rotate(180deg); }
/* 2-3 players (see bumpRequired in main.ts): a plain label, not a button - there's nothing
   left for a tap here to do once this always stays open. */
.lay-down-handle-static { cursor: default; touch-action: auto; }
/* Collapsed by default each round (see layDownDrawerOpen) - opened by tapping this handle,
   the arrow beside the discard pile, or dragging the handle up. max-height rather than
   height/auto because the slot content's own height is dynamic (wrapping, illegal-reason
   notes) and this only needs to be generous enough to never clip it. */
.lay-down-body {
  max-height: 0; overflow: hidden; opacity: 0;
  padding: 0 0.75rem; transition: max-height 0.25s ease, opacity 0.2s ease, padding 0.25s ease;
}
.lay-down-drawer.open .lay-down-body {
  max-height: 640px; opacity: 1; padding: 0 0.75rem 0.65rem;
}
@media (prefers-reduced-motion: reduce) {
  .lay-down-body, .lay-down-handle-arrow { transition: none; }
}
/* Side by side, not stacked - a round's melds read as "your two things to build", not a
   long scroll of one-at-a-time slots. They wrap to a second row only if the screen is too
   narrow to fit them, same idea as the table seats. */
#lay-down-slots { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.slot {
  flex: 1 1 160px; min-width: 160px;
  border: 1px dashed var(--gold); border-radius: 8px; padding: 0.5rem;
}
.slot .slot-label { font-size: 0.78rem; opacity: 0.85; margin-bottom: 0.3rem; }
/* A small overlapping fan (same box the table's meld groups use, just full size rather than
   scaled down - this is the "build it" view) instead of one full-width card per row, which
   read as a tall vertical scroll the moment a slot held more than a card or two. */
.slot .slot-cards { position: relative; width: 144px; height: 130px; max-width: 100%; }
.slot button.add-to-slot { margin-top: 0.4rem; }
/* Drag a card straight onto a slot to stage it - same honest "this will land" signal as the
   discard pile and the table's meld groups. */
.slot.drop-target-active {
  border-style: solid; box-shadow: 0 0 16px 4px rgba(224,160,0,0.6);
}
.lay-down-prep-note { font-size: 0.8rem; opacity: 0.7; margin: 0 0 0.5rem; }

@keyframes layDownReady {
  0%, 100% { box-shadow: 0 6px 18px rgba(0,0,0,0.4), 0 0 0 0 rgba(224,160,0,0.5); }
  50%      { box-shadow: 0 6px 18px rgba(0,0,0,0.4), 0 0 0 8px rgba(224,160,0,0); }
}

/* Why a slot won't go down, said under the cards it is about, while there is still time to
   fix it. Rejecting a lay-down only on the click reads as "the button did nothing". */
.slot.slot-illegal { border-color: var(--danger); }
.slot-why {
  margin-top: 0.4rem; padding: 0.4rem 0.55rem; border-radius: 8px;
  background: rgba(192,57,43,0.22); border: 1px solid rgba(192,57,43,0.55);
  font-size: 0.8rem; line-height: 1.35;
}

/* The gap between "I've committed" and "the table has it". Short, but it has to be visible:
   an empty panel here is what made a refused lay-down look like nothing had happened. */
.lay-down-pending {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 0.6rem; padding: 0.75rem 1rem; border-radius: 12px;
  border: 2px solid rgba(240,208,137,0.5); background: rgba(224,160,0,0.12);
  font-weight: bold;
}
.lay-down-pending::before {
  content: ''; flex: 0 0 auto; width: 1.1rem; height: 1.1rem; border-radius: 50%;
  border: 2px solid rgba(240,208,137,0.35); border-top-color: #f0d089;
  animation: layDownSpin 0.8s linear infinite;
}
@keyframes layDownSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .lay-down-pending::before { animation: none; }
}

#action-bar {
  display: flex; align-items: stretch; gap: 0.4rem; flex-wrap: wrap;
  margin-bottom: 0.3rem; padding-inline: 0.5rem;
}
/* The menu button and the meld count under it, as one column, so the count can't push the
   button out of line with Discard beside it. */
/* Cards you park in the lay-down drawer leave your hand, and a hand three cards shorter than
   you remember with nothing on screen to account for them reads as cards lost - especially
   after the device has gone round the table and come back. So this line is always present.

   It lives at the foot of the hand, hard right, opposite the "drag to rearrange" hint - room
   that was already empty. Under the menu button it was setting that button's width, which is
   how a 44px touch target ended up shrunk to fit a caption. Positioned rather than in flow so
   it cannot push the hand's own layout around as its number changes width. */
.meld-card-count {
  position: absolute; right: 0.15rem; bottom: 0.1rem;
  font-size: 0.68rem; line-height: 1.1; text-align: right; white-space: nowrap;
  opacity: 0.6; color: var(--cream); pointer-events: none;
}
.meld-card-count.has-staged { opacity: 1; color: var(--warn); font-weight: bold; }
/* ~44px is the usual comfortable minimum for a thumb, not a cursor - these were sitting
   around 31-33px tall, which is exactly the kind of thing that reads as "clunky" without
   anyone being able to point at why. */
/* :not(.sort-btn) so this catch-all - solid gold, no border, content-driven width - doesn't
   also win over .sort-btn's own outlined/transparent/fixed-width look just because an ID
   selector always beats a class one, regardless of source order (same bug shape as the
   avatar-btn fix below - a broad "any button in this container" rule swallowing a more
   specific-looking sibling that never intended to opt in). */
#action-bar button:not(.sort-btn), .slot button, .meld-group button {
  padding: 0.7rem 0.9rem; min-height: 44px; border-radius: 8px; border: none;
  background: var(--gold); color: #1a1a1a; font-weight: bold; cursor: pointer;
}
#action-bar button:disabled, .slot button:disabled { opacity: 0.4; cursor: not-allowed; }
/* Shrunk to content (from "Discard Selected Card") back when the fixed chat bubble sat over
   the bar's right edge and the sort buttons needed the room. That bubble is gone now (table
   talk lives in the top-right corner instead) - grow back to fill the row so the primary
   action gets a real thumb target instead of leaving that space empty. */
#btn-discard-selected { flex: 1 1 auto; }

/* A quick reorder, not a mode - see sortArrangement's own doc comment. Same footprint as the
   table-menu button beside it (44px, outlined rather than solid gold) so the row reads as
   "one primary action, everything else secondary" at a glance. */
.sort-buttons { display: flex; gap: 0.3rem; flex: 0 0 auto; }
.sort-btn {
  padding: 0.3rem 0.4rem; min-height: 48px; width: 3.4rem; border-radius: 8px;
  border: 1px solid rgba(217,201,163,0.5); background: transparent; color: var(--cream);
  font-weight: bold; cursor: pointer; font-family: inherit; font-size: 0.82rem;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.1rem; line-height: 1.1; -webkit-tap-highlight-color: transparent;
}
.sort-btn-icon { font-size: 1.35rem; line-height: 1; }
.sort-btn:hover { background: rgba(255,255,255,0.09); }
.sort-btn:active { transform: scale(0.96); }

/* Everything but discarding lives one tap behind this - a plain "..." rather than another
   full-width button, so it reads as secondary next to Discard. */
.table-menu-btn {
  padding: 0.7rem 0.9rem; min-height: 44px; min-width: 44px; border-radius: 8px;
  border: 1px solid rgba(217,201,163,0.5); background: transparent; color: var(--cream);
  font-size: 1.2rem; line-height: 1; cursor: pointer; font-family: inherit;
}
.table-menu-btn:hover { background: rgba(255,255,255,0.09); }

/* Shared shell for the two popovers (table menu, chat) - a centered card over a dimmed
   backdrop, same weight as the paused/table-gone overlays so nothing here feels bolted on. */
/* dvh, not the default fixed-position box: on iOS Safari a position:fixed element is laid out
   against the LAYOUT viewport, which stays tall while the toolbars are on screen - so the
   bottom of a full-height card inside it sits behind the address bar. The padding clears the
   notch and the home indicator where there are any, and is otherwise just a margin. */
.slide-overlay {
  position: fixed; inset: 0; height: 100dvh; z-index: 92;
  display: flex; align-items: center; justify-content: center;
  padding:
    max(0.6rem, env(safe-area-inset-top)) max(0.6rem, env(safe-area-inset-right))
    max(0.6rem, env(safe-area-inset-bottom)) max(0.6rem, env(safe-area-inset-left));
  background: rgba(4,20,14,0.86); backdrop-filter: blur(6px);
  /* A drag on the dimmed area is a drag on the dimmed area. Without this the browser treats
     it as a scroll and quietly moves the page underneath instead - which looks, from the
     outside, exactly like the popup has frozen: you drag, nothing in the popup moves, and
     the table slides about behind it. Reported after switching to Messages and back, which
     is simply when a page is most likely to have somewhere left to scroll. */
  touch-action: none;
  overscroll-behavior: contain;
}
.slide-overlay-card {
  background: var(--felt-dark); border: 1px solid var(--gold); border-radius: 12px;
  padding: 1rem; max-width: 420px; width: 100%; max-height: 85vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 0.7rem;
  /* The card may still scroll itself when it genuinely has to (a long rules page), but its
     scroll stops at its own ends rather than handing the leftover to the page behind. */
  touch-action: pan-y;
  overscroll-behavior: contain;
}
#table-menu-buttons { display: flex; flex-direction: column; gap: 0.75rem; }
/* Two to a row. Flex rather than a two-column grid on purpose: when one half is hidden -
   the host-only controls for a guest, the online-only ones in pass-and-play - a hidden flex
   child is out of the flow entirely and the other one takes the full width back, instead of
   sitting in half a row beside a gap. */
.menu-pair { display: flex; gap: 0.4rem; align-items: stretch; }
.menu-pair > * { flex: 1 1 0; min-width: 0; }
/* Squared up: the icon sits over the label instead of beside it, which is what lets two of
   these live in the width of one and still be read at a glance on a phone. */
.menu-pair > .menu-row {
  flex-direction: column; justify-content: center; text-align: center;
  gap: 0.2rem; padding: 0.5rem 0.4rem; min-height: 58px;
  font-size: 0.84rem; line-height: 1.15;
}
.menu-pair > .menu-row .menu-row-icon { flex: 0 0 auto; font-size: 1.15rem; }
/* One base row for everything you can press in here, so the menu reads as a list of choices
   rather than a pile of buttons that each arrived from somewhere else. Left-aligned with the
   icon in a fixed column: a ragged left edge is what made this feel unsorted even when the
   order was fine. */
.menu-section { display: flex; flex-direction: column; gap: 0.4rem; }
.menu-section-head {
  margin: 0 0 0.1rem; font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(217,201,163,0.55); font-weight: bold;
}
.menu-row {
  display: flex; align-items: center; gap: 0.6rem; width: 100%; margin: 0 !important;
  padding: 0.65rem 0.8rem; min-height: 46px; text-align: left;
  border: 1px solid rgba(217,201,163,0.35); border-radius: 10px;
  background: rgba(0,0,0,0.18); color: var(--cream);
  font-family: inherit; font-size: 0.92rem; cursor: pointer;
}
.menu-row:hover { background: rgba(255,255,255,0.09); }
.menu-row-icon {
  flex: 0 0 1.4rem; display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.05rem; line-height: 1;
}
/* The one you opened the menu for. */
.menu-row--primary {
  border-color: var(--gold); background: rgba(217,201,163,0.16); font-weight: bold;
}
.menu-row--primary:hover { background: rgba(217,201,163,0.26); }
/* Irreversible. Sits at the top, furthest from the thumb, and says so in red. */
.menu-row--danger { color: var(--danger); border-color: rgba(192,57,43,0.55); }
.menu-row--danger:hover { background: rgba(192,57,43,0.18); }
/* Present, not inviting - the last resort when something has gone wrong. */
.menu-row--quiet { opacity: 0.72; }
.menu-row--quiet:hover { opacity: 1; }
/* Anything that isn't a single press - the timer, the invite, the sound switch - shares the
   row's own box so a group of them reads as one column, not three different widgets. */
.menu-panel {
  padding: 0.7rem 0.8rem; border-radius: 10px;
  border: 1px solid rgba(217,201,163,0.35); background: rgba(0,0,0,0.18);
}

/* Every pixel the overlay can spare, on all three tabs. Fixed to one size rather than
   shrinking to whatever the shortest tab needs, so the card doesn't visibly resize when you
   switch; and taken to the full height of the overlay's content box rather than a fraction of
   the viewport, because the longest tab (eleven levels plus a picture and the points key) was
   losing its last row off the bottom on a phone. Content is sized to read comfortably, not
   squeezed to dodge a scrollbar - #scoreboard-body just scrolls whenever a tab runs long. */
.scoreboard-card { max-width: 560px; height: 100%; max-height: 100%; overflow: hidden; }
/* Three of them now, so they wrap rather than shrinking the labels to nothing on a narrow
   phone. */
.scoreboard-tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; flex: 0 0 auto; }
#scoreboard-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
}
.scoreboard-tab {
  flex: 1 1 auto; background: transparent; color: var(--cream);
  border: 1px solid rgba(217,201,163,0.5); border-radius: 8px;
  padding: 0.5rem 0.7rem; cursor: pointer; font-family: inherit; font-size: 0.85rem; font-weight: bold;
}
.scoreboard-tab.active { background: var(--gold); color: #1a1a1a; border-color: var(--gold); }

/* position:relative so the corner decor anchors to this card's own corner (top edge,
   right by the Close button's side, tucked as high as it can go) rather than to the
   fixed full-viewport overlay behind it. */
.instructions-card { max-width: 560px; position: relative; }
#instructions-body h2 { margin: 0 0 0.3rem; font-size: 1.35rem; }
#instructions-body h3 { margin: 1.1rem 0 0.3rem; font-size: 1rem; color: var(--gold); }
#instructions-body p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; opacity: 0.95; }
.instructions-levels {
  list-style: none; margin: 0.3rem 0 0.8rem; padding: 0; display: flex; flex-direction: column; gap: 0.3rem;
}
.instructions-levels li {
  display: flex; gap: 0.6rem; align-items: baseline; padding: 0.3rem 0.5rem;
  border: 1px solid rgba(217,201,163,0.25); border-radius: 7px; font-size: 0.82rem;
}
/* The one place in the app that's genuinely us talking, not the game - a real photo and a
   real sign-off, set apart from the rules text above it rather than blending into the list. */
.instructions-signoff {
  margin-top: 1.3rem; padding-top: 1.1rem; border-top: 1px solid rgba(217,201,163,0.3);
  text-align: center;
}
.instructions-signoff-art {
  display: block; width: 100%; border-radius: 10px; margin-bottom: 0.7rem;
  box-shadow: 0 4px 14px rgba(0,0,0,0.4);
}
.instructions-signoff p { font-style: italic; opacity: 0.9; font-size: 0.88rem; }
.scoreboard-tab:not(.active):hover { background: rgba(255,255,255,0.09); }

/* Cumulative total, prominent above the growing hand list - the thing everyone actually
   wants at a glance, rather than only found by scrolling to the table's own bottom row. */
.scoreboard-leaderboard {
  display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 0.7rem; flex: 0 0 auto;
}
.leaderboard-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.6rem; border-radius: 8px; border: 1px solid rgba(217,201,163,0.3);
  background: rgba(255,255,255,0.04);
}
.leaderboard-row .avatar-bubble { font-size: 1.2rem; flex: 0 0 auto; }
/* The one place a chair number is ever written down. On the felt itself the seating is said
   entirely by where people are sitting relative to you - which is different for every player
   looking - so a number there would contradict what everyone else sees. Here, where the
   scorecard reports the table as it actually is, the fixed chair is exactly what's wanted. */
.leaderboard-seat {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.35rem; height: 1.35rem; border-radius: 50%;
  background: rgba(0,0,0,0.3); border: 1px solid rgba(217,201,163,0.4);
  font-size: 0.72rem; opacity: 0.8;
}
.leaderboard-name { font-weight: bold; }
.leaderboard-level { opacity: 0.7; font-size: 0.78rem; }
.leaderboard-total { margin-left: auto; font-size: 1.05rem; font-weight: bold; }
/* Pro mode only. Sits just before the total and reads quieter than it, because that is
   exactly the relationship: a number worth glancing at and worth nothing at all. */
.leaderboard-kudos {
  margin-left: auto; flex: 0 0 auto; font-size: 0.76rem; opacity: 0.6;
  white-space: nowrap;
}
.scoreboard-leaderboard.with-kudos .leaderboard-total { margin-left: 0.6rem; }
.leaderboard-row.you-col { border-color: var(--warn); }
.leaderboard-row.leader .leaderboard-total { color: var(--warn); font-weight: 900; }
/* Two players' cells in the very same hand-row can be different levels now (the stuck-on-
   level rule), so the level rides right alongside each score instead of a shared row label. */
.hand-level {
  display: inline-block; font-size: 0.68rem; opacity: 0.65; margin-right: 0.25rem;
}

.scoreboard-table-wrap { overflow-x: auto; flex: 0 0 auto; }
table.scoreboard-grid { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
table.scoreboard-grid th, table.scoreboard-grid td {
  border: 1px solid rgba(217,201,163,0.35); padding: 0.35rem 0.5rem; text-align: center;
}
table.scoreboard-grid th { background: rgba(255,255,255,0.06); font-weight: bold; }
/* The avatar docks directly above the player's name inside the same header cell, rather
   than a separate avatar+name row repeating what these headers already say. */
.scoreboard-th { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.scoreboard-th .avatar-bubble { font-size: 1.3rem; }
table.scoreboard-grid td.round-label { text-align: left; opacity: 0.75; font-size: 0.75rem; }
table.scoreboard-grid tr.current-round td { background: rgba(224,160,0,0.14); }
table.scoreboard-grid tr.totals-row td { font-weight: bold; background: rgba(255,255,255,0.06); }
table.scoreboard-grid td.not-played { opacity: 0.35; }
/* Your own column, outlined top to bottom - easy to find your own line down a row of
   otherwise-identical cells without having to reread every avatar first. */
table.scoreboard-grid th.you-col, table.scoreboard-grid td.you-col {
  border-left: 2px solid var(--warn); border-right: 2px solid var(--warn);
}
table.scoreboard-grid thead th.you-col { border-top: 2px solid var(--warn); }
table.scoreboard-grid tr.totals-row td.you-col { border-bottom: 2px solid var(--warn); }
/* Whoever's actually ahead - lowest total wins this game - gets their name and running
   total called out, not just a position at the front of the row. */
table.scoreboard-grid strong.leader { color: var(--warn); font-weight: 900; }
table.scoreboard-grid td.leader { color: var(--warn); font-weight: 900; }

/* Campfire-by-the-lake art above the level list - the same quiet camp scene the game
   already leans on for branding, just giving the Levels tab a bit of its own place to
   breathe instead of opening straight into a bare list of eleven requirements. */
.levels-banner {
  display: block; width: 100%; height: auto; object-fit: cover;
  /* Sized by ratio, not by a fixed height. A flat 110px was a letterbox slot roughly a fifth
     of the picture tall on a phone and a tenth of it on the 560px card a tablet gets - the
     wider the screen, the more of the shot it threw away, which is backwards. A ratio scales
     the frame with the card instead, so every device sees the same crop of the same picture;
     the source is 760x500 (1.52:1), so 1.7:1 keeps nearly all of it. The cap stops it
     dominating the panel on a big screen, and the floor keeps it a real picture rather than
     a stripe on a small one. */
  aspect-ratio: 1.7 / 1;
  max-height: 260px; min-height: min(150px, 20dvh);
  /* Only the cap and the floor crop anything now, and both crop from the middle out - so
     bias up a touch to keep the treeline and night sky, which are the shot. */
  object-position: center 38%;
  border-radius: 10px;
  margin-bottom: 0.6rem; box-shadow: 0 4px 14px rgba(0,0,0,0.4); flex: 0 0 auto;
}
.rules-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; flex: 0 0 auto; }
.rules-list li {
  display: flex; gap: 0.6rem; align-items: baseline; padding: 0.35rem 0.5rem;
  border: 1px solid rgba(217,201,163,0.3); border-radius: 8px; font-size: 0.85rem;
}
.rules-list li.current-round { border-color: var(--warn); background: rgba(224,160,0,0.1); }
.rules-list .rule-num {
  flex: 0 0 auto; font-weight: bold; opacity: 0.6; font-size: 0.75rem; min-width: 1.6rem;
}

/* ---------- Scoring key: what each card costs you if it's still in your hand ---------- */
.scoreboard-points { margin-top: 0.7rem; flex: 0 0 auto; }
.scoreboard-section-head {
  font-size: 0.88rem; margin: 0 0 0.4rem; padding-top: 0.5rem;
  border-top: 1px solid rgba(217,201,163,0.25);
}
.points-key { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.points-key li {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0.5rem;
  border: 1px solid rgba(217,201,163,0.3); border-radius: 8px; font-size: 0.85rem;
}
.points-chip {
  flex: 0 0 auto; min-width: 3.2rem; text-align: center; font-weight: bold;
  background: var(--cream); color: #111; border-radius: 6px; padding: 0.2rem 0.4rem;
  font-size: 0.8rem;
}
.points-chip.wild { background: rgba(224,160,0,0.9); }
/* Marks a rules heading as a setting rather than the game - so nobody reads the Jokers Wild
   section and thinks they've been playing Frustration wrong all these years. */
.rules-optional {
  margin-left: 0.4rem; padding: 0.1rem 0.4rem; border-radius: 999px;
  border: 1px solid rgba(217,201,163,0.45); background: rgba(217,201,163,0.1);
  font-size: 0.6rem; font-weight: normal; text-transform: uppercase;
  letter-spacing: 0.06em; vertical-align: middle; opacity: 0.8;
}
.points-label { flex: 1 1 auto; opacity: 0.9; }
.points-value {
  flex: 0 0 auto; font-weight: bold; color: var(--warn); font-size: 0.95rem;
}
.points-value::after { content: ' pts'; font-size: 0.7rem; font-weight: normal; opacity: 0.8; }

/* The one genuine ambiguity in laying a wild off a run: either end costs the same, so the
   player picks. Small and inline, right where the drop happened. */
.side-picker {
  position: fixed; z-index: 1001; display: flex; gap: 0.4rem;
  background: rgba(8,30,24,0.97); border: 1px solid var(--warn); border-radius: 10px;
  padding: 0.5rem; box-shadow: 0 10px 26px rgba(0,0,0,0.5);
}
.side-picker button {
  border: none; border-radius: 7px; padding: 0.5rem 0.8rem; cursor: pointer;
  font-family: inherit; font-size: 0.85rem; font-weight: bold;
  background: var(--gold); color: #1a1a1a;
}
.side-picker button:hover { filter: brightness(1.07); }

/* Closing a table is rare and irreversible, so it sits well away from "Deal it in" and looks
   nothing like it. Nobody should ever hit this on the way to somewhere else. */
.quiet-danger {
  display: block; margin: 1.2rem auto 0; padding: 0.45rem 0.9rem;
  border: 1px solid rgba(192,57,43,0.5); border-radius: 8px; background: transparent;
  color: rgba(224,190,180,0.8); font-family: inherit; font-size: 0.82rem; cursor: pointer;
}
.quiet-danger:hover { background: rgba(192,57,43,0.18); color: #f0d5cf; }

/* Reversible and low-stakes, unlike closing the table - lives near the gold accent used
   elsewhere for host controls, not the red used for the destructive button above. */
.lock-table-btn {
  display: block; margin: 1rem auto 0; padding: 0.45rem 0.9rem;
  border: 1px solid rgba(217,201,163,0.5); border-radius: 8px; background: transparent;
  color: var(--cream); font-family: inherit; font-size: 0.82rem; cursor: pointer;
}
.lock-table-btn:hover { background: rgba(217,201,163,0.15); }

#error-banner {
  background: rgba(192,57,43,0.25); border: 1px solid var(--danger); border-radius: 8px; padding: 0.5rem 0.75rem;
}

/* ---------- It's your turn ---------- */
/* Loud on arrival, then a quiet glow that stays for as long as the turn is yours - so you
   can look away at your cards and still know, at a glance, that you're the one holding
   everybody up. */
.turn-banner {
  position: fixed; left: 50%; top: 12vh; transform: translate(-50%, -14px);
  z-index: 92; pointer-events: none; text-align: center;
  padding: 0.9rem 2.2rem; border-radius: 14px;
  background: linear-gradient(160deg, rgba(20,70,52,0.97), rgba(8,38,28,0.97));
  border: 2px solid var(--warn); box-shadow: 0 12px 34px rgba(0,0,0,0.55);
  opacity: 0; transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.2,1,0.3,1);
}
.turn-banner.showing { opacity: 1; transform: translate(-50%, 0); }
.turn-banner strong {
  display: block; font-size: 1.9rem; line-height: 1.1; color: var(--warn);
  font-family: 'Palatino Linotype', Georgia, serif;
}
.turn-banner span { font-size: 0.85rem; opacity: 0.7; }

#table-view.my-turn #hand-dock {
  box-shadow: 0 -2px 0 var(--warn), 0 -14px 30px rgba(224,160,0,0.18);
}
@media (prefers-reduced-motion: reduce) {
  .turn-banner { transition: opacity 0.2s ease; }
}

/* ---------- The loon surfaces ---------- */
.loon-surface {
  position: fixed; inset: 0; z-index: 90; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 0.35s ease;
}
.loon-surface.running { opacity: 1; }
.loon-surface.draining { opacity: 0; transition: opacity 0.45s ease; }

/* The screen floods: a deep lake wells up and blurs whatever was behind it. */
.loon-surface .water {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 80% at 50% 120%, rgba(30,140,180,0.75) 0%, rgba(8,44,64,0.92) 55%, rgba(3,20,30,0.96) 100%);
  backdrop-filter: blur(7px) saturate(1.3);
  -webkit-backdrop-filter: blur(7px) saturate(1.3);
  transform: scaleY(0.02); transform-origin: bottom;
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.loon-surface.running .water { transform: scaleY(1); }

/* Wobbling light on the surface. */
.loon-surface .caustics {
  position: absolute; inset: -20%;
  background:
    repeating-linear-gradient(115deg, rgba(255,255,255,0.06) 0 12px, transparent 12px 34px),
    repeating-linear-gradient(65deg, rgba(255,255,255,0.05) 0 10px, transparent 10px 30px);
  animation: causticDrift 3.2s linear infinite;
  opacity: 0.7;
}
@keyframes causticDrift {
  0%   { transform: translate3d(0,0,0) scale(1); }
  50%  { transform: translate3d(-18px, 10px, 0) scale(1.04); }
  100% { transform: translate3d(0,0,0) scale(1); }
}

.loon-surface .wave { position: absolute; left: 0; width: 220%; height: 26vh; }
.loon-surface .wave-a { top: 8%;  animation: waveSlide 4.5s linear infinite; }
.loon-surface .wave-b { top: 2%;  animation: waveSlide 6.5s linear infinite reverse; opacity: 0.75; }
@keyframes waveSlide {
  from { transform: translateX(0); }
  to   { transform: translateX(-45%); }
}

.loon-surface .bubbles span {
  position: absolute; bottom: -8%; border-radius: 50%;
  background: rgba(220,245,255,0.35); border: 1px solid rgba(255,255,255,0.35);
  animation: bubbleRise 2.3s ease-in forwards;
}
@keyframes bubbleRise {
  from { transform: translateY(0) scale(0.7); opacity: 0; }
  20%  { opacity: 0.9; }
  to   { transform: translateY(-105vh) scale(1.1); opacity: 0; }
}

/* The loon itself: rises from the depths, looms at the camera, then sinks back. */
.loon-stage { position: relative; width: min(74vw, 380px); }
.loon-figure {
  width: 100%; height: auto; display: block;
  filter: drop-shadow(0 18px 30px rgba(0,0,0,0.6));
  transform: translateY(46vh) scale(0.2);
  opacity: 0;
}
.loon-surface.running .loon-figure { animation: loonRise 2.6s cubic-bezier(0.3, 0.9, 0.3, 1) forwards; }
@keyframes loonRise {
  /* Comes up beak-first out of the dark, breaches, shakes, then slides back under. */
  0%   { transform: translateY(46vh) scale(0.2)  rotate(-24deg); opacity: 0; }
  18%  { opacity: 1; }
  /* Opacity is pinned through the shake: without it CSS interpolates straight from 46%
     to the 100% fade-out, and the bird is half transparent exactly when it's biggest. */
  46%  { transform: translateY(2vh)  scale(1.02) rotate(-6deg); opacity: 1; }
  58%  { transform: translateY(-2vh) scale(1.12) rotate(3deg);  opacity: 1; }
  66%  { transform: translateY(-2vh) scale(1.12) rotate(-3deg); opacity: 1; }
  80%  { transform: translateY(-1vh) scale(1.1)  rotate(2deg);  opacity: 1; }
  100% { transform: translateY(30vh) scale(0.5)  rotate(10deg); opacity: 0; }
}

/* The ring the surface makes as the bird breaks through it. */
.breach-ring {
  position: absolute; left: 50%; bottom: 6%;
  width: 40%; aspect-ratio: 3 / 1; margin-left: -20%;
  border: 3px solid rgba(215,245,255,0.7); border-radius: 50%;
  opacity: 0; pointer-events: none;
}
.loon-surface.running .breach-ring { animation: breachRing 1.4s 0.42s ease-out forwards; }
@keyframes breachRing {
  from { transform: scale(0.25); opacity: 0.95; }
  to   { transform: scale(3.4);  opacity: 0; }
}

/* Water thrown off the bird as it breaks the surface. */
.loon-splash { position: absolute; inset: 0; pointer-events: none; }
.loon-splash span {
  position: absolute; left: 50%; bottom: 10%; border-radius: 50%;
  background: rgba(224,247,255,0.8); opacity: 0;
}
.loon-surface.running .loon-splash span { animation: splashFly 0.85s ease-out forwards; }
@keyframes splashFly {
  from { transform: translate(0, 0) scale(1); opacity: 0.9; }
  to   { transform: rotate(var(--a)) translateX(var(--d)) scale(0.3); opacity: 0; }
}

/* ---------- The warning dive (15s left on the clock) ---------- */
/* Positioned over the stock pile, because that is where the card backs - and therefore
   the loons - actually are. It ducks under and leaves rings behind. */
.loon-dive {
  position: fixed; z-index: 85; width: 0; height: 0; pointer-events: none;
}
.loon-dive-bird {
  position: absolute; left: 50%; top: 0;
  width: 92px; height: auto;
  margin-left: -46px; margin-top: -66px;
  filter: drop-shadow(0 6px 12px rgba(0,0,0,0.45));
  transform-origin: 50% 80%;
  opacity: 0;
}
.loon-dive.running .loon-dive-bird { animation: loonDuck 1.5s cubic-bezier(0.5, 0, 0.75, 0.3) forwards; }
@keyframes loonDuck {
  0%   { transform: translateY(6px) rotate(0deg) scale(0.9); opacity: 0; }
  14%  { transform: translateY(-6px) rotate(-6deg) scale(1); opacity: 1; }
  30%  { transform: translateY(-10px) rotate(-14deg) scale(1); opacity: 1; }
  55%  { transform: translateY(10px) rotate(46deg) scale(0.92); opacity: 1; }
  100% { transform: translateY(52px) rotate(86deg) scale(0.55); opacity: 0; }
}

.loon-ripple {
  position: absolute; left: 50%; top: 0;
  width: 74px; height: 26px; margin-left: -37px; margin-top: -13px;
  border: 2px solid rgba(140, 214, 240, 0.85); border-radius: 50%;
  opacity: 0;
}
.loon-dive.running .loon-ripple { animation: divePulse 1.1s ease-out forwards; }
.loon-dive.running .r1 { animation-delay: 0.72s; }
.loon-dive.running .r2 { animation-delay: 0.92s; }
.loon-dive.running .r3 { animation-delay: 1.12s; }
@keyframes divePulse {
  from { transform: scale(0.3); opacity: 0.9; }
  to   { transform: scale(2.6); opacity: 0; }
}

.loon-caption {
  position: absolute; bottom: 16%; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 0.5rem; text-align: center;
  background: rgba(3,20,30,0.72); border: 1px solid rgba(217,201,163,0.55);
  border-radius: 999px; padding: 0.45rem 0.95rem; color: var(--cream);
  font-size: 1rem; max-width: 92vw; white-space: nowrap;
}
.loon-caption-text { overflow: hidden; text-overflow: ellipsis; }
.loon-surface.scold .loon-caption { border-color: var(--warn); }
.loon-surface.cheer .loon-caption { border-color: #7fd08a; }
.loon-avatar {
  width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  border: 1px solid rgba(217,201,163,0.6);
}
.loon-avatar.emoji {
  display: inline-flex; align-items: center; justify-content: center; font-size: 1.15rem;
  background: rgba(255,255,255,0.08);
}

@media (prefers-reduced-motion: reduce) {
  .loon-surface .caustics, .loon-surface .wave, .loon-surface .bubbles span { animation: none; }
  .loon-surface.running .loon-figure { animation: none; transform: none; opacity: 1; }
  .loon-surface.running .breach-ring, .loon-surface.running .loon-splash span { animation: none; }
  .loon-dive.running .loon-dive-bird { animation: none; opacity: 1; transform: rotate(70deg); }
  .loon-dive.running .loon-ripple { animation: none; opacity: 0.5; }
}

/* ---------- Round-end cascade ---------- */
.celebration-layer {
  position: fixed; inset: 0; z-index: 80; pointer-events: auto; overflow: hidden;
  transition: opacity 0.6s ease;
}
.celebration-layer.fading { opacity: 0; }
.cascade-card, .confetti-bit {
  position: absolute; top: 0; left: 0; will-change: transform;
}
.confetti-bit { width: 8px; height: 12px; border-radius: 2px; }

#round-summary-screen, #awaiting-deal-screen {
  background: rgba(0,0,0,0.25); border-radius: 10px; padding: 1.5rem 1rem; text-align: center;
  max-height: calc(100dvh - 2rem); overflow-y: auto;
}
#game-over-screen { border-radius: 10px; overflow: hidden; max-height: calc(100dvh - 2rem); }
#game-over-screen[hidden] { display: none; }
/* The photo backdrop moved to the Instructions page (with the actual sign-off message) -
   this screen is the celebratory one, so it gets a lake-toned gradient and a scatter of the
   game's own fun avatar cast behind the standings instead of a real photo. */
.game-over-bg {
  position: relative; overflow: hidden;
  background: radial-gradient(ellipse at 50% 20%, #1a5a42 0%, #0d3626 55%, #06140f 100%);
  min-height: 100%; display: flex; align-items: center; justify-content: center;
}
.game-over-avatar-scatter { position: absolute; inset: 0; z-index: 0; }
.go-scatter {
  position: absolute; width: 64px; height: 64px; border-radius: 50%;
  opacity: 0.28; filter: saturate(0.9);
}
.go-scatter-0 { top: 4%;  left: 6%;  width: 78px; height: 78px; transform: rotate(-12deg); }
.go-scatter-1 { top: 8%;  right: 8%; transform: rotate(9deg); }
.go-scatter-2 { top: 30%; left: 2%;  width: 52px; height: 52px; transform: rotate(6deg); }
.go-scatter-3 { top: 62%; right: 3%; width: 70px; height: 70px; transform: rotate(-8deg); }
.go-scatter-4 { bottom: 6%; left: 10%; transform: rotate(14deg); }
.go-scatter-5 { bottom: 3%; right: 14%; width: 58px; height: 58px; transform: rotate(-6deg); }
.go-scatter-6 { top: 45%; left: 46%;  width: 46px; height: 46px; transform: rotate(10deg); opacity: 0.18; }
.go-scatter-7 { bottom: 32%; right: 42%; width: 50px; height: 50px; transform: rotate(-14deg); opacity: 0.18; }
.game-over-content {
  position: relative; z-index: 1;
  width: 100%; max-height: calc(100dvh - 2rem); overflow-y: auto;
  padding: 1.6rem 1.2rem; text-align: center; color: #fff;
}
.calling-it-a-night { font-size: 0.95rem; opacity: 0.92; margin: 0 0 1rem; }
/* The winner is the actual news here - a bigger cutout avatar and name, not just another
   row in a list further down. */
.winner-cutout { margin-bottom: 1.1rem; }
.winner-cutout h2 { margin: 0.5rem 0 0.15rem; font-size: 1.7rem; }
.winner-subtext { margin: 0; opacity: 0.85; font-size: 0.85rem; }
.late-join-note { margin: 0.35rem 0 0; opacity: 0.7; font-size: 0.75rem; font-style: italic; }
.avatar-bubble.winner-avatar {
  width: 84px; height: 84px; font-size: 2.6rem; margin: 0 0.2rem;
  border: 3px solid var(--gold); box-shadow: 0 6px 20px rgba(0,0,0,0.5);
}
.tidy-reveal { font-size: 0.85rem; opacity: 0.9; margin: 0 0 1rem; }
.game-over-standings {
  list-style: none; margin: 0 0 1.3rem; padding: 0; display: flex; flex-direction: column;
  gap: 0.35rem; max-width: 360px; margin-inline: auto;
}
.game-over-standings li {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.65rem;
  border-radius: 8px; background: rgba(255,255,255,0.1);
}
.game-over-standings li.winner-row { background: rgba(224,160,0,0.28); }
.game-over-standings li.you-row { border: 1px solid rgba(255,255,255,0.5); }
.gos-name { font-weight: bold; }
.gos-total { margin-left: auto; font-weight: bold; font-size: 1.05rem; }
.game-over-actions { display: flex; flex-direction: column; gap: 0.6rem; max-width: 320px; margin-inline: auto; }
.game-over-actions .link-back { color: #fff; opacity: 0.85; }
#awaiting-deal-screen[hidden] { display: none; }
.awaiting-deal-standings {
  list-style: none; margin: 1rem 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem;
  max-width: 320px; margin-inline: auto; text-align: left;
}
.awaiting-deal-standings li {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem;
  background: rgba(255,255,255,0.05); border-radius: 8px;
}
.awaiting-deal-standings li span { margin-left: auto; font-weight: bold; opacity: 0.85; }

/* Host's queue of people asking to join a table already under way. Sits above everything -
   whichever sub-screen happens to be showing - because a request doesn't pick a convenient
   moment to arrive. */
#join-requests-panel { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.6rem; }
#join-requests-panel[hidden] { display: none; }
.join-request {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  padding: 0.55rem 0.7rem; border-radius: 10px;
  border: 1px solid rgba(240,208,137,0.5); background: rgba(224,160,0,0.12);
}
.join-request-who { display: flex; align-items: center; gap: 0.4rem; }
.join-request-score { font-size: 0.82rem; opacity: 0.8; margin-left: auto; }
.join-request-approve, .join-request-decline {
  padding: 0.35rem 0.65rem; border-radius: 7px; border: none; font-weight: bold; cursor: pointer;
  font-family: inherit; font-size: 0.85rem;
}
.join-request-approve { background: var(--gold); color: #1a1a1a; }
.join-request-decline { background: rgba(255,255,255,0.08); color: var(--cream); }
.join-request-decline:hover { background: rgba(255,255,255,0.14); }
.round-winner-line { font-size: 1.05rem; }
.next-level-line {
  margin: 0.8rem 0; padding: 0.6rem 0.8rem; border-radius: 8px;
  background: rgba(224,160,0,0.12); border: 1px solid rgba(224,160,0,0.35); font-size: 0.9rem;
}
.breakdown-heading { margin: 1.4rem 0 0.6rem; font-size: 1rem; opacity: 0.9; }
.big-advance {
  margin-top: 1.4rem; padding: 0.9rem 1.2rem; width: 100%; font-size: 1.02rem;
  border: none; border-radius: 10px; background: var(--gold); color: #1a1a1a;
  font-weight: bold; cursor: pointer;
}
.big-advance:hover { filter: brightness(1.08); }
.big-advance:disabled { opacity: 0.5; cursor: not-allowed; filter: none; }

/* ---------- Round-summary ready check ---------- */
/* Online only - see renderRoundSummary. A row of chips, one per active seat, so nobody has to
   guess who the table is actually waiting on. */
.round-ready-list {
  display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: center; margin-top: 1rem;
}
.round-ready-chip {
  display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.6rem;
  border-radius: 999px; border: 1px solid rgba(217,201,163,0.4);
  background: rgba(255,255,255,0.04); font-size: 0.82rem; opacity: 0.75;
}
.round-ready-chip.is-ready {
  border-color: var(--warn); background: rgba(224,160,0,0.14); opacity: 1;
}
.round-ready-mark { font-weight: bold; color: var(--warn); }

/* ---------- Per-player hand breakdown ---------- */
.hand-breakdowns { display: flex; flex-direction: column; gap: 0.6rem; text-align: left; }
.hand-breakdown {
  border: 1px solid rgba(217,201,163,0.4); border-radius: 8px; padding: 0.5rem 0.6rem;
  background: rgba(255,255,255,0.04);
}
.hand-breakdown.went-out { border-color: #7fd08a; background: rgba(127,208,138,0.12); }
.breakdown-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; }
.breakdown-total { font-weight: bold; color: var(--warn); }
.went-out-tag { color: #7fd08a; font-weight: bold; font-size: 0.85rem; }
/* Left the table for good - frozen score, not a round they simply haven't played yet. */
.dnf-tag { color: var(--warn); font-weight: bold; font-size: 0.85rem; opacity: 0.85; }
.hand-breakdown.dnf { opacity: 0.6; }
.breakdown-empty { margin: 0.3rem 0 0; opacity: 0.6; font-size: 0.85rem; }
.breakdown-hidden { margin: 0.3rem 0 0; opacity: 0.55; font-size: 0.85rem; font-style: italic; }
.breakdown-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.4rem; }
.breakdown-card {
  display: inline-flex; flex-direction: column; align-items: center; min-width: 38px;
  background: var(--cream); border: 1px solid #999; border-radius: 5px; padding: 0.15rem 0.25rem;
  line-height: 1.1;
}
.breakdown-card.red .bc-face { color: var(--danger); }
.breakdown-card.black .bc-face { color: #111; }
.breakdown-card .bc-face { font-weight: bold; font-size: 0.9rem; }
.breakdown-card .bc-pts {
  font-size: 0.65rem; color: #fff; background: #444; border-radius: 3px;
  padding: 0 0.25rem; margin-top: 0.1rem;
}
table.scoreboard { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.scoreboard th, table.scoreboard td { border: 1px solid var(--gold); padding: 0.4rem 0.6rem; text-align: center; font-size: 0.85rem; }
table.scoreboard th { background: rgba(255,255,255,0.08); }
table.scoreboard tr.winner td { background: rgba(76,175,80,0.25); font-weight: bold; }
table.scoreboard tr.dnf-row td { opacity: 0.6; }


/* ---------- The drawn card's flight from pile to hand ---------- */

/* Fixed and inert: the flight has to cross the table, the seats and the hand dock without
   any of them reflowing or intercepting it. */
.draw-flight-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
  overflow: hidden;
}

.draw-flyer {
  position: absolute;
  perspective: 700px;
  will-change: transform, opacity;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45));
}

.draw-flyer-spin {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}

/* Both faces occupy the same space; which one you see is decided by the Y rotation. */
.draw-flyer-side {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: 6px;
  overflow: hidden;
}

.draw-flyer-front { transform: rotateY(180deg); }
.draw-flyer-back  { transform: rotateY(0deg); }

@media (prefers-reduced-motion: reduce) {
  .draw-flight-layer { display: none; }
}

/* A card is in the air and aimed at a seat, so the strip stops answering to a thumb until it
   lands (see flightsAloft in main.ts). touch-action is what actually stops the pan; the
   scroll position itself is left exactly where it was, so nothing jumps when it unlocks. */
.ring-scroll.flight-locked {
  touch-action: none;
  scroll-snap-type: none;
}

/* Why the melds in the blown-up view aren't accepting anything yet. Laying off before you
   have opened your own melds is not allowed (see the LAY_OFF handler), and the overlay used
   to simply refuse the drop with no explanation at all - which reads as a broken drag rather
   than as a rule. */
.meld-focus-locked {
  margin: 0 0 0.5rem; padding: 0.4rem 0.6rem; border-radius: 8px;
  background: rgba(217,201,163,0.1); border: 1px dashed rgba(217,201,163,0.4);
  color: var(--cream); font-size: 0.8rem; line-height: 1.3; text-align: center;
}


/* A meld's box is its card track times whatever that tier scales the cards by. Written this
   way round because the track is no longer a constant: a meld that has been shed onto all
   hand widens its own (see condensedMeldLefts and --meld-track), and the box has to follow
   or the cards would simply hang out of it again. */
.meld-scale { width: calc(var(--meld-track, 160px) * var(--meld-cards-scale, 1)); }

/* `hidden` is only a UA default of display:none, and .menu-section sets its own display - so
   without this the attribute is quietly overruled and an empty section keeps its heading. */
.menu-section[hidden] { display: none; }

/* A pair with only one of its two rows showing gets the plain single-line treatment back -
   icon beside the label rather than stacked above it. The stacked form earns its extra height
   by putting two controls on one line; a lone survivor spending it just makes the menu taller,
   which on the smallest phone was enough to push the whole card back into scrolling. */
.menu-pair:has(> .menu-row[hidden]) > .menu-row:not([hidden]) {
  flex-direction: row;
  justify-content: flex-start;
  text-align: left;
  min-height: 44px;
  gap: 0.5rem;
}

/* ---------- Shared chrome for the two ways into a game ---------- */

/* Pass-and-play and online tables do the same job - get you to a table - so they wear the
   same head: a bar with the way out on the left and the rules on the right, then the
   wordmark, then the page's own name. Pinned to the top of the page rather than floating in
   from the flow, because the whole complaint was dead space above the fold. */
.page-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin: 0 0 0.35rem;
}
.page-bar-btn {
  background: none; border: none; color: var(--gold); cursor: pointer;
  font-family: inherit; font-size: 0.95rem; padding: 0.3rem 0.1rem;
  min-height: 36px; white-space: nowrap;
}
.page-bar-btn:active { opacity: 0.6; }

/* The name of the page, left where the eye starts, with the roundel holding the right-hand
   end the way the watermark does on a card face - present, quiet, not competing. */
.page-title-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; margin: 0 0 0.5rem;
  position: relative;
}
.page-title-row h2 {
  margin: 0; text-align: left; flex: 1 1 auto; min-width: 0;
  font-size: 1.35rem; line-height: 1.15;
  position: relative; z-index: 1;   /* the title reads over the mark, never under it */
}
/* The house mark, sunk into the page rather than sitting in the row. Taken out of flow on
   purpose: as a flex item it competed for width with the title beside it, so every px it grew
   came straight out of the heading. Absolute means it can be as large as it likes - spilling
   the row's own height into the empty space above and below - without moving one thing. */
.page-title-roundel {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 96px; height: 96px; opacity: 0.4;
  object-fit: contain; pointer-events: none; z-index: 0;
}

/* Tighter than the old centred stack - the mark says whose table this is, it doesn't need to
   take the top third of a phone to do it. */
#lobby-screen .page-wordmark,
#setup-screen .page-wordmark {
  width: min(150px, 42%); margin: 0 0 0.15rem;
}

/* The tip jar is the last thing on both of these pages and nowhere else. */
#setup-screen .tip-jar { margin-top: 1.2rem; }

/* ---------- The games page ---------- */

/* No page title above these: the two tabs are the title now, sized to carry it, sitting
   straight under the bar. They also got taller, because they are the biggest decision on the
   page and were reading as a filter strip on something else. */
#active-tables-screen { padding: 0.6rem 1rem 2rem; }
#active-tables-screen .games-tabs { margin: 0.2rem 0 0.5rem; gap: 0.5rem; }
#active-tables-screen .games-tab {
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  gap: 0.1rem; min-height: 62px; padding: 0.6rem 0.75rem; text-align: left;
  font-size: 1.02rem; line-height: 1.15;
}
.games-tab-name { display: block; }
/* How many are in there, on the tab itself - the number people came to the page for, said
   before they have to pick a side to find it out. Blank rather than zero: an empty tab is
   already saying nothing is in it, and a "0" just adds noise to that. */
.games-tab-count {
  font-size: 0.72rem; font-weight: normal; opacity: 0.75; color: var(--gold);
  min-height: 0.9rem;
}
.games-tab-count:not(:empty)::after { content: ' waiting'; opacity: 0.8; }

/* The line under the tabs explaining what this half is - a header between the choice and the
   list, rather than a paragraph the page opens with. */
.games-blurb {
  margin: 0.1rem 0 0.6rem; font-size: 0.78rem; line-height: 1.35; opacity: 0.7;
}

/* Where games will sit, drawn as empty places rather than described in a sentence. A page
   that has never held a game should still look like the page it is going to be. */
.game-slots { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.2rem 0 0.9rem; }
.game-slot {
  display: flex; align-items: center; gap: 0.7rem;
  min-height: 56px; padding: 0.6rem 0.75rem; border-radius: 12px;
  border: 1px dashed rgba(217,201,163,0.22); background: rgba(255,255,255,0.02);
}
.game-slot-mark {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  border: 1px dashed rgba(217,201,163,0.28);
}
.game-slot-lines { flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.4rem; }
.game-slot-lines i { display: block; height: 7px; border-radius: 4px; background: rgba(217,201,163,0.12); }
.game-slot-lines i:last-child { width: 45%; background: rgba(217,201,163,0.08); }

#active-tables-screen .empty-note { font-size: 0.82rem; line-height: 1.4; text-align: center; }
#active-tables-screen .empty-note strong { display: block; margin-bottom: 0.2rem; opacity: 0.9; }
@media (max-width: 700px) and (max-height: 720px) {
  .studio-mark-footer { max-width: 150px; margin-top: 1rem; }
}

/* The studio mark closes this page the same way it closes the home screen, in the same place
   and at the same size - so moving between the two feels like the page changed underneath a
   fixed mark rather than everything moving at once. */
.studio-mark-footer {
  width: 100%; max-width: 196px; height: auto; display: block; margin: 1.6rem auto 0;
  border-radius: 12px; padding: 0.6rem 0.7rem;
  background: rgba(244,239,227,0.94); box-shadow: 0 4px 14px rgba(0,0,0,0.35);
}

/* A joker is all art - no pip, no suit glyph, and a corner that says J over a star rather
   than a rank over a suit it does not have. The drawing is the card, so it fills the face
   instead of sitting behind an index the way the J/Q/K illustrations do. */
.card.face.joker .joker-art {
  position: absolute; inset: 2px; border-radius: 7px;
  background-size: cover; background-position: center; background-repeat: no-repeat;
}
