/* Emily & Paul — a modern Canadian-Greek wedding.
   Midnight blue, ivory, gold. Mobile-first; desktop is the same phone column. */

/* Dark is the default, and `.midnight` is the same palette as a scope anything
   can opt into — the 69,000-point takeover is a blackout moment and stays dark
   on a phone set to light mode. Declared here rather than copied there, so a
   retuned gold cannot leave the two disagreeing. */
:root, .midnight {
  --midnight: #0A1128;
  --gold: #D4AF37;
  --gold-lit: #EBCF7A;
  /* Gold as *text*. Light mode needs a much deeper one than the gradient end,
     so the two cannot be the same token. */
  --gold-ink: var(--gold-lit);
  --olive: #8CA06A;
  --win: #6FBF8B;
  --loss: #D9736B;
  /* The leaderboard's first three places. They are the chip a rank sits in. */
  --silver: #C3C9D6;
  --bronze: #C08A5E;

  --bg: #070C1E;
  --surface: #0F1834;
  --surface-2: #16224A;
  --line: rgba(212,175,55,.22);
  --line-soft: rgba(255,255,255,.09);
  --ink: #F4F6FB;
  --ink-dim: #A7B2CE;
  --ink-faint: #828EB2;
  --shadow: 0 18px 40px -18px rgba(0,0,0,.85);

  --r: 18px;              /* cards and panels */
  --r-md: 14px;           /* buttons, inputs, rows — anything you tap */
  --r-sm: 12px;
  --tap: 52px;
  --col: 460px;
  --pad: 18px;              /* the gutter every content screen uses */
  --odds-w: 104px;          /* the odds box, wherever odds are typed */
  --sans: "Avenir Next", "Segoe UI", system-ui, -apple-system, sans-serif;
  --serif: "Hoefler Text", "Iowan Old Style", Palatino, Georgia, serif;

  /* The notch, the home indicator and the curved corners. Named once so every
     edge can respect them without repeating env() twelve times. */
  --sat: env(safe-area-inset-top, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --gut-l: calc(var(--pad) + var(--sal));
  --gut-r: calc(var(--pad) + var(--sar));
  --bar-top: 0px;           /* the notch, for whichever bar is at the top */

  /* The motion system. Four durations and two curves, and nothing in the app
     picks its own — see public/motion.js, which holds the same numbers for the
     handful of moves that have to be driven from script. Quick on purpose: a
     tipsy guest at midnight should never be waiting on an animation. */
  --m-press: 110ms;
  --m-fast: 150ms;
  --m: 200ms;
  --m-slow: 260ms;
  --m-ease: cubic-bezier(.2,.7,.3,1);      /* out — everything arriving */
  --m-ease-in: cubic-bezier(.45,0,.85,.35); /* in — everything leaving */
  /* How far a control gives under a thumb. Three sizes, because the same scale
     on a 44px chip and a full-width bar are not the same movement — and one
     slot, `--press`, which each family of controls fills from them. */
  --press-sm: .94;
  --press-md: .97;
  --press-wide: .985;
  --press: var(--press-md);

  /* Guess Who's two ends. Drawn from the photographs themselves — his navy
     shirt, her terracotta wall — and deliberately neither gold nor the win/loss
     pair: gold is "your pick" everywhere else in the app, and green-and-red on
     a slider would read as right-and-wrong hours before anybody is scored. */
  --gw-paul: #7B9CE8;
  --gw-emily: #C489C8;
}

:root[data-theme="light"] {
  --bg: #F7F2E7;
  --surface: #FFFFFF;
  --surface-2: #FBF6EA;
  --line: rgba(160,124,26,.30);
  --line-soft: rgba(10,17,40,.10);
  --ink: #131C38;
  --ink-dim: #56618A;
  --ink-faint: #616A88;
  --gold: #A97F1B;
  --gold-lit: #C9992C;
  --gold-ink: #8A6712;
  --win: #1F7A47;
  --loss: #B8443B;
  --silver: #8E97A8;
  --bronze: #9C6B41;
  --shadow: 0 14px 34px -18px rgba(19,28,56,.32);
  --gw-paul: #33549E;
  --gw-emily: #8E4A93;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.5 var(--sans);
  overscroll-behavior-y: none;
}

/* A whisper of gold light at the top of the page, like candlelight. It is the
   column's light, not the window's: the sticky header and tab bar are opaque
   and only as wide as the column, so a glow running the full width of a desktop
   window drew them as a lighter box with two hard vertical edges down the page.
   On a phone the column IS the window and nothing changes. */
body::before {
  content: ""; position: fixed; inset: 0 auto auto 50%; transform: translateX(-50%);
  width: min(100%, var(--col)); height: 42vh; pointer-events: none; z-index: 0;
  background: radial-gradient(120% 100% at 50% 0%, rgba(212,175,55,.13), transparent 65%);
}
:root[data-theme="light"] body::before { background: radial-gradient(120% 100% at 50% 0%, rgba(169,127,27,.10), transparent 65%); }

#app { position: relative; z-index: 1; max-width: var(--col); margin: 0 auto; min-height: 100dvh; display: flex; flex-direction: column; transition: opacity var(--m-slow) var(--m-ease); }
/* The first paint arrives rather than appearing — one fade for the whole
   app, which is also why nothing inside it animates on the first draw
   (public/motion.js -> primed). */
#app.booting { opacity: 0; }

/* Chrome keeps the height it asks for; the body of the page takes the rest.
   Without this the sticky header shrank under a long page and the offset it
   stuck at no longer matched its own height — the gap content bled through. */
#app > * { flex: none; }
#app > .screen, #app > .welcome, #app > .couple { flex: 1; }

h1, h2, h3 { font-family: var(--serif); font-weight: 600; margin: 0; letter-spacing: .01em; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }
input { font: inherit; }

/* Keyboard focus used to be invisible on every control in the app. One ring,
   once, on anything you can land on. */
:where(button, input):focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* ---------- icons -------------------------------------------------------- */

/* One set, one grid, one stroke weight — see public/icons.js. An icon is sized
   by the font-size of whatever holds it and takes that thing's colour, so a
   badge, a button and a tab each get the right icon with no rule of their own.
   `.f` is the handful of parts that are filled rather than stroked. */
.icn {
  width: 1em; height: 1em; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.75;
  stroke-linecap: round; stroke-linejoin: round;
  vertical-align: -.14em;
}
.icn .f { fill: currentColor; stroke: none; }
.icn.sm { width: .95em; height: .95em; }

/* ---------- Greek key + olive ------------------------------------------ */

.greek-key {
  height: 12px; flex: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='12' viewBox='0 0 16 12'%3E%3Cpath d='M0 10.6H16M3 10.6V3h9.5v4.6H7V5.6' fill='none' stroke='%23D4AF37' stroke-width='1.25' stroke-linecap='square'/%3E%3C/svg%3E");
  /* Centred, so the band is cut by the same amount at both ends instead of
     running out mid-motif on the right. */
  background-repeat: repeat-x; background-size: auto 12px; background-position: 50% 0;
  opacity: .6;
}
:root[data-theme="light"] .greek-key { filter: saturate(.7) brightness(.72); opacity: .7; }
.greek-key.faint { opacity: .28; }

/* A claimed name is still tappable — it opens the log-in door with the name in
   it — so it reads as quieter rather than dead. */
.picker-list button.is-taken { color: var(--ink-dim); }
.picker-list button.is-taken .taken { color: var(--gold-ink); }

.rule { display: flex; align-items: center; gap: 12px; margin: 22px 0 14px; }
.rule::before, .rule::after { content: ""; height: 1px; flex: 1; background: linear-gradient(90deg, transparent, var(--line), transparent); }
.rule span { font: 600 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--gold-ink); }
.rule:first-child { margin-top: 4px; }
/* The Bets tab's section headings. Which part of the night you are scrolling
   through stays on the screen, so thirty-three cards read as five short lists
   rather than one wall. `--appbar` is published by public/app.js -> appbar; the
   fallback is the header with no notch and no "viewing as" banner. The z-index
   sits under the header's 30 so a heading slides away beneath it. */
.rule.stick {
  position: sticky; top: calc(var(--appbar, 72px) - 1px); z-index: 10;
  margin: 26px 0 12px; padding: 10px 0 8px; background: var(--bg);
}
.rule.stick:first-child { margin-top: 0; }

/* ---------- welcome ----------------------------------------------------- */

.welcome { min-height: 100dvh; display: flex; flex-direction: column; }
.hero { position: relative; flex: 1; display: flex; flex-direction: column; justify-content: flex-end; min-height: 58dvh; overflow: hidden; }
.hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 40% 28%; }
.hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(7,12,30,.42) 0%, rgba(7,12,30,.05) 30%, rgba(7,12,30,.72) 72%, var(--bg) 100%);
}
:root[data-theme="light"] .hero::after { background: linear-gradient(180deg, rgba(20,30,60,.30) 0%, rgba(20,30,60,0) 30%, rgba(247,242,231,.72) 62%, rgba(247,242,231,.96) 82%, var(--bg) 100%); }
.hero-body { position: relative; z-index: 2; padding: 0 calc(24px + var(--sar)) 26px calc(24px + var(--sal)); text-align: center; }
.monogram { font-family: var(--serif); font-size: 15px; letter-spacing: .42em; color: var(--gold-lit); text-transform: uppercase; margin-bottom: 10px; text-shadow: 0 2px 12px rgba(0,0,0,.6); }
/* Light mode puts near-ivory behind these words, and the lit gold that reads
   beautifully on midnight all but vanishes on it. The deep gold, a heavier
   weight and a wider scrim under the whole hero-body are what make it legible
   without turning the photo into a grey box. */
:root[data-theme="light"] .monogram { color: var(--gold-ink); font-weight: 700; text-shadow: 0 1px 2px rgba(247,242,231,.9); }
.hero h1 { font-size: clamp(34px, 11vw, 46px); line-height: 1.06; text-shadow: 0 3px 22px rgba(0,0,0,.55); }
:root[data-theme="light"] .hero h1 { text-shadow: none; }
.hero .when { margin-top: 10px; font-size: 13px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-dim); }

.olive { display: block; margin: 18px auto 0; width: 132px; height: 28px; }
.olive .stem { stroke: var(--olive); fill: none; stroke-width: 1.5; stroke-linecap: round; }
.olive ellipse { fill: var(--olive); opacity: .92; }
.olive circle { fill: var(--gold); }

.welcome-actions { padding: 20px var(--gut-r) calc(30px + var(--sab)) var(--gut-l); display: grid; gap: 12px; }
/* The quiet third way in sits under two full-width buttons that centre their
   own words, so it centres too — left against those two reads as a stray. */
.welcome-actions .textbtn { justify-content: center; }
.welcome-tag { text-align: center; color: var(--ink-dim); font-size: 14.5px; margin: 4px 0 16px; padding: 0 6px; }

/* ---------- buttons ----------------------------------------------------- */

.btn {
  min-height: var(--tap); border-radius: var(--r-md); padding: 14px 18px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-weight: 650; font-size: 16.5px; letter-spacing: .01em;
  border: 1px solid var(--line-soft); background: var(--surface); color: var(--ink);
  text-decoration: none;                 /* the 404 page's way back is a link */
}
.btn.gold {
  background: linear-gradient(160deg, var(--gold-lit), var(--gold));
  color: #17120A; border-color: transparent;
  box-shadow: 0 10px 26px -12px rgba(212,175,55,.7), inset 0 1px 0 rgba(255,255,255,.34);
}
/* Ghost is declared after gold and matches just as hard, so a button wearing
   both — the picked side in Guess Who's answer key — used to come out looking
   exactly like the one that was not picked. Ghost now steps aside for gold
   rather than depending on which rule was written last. */
.btn.ghost:not(.gold) { background: transparent; border-color: var(--line); color: var(--ink); }
.btn.wide { width: 100%; }
.btn:disabled { opacity: .45; }
.btn.danger { color: var(--loss); border-color: rgba(217,115,107,.45); }

.link { color: var(--gold-ink); font-weight: 600; text-decoration: none; border-bottom: 1px solid var(--line); padding-bottom: 1px; }
/* No side padding: a text button at the edge of a screen has to line up with
   the heading above it, and it still needs a thumb-sized target. */
.textbtn {
  color: var(--ink-dim); font-size: 14.5px; padding: 11px 0; min-height: 44px;
  display: inline-flex; align-items: center; gap: 6px;
  text-decoration: underline; text-underline-offset: 3px;
}
.screen > .textbtn { margin-top: 4px; }

/* ---------- forms / picker ---------------------------------------------- */

.screen { padding: 20px var(--gut-r) 40px var(--gut-l); }
/* Welcome, claim and log-in have no header above them, so they own the notch;
   a screen that is the last thing on the page owns the home indicator. The tab
   bar pads for it itself, so screens above one must not. */
#app > .screen:first-child { padding-top: calc(20px + var(--sat)); }
#app > .screen:last-child, .couple > .screen { padding-bottom: calc(40px + var(--sab)); }
/* The not-found page (public/404.html) is the only screen with no bar above it,
   so it sits in the middle of the window rather than against the top edge. */
.screen.notfound { display: flex; flex-direction: column; justify-content: center; }
.screen h2 { font-size: 26px; margin-bottom: 6px; }
.screen .sub { color: var(--ink-dim); font-size: 15px; margin: 0 0 18px; }

.field { display: block; margin-bottom: 14px; }
.field span { display: block; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); margin-bottom: 7px; }
.field input, .picker > input, .oddsrow input, .compose input, input.wide {
  width: 100%; min-height: var(--tap); padding: 13px 15px; border-radius: var(--r-md);
  background: var(--surface); color: var(--ink); border: 1px solid var(--line-soft);
  -webkit-appearance: none; appearance: none;
}
.field input::placeholder, .picker > input::placeholder, .compose input::placeholder, input.wide::placeholder { color: var(--ink-faint); }
.field input:focus, .picker > input:focus, .oddsrow input:focus, .compose input:focus, input.wide:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(212,175,55,.16); }

/* An outcome and what it pays belong on one line — the odds box never needs
   the full width, and five stacked fields is a lot of thumb. Adding a bet
   labels both halves; the odds editor names the outcome and hands you the box.
   One width either way. */
.field-row { display: grid; grid-template-columns: 1fr var(--odds-w); gap: 10px; margin-bottom: 14px; }
.field-row .field { margin-bottom: 0; }
.oddsrow { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.field.pays input, .oddsrow input {
  padding-inline: 10px; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums;
}
.oddsrow input { flex: none; width: var(--odds-w); min-height: 48px; }

.picker { position: relative; }
.picker-list {
  margin-top: 8px; border: 1px solid var(--line-soft); border-radius: var(--r-md);
  background: var(--surface); overflow: hidden; max-height: 46dvh; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* Inside a panel the list is one control among several: show five rows and a
   sliver of the sixth, so it reads as scrollable without eating the screen. */
.panel .picker-list { max-height: 264px; }
.picker-list button {
  display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 50px; padding: 12px 15px; text-align: left; font-size: 16px;
  border-bottom: 1px solid var(--line-soft);
}
.picker-list button:last-child { border-bottom: 0; }
.picker-list button:active { background: var(--surface-2); }
.picker-list button[disabled] { color: var(--ink-faint); }
.picker-list .taken { font-size: 11px; font-weight: 700; color: var(--ink-faint); letter-spacing: .1em; text-transform: uppercase; flex: none; }
.picker-empty { padding: 18px 15px; color: var(--ink-dim); font-size: 15px; }

.chosen {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-radius: var(--r-md); background: var(--surface-2);
  border: 1px solid var(--line); margin-bottom: 16px;
}
.chosen b { font-family: var(--serif); font-size: 19px; font-weight: 600; min-width: 0; }
.chosen .textbtn { flex: none; }

/* ---------- app chrome --------------------------------------------------- */

/* The gold "viewing as" banner and the header are one sticky block. They can
   never drift apart or leave a strip of page showing between them. */
.appbar { position: sticky; top: 0; z-index: 30; }
/* Whichever of the two is at the very top of the screen owns the notch. */
.appbar > :first-child { --bar-top: var(--sat); }

.topbar {
  background: var(--bg);
  background-image: linear-gradient(180deg, rgba(212,175,55,.10), rgba(212,175,55,0));
  border-bottom: 1px solid var(--line-soft);
}
:root[data-theme="light"] .topbar { background-image: linear-gradient(180deg, rgba(169,127,27,.09), rgba(169,127,27,0)); }
.topbar-in {
  display: flex; align-items: center; gap: 12px;
  min-height: calc(62px + var(--bar-top));
  padding: calc(10px + var(--bar-top)) calc(16px + var(--sar)) 10px calc(16px + var(--sal));
}
.topbar .who { flex: 1; min-width: 0; }
.topbar .who b { display: block; font-family: var(--serif); font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .who small { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-dim); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
.topbar .who small .icn { width: 13px; height: 13px; color: var(--gold-ink); }
.balance { text-align: right; flex: none; }
.balance b { color: var(--gold-ink); }
.figure b, .balance b, .stats b { display: block; font-size: 20px; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; }
.figure small, .balance small, .stats small { display: block; margin-top: 3px; font-size: 10.5px; line-height: 1.2; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.iconbtn {
  width: 44px; height: 44px; border-radius: var(--r-md); display: grid; place-items: center;
  border: 1px solid var(--line-soft); flex: none; color: var(--ink-dim);
}
.iconbtn .icn { width: 19px; height: 19px; }

/* The foot of the screen is one sticky block, for the same reason the banner
   and the header are: two of them sticking separately leaves a strip of page
   showing between them. On the chat tab the composer rides in it, directly
   above the tab bar and always on screen — public/app.js -> dock. */
.dock { position: sticky; bottom: 0; z-index: 20; }
/* Safari does not shrink the page for the soft keyboard, it slides one over the
   bottom of it. `--kb` is what is covered — public/app.js -> watchKeyboard — so
   the box you are typing into is never the thing underneath. */
.dock.chatting { transform: translateY(calc(-1 * var(--kb, 0px))); }

.tabs {
  position: relative;
  /* One column per tab and all of them equal: the gold mark below divides the
     bar by the same --tab-n, so the buttons have to match or it sits off the
     icon on every tab but the middle. Equal columns also stop the tab you are
     standing on from widening its own column with its heavier word. */
  display: grid; grid-template-columns: repeat(var(--tab-n), 1fr);
  background: var(--bg);
  border-top: 1px solid var(--line-soft);
  padding: 0 var(--sar) var(--sab) var(--sal);
}
.tabs button {
  position: relative;
  min-height: 58px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint);
}
/* The tab you are on is marked at the top edge of the bar, where the eye is
   already going, rather than by colour alone. One mark for the whole bar
   rather than one per button, so moving tabs slides it across instead of
   putting one out and another in — public/motion.js -> slide drives the move,
   because the bar is rebuilt on every draw and cannot transition from its own
   last value. It is a column wide and the gold sits centred inside it,
   so it lands on the icon whatever the bar is holding. */
.tabmark {
  position: absolute; top: 0; height: 2px; pointer-events: none;
  left: var(--sal); width: calc((100% - var(--sal) - var(--sar)) / var(--tab-n));
}
.tabmark::before {
  content: ""; display: block; width: 30px; height: 2px; margin: 0 auto;
  border-radius: 0 0 2px 2px; background: var(--gold);
}
.tabs button .ic { width: 22px; height: 22px; stroke-width: 1.7; }
/* Live sits in the middle of the bar and is the tab the room comes back to all
   night, so it is drawn a shade warmer and a hair heavier than its neighbours.
   Same column, same row, same word in the same place: the emphasis is colour
   and line-weight, which the bar already speaks. The two extra pixels are an
   optical correction — the radio glyph is mostly air, so it reads small beside
   the dice and the speech bubble at the same box. */
.tabs button.live { color: color-mix(in srgb, var(--gold-ink) 62%, var(--ink-faint)); }
.tabs button.live .ic { width: 24px; height: 24px; stroke-width: 2; }
/* The tab you are standing on is the bold one. It comes AFTER Live above and
   ties with it on specificity, so standing on Live takes the full gold off
   this rule rather than needing one of its own. */
.tabs button[aria-current="true"] { color: var(--gold-ink); font-weight: 600; }
.tabs button[aria-current="true"] .ic { transform: translateY(-1px); }
/* How many side bets have gone up since this phone last opened the Bets tab.
   Gold on midnight, on the icon rather than beside the word, because that is
   where the badge is on every other app in the room's pockets. It goes the
   moment they look — public/newbets.js. */
.tabs .pip {
  position: absolute; top: 6px; left: 50%; margin-left: 5px;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--gold); color: #17120A;
  font-size: 11px; font-weight: 700; letter-spacing: 0; font-variant-numeric: tabular-nums;
  border: 1.5px solid var(--bg);
}

/* Offered once to the accounts that were handed a password, above whatever
   screen they landed on. */
.pwoffer {
  margin: 14px var(--gut-r) 0 var(--gut-l); padding: 15px 16px;
  border-radius: var(--r); background: var(--surface); border: 1px solid var(--line);
}
.pwoffer b { display: block; font-family: var(--serif); font-size: 17px; font-weight: 600; }
.pwoffer p { margin: 5px 0 0; color: var(--ink-dim); font-size: 14px; line-height: 1.45; }
.pwoffer .field { margin: 12px 0 0; }
.pwoffer-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.pwoffer-actions .btn { min-height: 46px; font-size: 15px; padding: 10px 12px; }
/* Side by side stops fitting before 360px — stack rather than wrap one label. */
@media (max-width: 359px) { .pwoffer-actions { grid-template-columns: 1fr; } }

/* The one line somebody sees before they have bet on anything. It takes itself
   away the moment they have, so it is never in the way of a guest who already
   knows what the app is. */
.firstrun {
  margin: 0 0 14px; padding: 14px 16px; border-radius: var(--r);
  background: color-mix(in srgb, var(--gold) 9%, transparent);
  border: 1px solid var(--line);
}
.firstrun b { display: block; font-family: var(--serif); font-size: 17px; font-weight: 600; color: var(--gold-ink); }
.firstrun p { margin: 5px 0 0; color: var(--ink-dim); font-size: 14.5px; line-height: 1.45; }

/* ---------- bet cards ---------------------------------------------------- */

/* One card shell for every card on the site — the wedding's own bets (`.bet`),
   one guest against another (`.chal`, further down) and a Guess Who question
   (`.gw`). Written once rather than three times, because two kinds of card on
   one tab in two different colours read as two different species of thing, and
   a hand-kept copy of a surface and a radius is how they drift apart again.
   Each keeps only what differs: `.bet` clips its own head and body, `.chal` and
   `.gw` have no inner parts so they carry their padding themselves. */
.bet, .chal, .gw {
  background: var(--surface); border: 1px solid var(--line-soft); border-radius: var(--r);
  margin-bottom: 14px; box-shadow: var(--shadow);
}
.bet { overflow: hidden; }
/* The open card lifts off the stack with a gold hairline inside its own edge,
   rather than by growing a heavier border and shifting everything below it. */
.bet.open-card {
  border-color: var(--line);
  box-shadow: var(--shadow), inset 0 0 0 1px color-mix(in srgb, var(--gold) 16%, transparent);
}
.bet-head:active { background: color-mix(in srgb, var(--ink) 4%, transparent); }
.bet-head { width: 100%; display: flex; gap: 12px; align-items: flex-start; padding: 15px 16px; text-align: left; }
.bet-head h3 { font-size: 18px; line-height: 1.28; flex: 1; }
.bet-head .chev { width: 18px; height: 18px; color: var(--ink-faint); margin-top: 3px; }
.bet.open-card .bet-head .chev { transform: rotate(90deg); color: var(--gold-ink); }
/* Title, note and badges all close on the same 15px, whichever of them a card
   happens to end with. */
.bet-note { padding: 0 16px 15px; color: var(--ink-dim); font-size: 14px; margin-top: -6px; }

.badges { display: flex; flex-wrap: wrap; gap: 7px; padding: 0 16px 15px; }
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  padding: 5px 10px; border-radius: 999px; border: 1px solid var(--line-soft); color: var(--ink-dim);
  align-self: center; white-space: nowrap; flex: none;
}
/* A badge is 11px type, so its icon is set in pixels and carries a touch more
   weight — at 1em it would thin out to nothing. */
.badge .icn { width: 12px; height: 12px; stroke-width: 2.1; }
.badge.gold { color: var(--gold-ink); border-color: var(--line); background: rgba(212,175,55,.09); }
/* The one bet that pays twice. Filled gold rather than outlined, because on a
   phone it has to be the loudest thing on the card. */
.badge.double {
  color: #17120A; border-color: transparent; letter-spacing: .08em;
  background: linear-gradient(160deg, var(--gold-lit), var(--gold));
  box-shadow: 0 6px 16px -9px rgba(212,175,55,.9);
}
.badge.win { color: var(--win); border-color: color-mix(in srgb, var(--win) 45%, transparent); background: color-mix(in srgb, var(--win) 12%, transparent); }
.badge.loss { color: var(--loss); border-color: color-mix(in srgb, var(--loss) 45%, transparent); }
.badge.lock { color: var(--ink-faint); }

/* A bet paying double wears a gold edge as well as the badge, so it is obvious
   from the list without opening anything — being noticed is the whole point of
   flagging one. Deliberately the card's border and not a glow: a shadow under a
   card in a list of cards reads as the card being dragged. */
.bet.dbl { border-color: var(--gold); }
/* Said again inside the four-tap flow. The payouts under it are already
   doubled, and a number that looks too good reads as a mistake unless something
   on the card says it is not one. */
.dbl-note {
  display: flex; align-items: flex-start; gap: 8px; margin: 0 0 12px;
  padding: 10px 12px; border-radius: var(--r-md);
  background: rgba(212,175,55,.12); border: 1px solid var(--line);
  color: var(--gold-ink); font-size: 13.5px; line-height: 1.4; font-weight: 600;
}
.dbl-note .icn { flex: none; margin-top: 1px; }
/* Turning it on and off, in the odds panel because it is the other thing that
   changes what a bet pays. */
.dbl-set { margin-top: 14px; }
.dbl-set .btn { margin-bottom: 8px; }
/* An outcome on the couple's card is a whole sentence, not a chip. Uppercase
   and tracked out, "She holds it together" read as shouting, and `nowrap` sent
   a long one off the edge of a phone — so it wraps here, and it joins the
   typed-text rule further down like every other place a label is drawn. */
.badge.outcome {
  text-transform: none; letter-spacing: .01em; font-size: 12.5px; font-weight: 600;
  white-space: normal; text-align: left; color: var(--ink-dim);
}
.badge.outcome b { color: var(--gold-ink); font-variant-numeric: tabular-nums; }
.bet-body { padding: 14px 16px 16px; border-top: 1px solid var(--line-soft); }
.opt {
  width: 100%; min-height: var(--tap); display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--line-soft);
  background: var(--surface-2); margin-bottom: 9px; text-align: left;
}
.opt[aria-pressed="true"] { border-color: var(--gold); background: rgba(212,175,55,.12); }
/* The row a guest has staged, once the room's money has re-priced it: a lift on
   the gold it already wears, held for as long as the line under the stakes is
   explaining why and gone the moment they act. The payout changing has to be an
   event, not a silent edit. `.lit` is the one pass of gold on the draw that
   changed it — the whole screen is rebuilt on every change in the room, so that
   pass can be cut short by somebody else's bet, and the lift is what survives
   it. Reduced motion drops the pass and keeps the lift, which is also why the
   line says it in words. From-only, like every other one-shot in this file. */
.opt.moved { background: rgba(212,175,55,.22); }
.opt.moved.lit { animation: requote 1.2s ease; }
@keyframes requote { from { background: rgba(212,175,55,.45); } }
.opt .lbl, .oddsrow .lbl { flex: 1; min-width: 0; font-weight: 600; font-size: 16px; }
.opt .odds { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--gold-ink); font-size: 15px; flex: none; text-align: right; }
/* The multiplier, under the payout in points rather than over it. Quiet — the
   number a guest is comparing across three outcomes is the one in points, and
   "6.00×" is the shorthand for it, not the headline. */
.opt .odds small { display: block; margin-top: 2px; font-size: 11.5px; font-weight: 600; letter-spacing: .01em; color: var(--ink-faint); }
.opt.winner .odds small { color: var(--win); }
/* Which way the room's money has moved this outcome off the odds the couple
   set. Deliberately small and beside the multiplier rather than the payout: it
   is a note in the margin, not a second number to read. Gold where the money
   has come in, faint where it has left. */
.opt .odds .mv { width: 11px; height: 11px; vertical-align: -1px; margin-right: 2px; }
.opt .odds .mv.in { color: var(--gold-ink); }
.opt .odds .mv.out { color: var(--ink-faint); }
.opt .cnt { font-size: 12px; color: var(--ink-faint); flex: none; }
.opt.winner { border-color: var(--win); background: color-mix(in srgb, var(--win) 12%, transparent); }
.opt.winner .odds { color: var(--win); }
.opt:disabled { cursor: default; }

/* An outcome nobody can tap is a row, not a button. A locked, finished, called-
   off or already-backed card used to draw the same raised chips as a live one,
   only disabled — and a chip that looks exactly like the one above it and does
   nothing is the one thing a thumb at midnight cannot forgive. So it goes flat:
   no chip, quiet ink, one hairline between rows instead of a box each. */
.opt.flat {
  background: none; border-color: transparent; border-radius: 0;
  margin-bottom: 0; padding: 11px 4px; min-height: 0;
}
.opt.flat + .opt.flat { border-top-color: var(--line-soft); }
.opt.flat .lbl { color: var(--ink-dim); font-weight: 600; }
.opt.flat .odds { color: var(--ink-dim); }
/* The two rows that still say something keep a plate: the one that won, and the
   one this guest is on. A hairline under a plate would read as a second edge. */
.opt.flat.winner, .opt.flat.took { margin: 5px 0; padding: 11px 12px; border-radius: var(--r-md); }
.opt.flat.winner + .opt.flat, .opt.flat.took + .opt.flat { border-top-color: transparent; }
.opt.flat.winner { background: color-mix(in srgb, var(--win) 13%, transparent); border-color: transparent; }
.opt.flat.took { background: color-mix(in srgb, var(--gold) 12%, transparent); }
.opt.flat.winner .lbl, .opt.flat.took .lbl { color: var(--ink); }
.opt.flat.winner .odds { color: var(--win); }
.opt.flat.took .odds { color: var(--gold-ink); }

/* All / wedding bets / side bets. One row, three taps' worth of target each,
   and the one you are on is the gold pill — a select on a phone is two taps and
   a scroll wheel for the same answer. 14px under it, the one gap every card and
   panel on this tab leaves, so the head of the tab — the door, the points line
   and this — is one rhythm rather than three numbers. */
.filters { display: grid; grid-auto-flow: column; gap: 6px; margin: 0 0 14px; padding: 4px;
  border: 1px solid var(--line-soft); border-radius: var(--r-md); background: var(--surface); }
.filters button {
  min-height: 40px; border-radius: var(--r-sm); padding: 8px 6px;
  font-size: 13.5px; font-weight: 650; color: var(--ink-dim);
}
.filters button[aria-selected="true"] { background: linear-gradient(160deg, var(--gold-lit), var(--gold)); color: #17120A; }

/* The last step, and the only one that spends anything. It says the whole bet
   back in one sentence before the button that commits it, because there is no
   way back out afterwards. */
.confirmbet { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
.confirmbet-said { margin: 0 0 12px; font-size: 15px; line-height: 1.45; color: var(--ink); }
.confirmbet-said b { color: var(--gold-ink); }
.confirmbet .hint { margin: 8px 0 0; text-align: center; }

.stakes { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 12px 0 0; }
.stakes button {
  min-height: 62px; border-radius: var(--r-md); border: 1px solid var(--line-soft); background: var(--surface-2);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  padding: 0 3px; font-variant-numeric: tabular-nums;
}
.stakes button b { font-size: 17px; font-weight: 700; }
/* What this amount comes back as, on the amount itself. The four of them used
   to run together in one line of small print under the row. */
.stakes button small {
  font-size: 11px; font-weight: 600; color: var(--ink-faint); letter-spacing: .01em;
  /* "→ 3,000" has to hold one line inside a quarter of a 320px phone, or the
     two big amounts stand a line taller than the two small ones. */
  white-space: nowrap;
}
.stakes button[aria-pressed="true"] small { color: #4B3A11; }
.stakes button[aria-pressed="true"] { border-color: var(--gold); background: linear-gradient(160deg, var(--gold-lit), var(--gold)); color: #17120A; }
.stakes button:disabled { opacity: .40; }

.hint { font-size: 13.5px; color: var(--ink-dim); margin: 12px 2px 0; }
.hint.lead { margin-top: 0; margin-bottom: 12px; }
/* A hint that introduces something needs air under it; one that closes a card
   does not. */
.hint:not(:last-child) { margin-bottom: 12px; }
.hint.err { color: var(--loss); }
/* What each stake actually returns, spelled out before the tap. */
.hint.pays { color: var(--gold-ink); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The one that says the room has just re-priced a staged quote. Same gold as
   the payouts it is about, with a rule beside it so it reads as something that
   happened rather than one more line of small print. */
.hint.moved { padding-left: 9px; border-left: 2px solid var(--gold); }
.hint:empty { display: none; }
/* How much room is left in a capped box. Right under the field, right-aligned,
   and silent until the last stretch — see public/dom.js -> counter. */
.count {
  margin: 6px 2px 0; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--ink-faint); letter-spacing: .02em;
}
.count:empty { display: none; }
.count.full { color: var(--loss); font-weight: 600; }
.mywager {
  margin-top: 12px; padding: 12px 14px; line-height: 1.5;
  border-radius: var(--r-md); background: rgba(212,175,55,.10); border: 1px solid var(--line); font-size: 14.5px;
}
.mywager .lead { width: 16px; height: 16px; color: var(--gold-ink); margin-right: 6px; vertical-align: -.2em; }
.mywager b { color: var(--gold-ink); }
.mywager .dbl { color: var(--gold-ink); font-weight: 600; }

/* ---------- oracle / admin ---------------------------------------------- */

/* The two seats that are not a guest wear the same card: the gold-edged frame,
   their own photograph across the top of it, and who they are. The Oracle's
   words sit over the picture because his card is a badge; the couple's sit
   under it — see below. */
.oracle-card, .couple-card {
  position: relative; border-radius: var(--r); overflow: hidden; margin-bottom: 16px;
  border: 1px solid var(--gold); box-shadow: 0 16px 40px -20px rgba(212,175,55,.55);
}
.oracle-card img, .couple-card img { width: 100%; height: 168px; object-fit: cover; object-position: 50% 30%; display: block; }
.oracle-card .veil { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(7,12,30,.15), rgba(7,12,30,.9)); }
.oracle-card .txt { position: absolute; inset: auto 0 0; padding: 14px 16px; }
.oracle-card .crown { line-height: 1; margin-bottom: 7px; }
.oracle-card .crown .icn { width: 25px; height: 25px; color: var(--gold-lit); }
.oracle-card h3 { color: #fff; font-size: 20px; }
.oracle-card p { margin: 3px 0 0; color: rgba(255,255,255,.82); font-size: 13.5px; }

/* The couple's card puts the words UNDER the picture rather than over it. It is
   a selfie through a fence at a ball game: the two faces fill the middle of the
   frame, so a veil and four lines of type across the bottom of it printed
   "Congratulations" over the bride. The crop is pushed down to sit both heads
   inside the band, and the words stand on their own gold-lit plate beneath. */
.couple-card { margin-bottom: 14px; }
.couple-card img { height: 190px; object-position: 50% 86%; }
.couple-card .txt {
  padding: 16px 18px 18px; text-align: center;
  border-top: 1px solid var(--line);
  background: radial-gradient(120% 130% at 50% 0%, color-mix(in srgb, var(--gold) 14%, transparent), transparent 72%), var(--surface);
}
.couple-card .rings { line-height: 1; margin-bottom: 7px; }
.couple-card .rings .icn { width: 30px; height: 30px; color: var(--gold-ink); stroke-width: 1.5; }
.couple-card .monogram { margin: 0 0 5px; text-shadow: none; }
.couple-card h2 { font-size: 30px; line-height: 1.1; }
.couple-card .when { margin: 7px 0 0; color: var(--ink-dim); font-size: 12px; letter-spacing: .16em; text-transform: uppercase; }

/* The wreath and the one warm line under the card, before the numbers start. */
.couple-wish { text-align: center; margin: 0 0 16px; }
.couple-wish .olive { margin: 2px auto 10px; }
.couple-wish p { margin: 0 6px; color: var(--ink-dim); font-size: 14.5px; line-height: 1.5; }

/* What the room is doing, under their three tiles. A list rather than a
   paragraph: three short facts read at a glance, and any of them can be absent
   without leaving a gap in a sentence. */
.nightlines { list-style: none; margin: 16px 0 0; padding: 0; }
.nightlines li {
  padding: 11px 0 0; margin-top: 11px; border-top: 1px solid var(--line-soft);
  font-size: 14.5px; line-height: 1.45; color: var(--ink-dim);
}
.nightlines li:first-child { border-top: 0; margin-top: 0; padding-top: 0; color: var(--ink); }

/* Viewing as someone else — impossible to miss, and one tap back out. */
.viewas {
  display: flex; align-items: center; gap: 12px;
  min-height: calc(52px + var(--bar-top));
  padding: calc(7px + var(--bar-top)) calc(14px + var(--sar)) 7px calc(14px + var(--sal));
  background: linear-gradient(160deg, var(--gold-lit), var(--gold)); color: #17120A;
}
.viewas span { flex: 1; min-width: 0; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viewas b { font-weight: 750; }
.viewas .btn {
  flex: none; min-height: 38px; padding: 8px 14px; border-radius: 10px; font-size: 13.5px;
  white-space: nowrap; background: rgba(23,18,10,.92); border-color: transparent; color: var(--gold-lit);
}

/* One dashed rule marks where the Oracle's half of a card starts: the first
   tool block that follows real card content. Blocks after it separate on space,
   and on the couple's cards, where the tools are all there is, the card body's
   own line already does the job. */
.bet-body > :not(.oracle-tools) + .oracle-tools { border-top: 1px dashed var(--line); margin-top: 16px; padding-top: 14px; }
.oracle-tools + .oracle-tools { margin-top: 20px; }
.oracle-tools > .textbtn { margin-top: 2px; }
.tools-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.tools-head .settle-title { margin-bottom: 0; }
.tools-head .textbtn { padding: 6px 0; min-height: 32px; font-size: 13px; flex: none; }

/* What the three buttons do, in plain words — one tap, then out of the way. */
.explain { margin: 12px 0; border: 1px solid var(--line-soft); border-radius: var(--r-md); background: var(--surface-2); overflow: hidden; }
.explain > div { padding: 11px 13px; border-bottom: 1px solid var(--line-soft); }
.explain > div:last-child { border-bottom: 0; }
.explain b { display: flex; align-items: center; gap: 7px; font-size: 13.5px; color: var(--gold-ink); margin-bottom: 3px; }
.explain b .icn { width: 14px; height: 14px; }
.explain span { font-size: 13.5px; color: var(--ink-dim); line-height: 1.45; }
/* A fixed two-up grid: lock and settle share a row, calling it off gets its
   own. Flex-wrap re-flowed differently at every phone width. */
.oracle-tools .rowbtns { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.oracle-tools .rowbtns .btn { min-height: 48px; font-size: 14.5px; padding: 10px 12px; }
.oracle-tools .rowbtns .btn:last-child { grid-column: 1 / -1; }
.settle-title { font: 600 11px/1.3 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 10px; }

/* The app's own "are you sure?". Boxed and edged in the loss colour so it reads
   as the serious thing it is, wherever it is asked from — a bet card or the
   Admin tab. The browser's own dialog was the old answer to both. */
.confirm {
  margin-top: 12px; padding: 14px;
  border: 1px solid color-mix(in srgb, var(--loss) 45%, transparent);
  border-radius: var(--r-md); background: var(--surface-2);
}
.confirm .settle-title { margin-bottom: 6px; }
.confirm > .textbtn { margin-top: 2px; }

.panel { background: var(--surface); border: 1px solid var(--line-soft); border-radius: var(--r); padding: 16px; margin-bottom: 14px; }
.panel h3 { font-size: 18px; margin-bottom: 4px; }
/* One shape for everything that is closed until it is asked for: "bet a friend"
   at the top of the Bets tab, "add a bet" and "how the game works" on the You
   tab. One quiet button and no box around it — a bordered panel at the top of
   the Bets tab outweighed the bets themselves, and three different closed
   shapes on two tabs is three things to learn instead of one. */
.shut { margin-bottom: 14px; }
/* The way to the rest of the side bets at the head of the Bets tab, and back.
   Centred and quiet, under the cards it opens. */
.moreside { display: flex; justify-content: center; margin: 0 0 8px; }
.shut .btn { min-height: 46px; font-size: 15px; }
.panel.danger { border-color: color-mix(in srgb, var(--loss) 45%, transparent); }
/* Flat broke. A roast, not an error, so nothing here is red: the edge stays the
   soft one every other panel wears and the surface only warms, the way a face
   does. What actually says it is that the GOLD is gone — the three figures and
   the place ordinal, which are gold or bright on every other card on the page,
   go grey on this one. The words do the rest (public/views.js -> BROKE). */
.panel.broke { background: color-mix(in srgb, var(--loss) 5%, var(--surface)); }
.panel.broke .stats b, .panel.broke .place b { color: var(--ink-dim); }
.panel p.sub { color: var(--ink-dim); font-size: 14px; margin: 0 0 14px; }
.panel > :last-child { margin-bottom: 0; }

/* Pocket / on the table / won so far. The captions each hold one line from
   360px up; on a narrower phone "On the table" needs two, so all three reserve
   the second line and the tiles still close together. */
/* Where this guest sits on the board that decides the prize — the one fact
   they came to this page for, so it sits with their name rather than in a
   tile. */
.panel .place { margin-top: 8px; font-size: 14px; color: var(--ink-dim); }
.panel .place b { font-family: var(--serif); font-size: 19px; font-weight: 600; color: var(--gold-ink); }

.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 14px; }
@media (max-width: 359px) { .stats small { min-height: 2.4em; } }

/* ---------- feed / leaderboard ------------------------------------------ */

/* The ticker reads as one thread through the evening: a hairline runs behind
   the dots, and each dot sits on the page rather than on the line. */
.feed-item {
  position: relative;
  display: flex; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--line-soft);
}
.feed-item::before {
  content: ""; position: absolute; left: 4px; top: 30px; bottom: -8px; width: 1px;
  background: var(--line-soft);
}
.feed-item:last-child::before { display: none; }
.feed-item .dot {
  position: relative; z-index: 1;
  width: 9px; height: 9px; border-radius: 50%; background: var(--gold); margin-top: 6px; flex: none;
  box-shadow: 0 0 0 3px var(--bg);
}
.feed-item.settle .dot { background: var(--win); }
.feed-item.void .dot, .feed-item.lock .dot { background: var(--ink-faint); }
.feed-item.robber .dot { background: var(--loss); }
.feed-item.odds .dot { background: var(--gold-ink); }
.feed-item.guess .dot { background: var(--gw-emily); }
.feed-item .txt { flex: 1; font-size: 15px; line-height: 1.42; }
.feed-item time { display: block; color: var(--ink-faint); font-size: 12px; margin-top: 3px; }

/* The column label over the board, in place of the paragraph that used to sit
   under it. Same 8px gutter as a row, so it sits over the .pts column the rows
   put `worth` in. */
.lb-head { display: flex; justify-content: flex-end; padding: 0 14px 6px; }
.lb-head span { font: 600 10.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }

/* Every row keeps the same gutter, so highlighting your own does not nudge it
   sideways, and rows with nothing in play stay the same height as rows with.
   The gutter is 14px rather than 8 for two reasons, both of them your own row:
   the rank chip has to clear the gold bar down the left of it and the rounded
   corner above it, and the number on the right has to stop short of the edge
   of the screen instead of running into it. */
.lb {
  display: flex; align-items: center; gap: 12px; min-height: 62px; padding: 8px 14px;
  border-bottom: 1px solid var(--line-soft);
}
/* The place is the information, so the place is what is shown. Gold, silver
   and bronze are the chip it sits in — legible at a glance, and the same size
   whether you came first or forty-first. */
.lb .rank {
  width: 27px; height: 27px; border-radius: 50%; display: grid; place-items: center;
  font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px;
  color: var(--ink-faint); border: 1px solid var(--line-soft); flex: none;
}
.lb.top1 .rank, .lb.top2 .rank, .lb.top3 .rank { color: #17120A; border-color: transparent; }
.lb.top1 .rank { background: linear-gradient(160deg, var(--gold-lit), var(--gold)); }
.lb.top2 .rank { background: linear-gradient(160deg, #EEF1F7, var(--silver)); }
.lb.top3 .rank { background: linear-gradient(160deg, #E6BC97, var(--bronze)); }
.lb .nm { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb .nm small { display: block; font-weight: 500; color: var(--ink-faint); font-size: 12px; }
.lb .pts { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--gold-ink); flex: none; }
/* Your own row, and it has to read as a box. The tint alone is very nearly the
   page's own ivory in light mode, so the gold bar down the left was the only
   edge anybody could actually see — and the rank chip beside it read as sitting
   half outside a box whose edge was invisible. The ring is a second inset
   shadow rather than a border, so marking a row still cannot change its height
   and nudge the board. */
.lb.me {
  background: rgba(212,175,55,.13); border-radius: var(--r-sm); border-bottom-color: transparent;
  box-shadow: inset 3px 0 0 var(--gold), inset 0 0 0 1px var(--line);
}
/* The fold above your own row when the board is showing its top ten and you are
   further down it. It names the place the chip under it carries, so the pinned
   row reads as "and here is you" rather than as eleventh. */
.lb-you {
  display: flex; align-items: center; gap: 10px; margin: 14px 0 6px;
  font: 600 10.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint);
}
.lb-you::before, .lb-you::after { content: ''; flex: 1; height: 1px; background: var(--line-soft); }

.crown-reveal { text-align: center; padding: 26px 18px 22px; border: 1px solid var(--gold); border-radius: var(--r); background: radial-gradient(120% 100% at 50% 0%, rgba(212,175,55,.20), transparent 70%); margin-bottom: 16px; }
.crown-reveal .c { line-height: 1; }
.crown-reveal .c .icn { width: 46px; height: 46px; color: var(--gold); stroke-width: 1.5; }
.crown-reveal.pop .c { animation: pop .6s ease both; }
.crown-reveal h2 { font-size: 27px; margin: 10px 0 4px; }
.crown-reveal p { color: var(--ink-dim); margin: 0; font-size: 14.5px; }
@keyframes pop { from { transform: scale(.4) rotate(-14deg); opacity: 0; } }

/* ---------- chat, challenges and dares ---------------------------------- */

/* Every place the app draws words somebody typed — a chat line, the ticker, a
   side bet, an outcome or a dare, a bet's own title and note, a row on the You
   page. A browser only breaks a line between words, and 120 characters with no
   space in them is one word: one of those in the chat stretched the page past
   six times a phone's width, and the whole app shrank and scrolled sideways for
   everyone who had it on screen. `anywhere` rather than `break-word` because it
   is also what tells a flex row the text may be narrower than its longest word.
   Pasted URLs already broke on their own; letter and digit runs never do. */
.bubble p, .sysline p, .feed-item .txt, .chal-text, .opt .lbl, .badge.outcome,
.bet-head h3, .bet-note, .result-row .what, .gw-q { overflow-wrap: anywhere; }

/* A field and the button that sends it, on one line — the shape is the same
   for a chat message and for adding a dare. */
.compose { display: flex; gap: 8px; align-items: stretch; }
.compose input { flex: 1; min-width: 0; }
.btn.send { flex: none; width: var(--tap); padding: 0; }
.btn.send .icn { width: 19px; height: 19px; }

/* A challenge: one guest against one other for the same stake each side. It is
   a bet, so it is drawn as one — the shell is shared with `.bet` up in the bet
   cards section, and only the gutter is its own. What is theirs stays theirs:
   the take/call controls inside, and the gold edge below. */
.chal { padding: 15px 16px; }
/* The side bet a chat line sent somebody to: at the top of the section with a
   gold edge, so the tap lands on the bet rather than somewhere near it — after
   the state rules above, because a called one is still the one you asked for.
   It arrives wearing the motion system's own entrance — public/bets.js. */
.chal.spot { border-color: var(--gold); }
.chal-head { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }
.chal-head .icn { width: 15px; height: 15px; color: var(--gold-ink); }
.chal-head b { font-family: var(--serif); font-size: 15.5px; font-weight: 600; }
.chal-head .badge { margin-left: auto; }
.chal-vs { margin: 4px 0 0; font-size: 13px; color: var(--ink-faint); }
.chal-vs b { font-family: var(--serif); font-size: 15.5px; font-weight: 600; color: var(--ink); }
.chal-text { margin: 9px 0 12px; font-size: 15.5px; line-height: 1.45; }
.chal-out { display: flex; align-items: center; gap: 7px; margin: 0; font-size: 14px; color: var(--win); }
.chal-settle { margin-top: 12px; }
.chal-settle .opt { margin-bottom: 8px; }
.chal-settle .opt:last-child { margin-bottom: 0; }
.chal .hint { margin-bottom: 0; }

/* The dare poll, docked at the bottom of the chat above the composer for as long
   as it runs. It is the loudest thing on the page on purpose: it lasts 45
   minutes and then it is gone. */
.poll {
  border: 1px solid var(--gold); border-radius: var(--r); padding: 15px 15px 14px; margin-bottom: 12px;
  background: radial-gradient(120% 100% at 50% 0%, rgba(212,175,55,.16), transparent 70%), var(--surface);
}
.poll-head { display: flex; align-items: center; gap: 9px; }
.poll-head .icn { width: 21px; height: 21px; color: var(--gold-ink); }
.poll-head b { font-family: var(--serif); font-size: 17px; font-weight: 600; }
.poll .sub { color: var(--ink-dim); font-size: 13.5px; margin: 5px 0 13px; }
.poll .compose { margin-top: 12px; }
.opt.dare { align-items: flex-start; }
.opt.dare .lbl { font-weight: 600; font-size: 15px; line-height: 1.4; }
.opt.dare .lbl small { display: block; margin-top: 3px; font-weight: 500; font-size: 12px; color: var(--ink-faint); }
.votes {
  flex: none; min-width: 30px; text-align: center; font-variant-numeric: tabular-nums;
  font-weight: 700; font-size: 15px; color: var(--gold-ink);
}

/* The stream, and it reads as a conversation. What somebody typed is a bubble —
   your own on the right, everybody else's on the left, initials in a ring where
   a photo would be if the venue's signal could carry 150 of them. What the app
   wrote about the night runs between them, centred and quiet, so a settle or a
   robbery is plainly not somebody talking. */
.msg { display: flex; gap: 8px; align-items: flex-end; margin: 8px 0; }
.msg.mine { flex-direction: row-reverse; }

.avatar {
  width: 30px; height: 30px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  color: var(--gold-ink); background: color-mix(in srgb, var(--gold) 14%, transparent);
  border: 1px solid var(--line);
}
/* A message in the middle of a run from one person keeps the indent and drops
   the ring, so the bubbles below the name line up under it. */
.avatar.blank { background: none; border-color: transparent; }

/* The couple wear their own photograph where everybody else wears initials —
   they are the two people the night is about, so their name in the room looks
   like them. A double gold ring rather than the single one a guest gets, and
   the crop is the one .couple-hello already proved on the same picture: at
   1200x750 a square cover keeps both faces at 42% across. One asset, no upload
   and no per-guest avatar system — this is the couple's seat and nobody
   else's. */
.avatar.couple {
  overflow: hidden; padding: 2px; background: var(--gold);
  border-color: var(--gold);
}
.avatar.couple img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; object-position: 42% 30%; display: block; }
.avatar.couple.sm { width: 38px; height: 38px; padding: 1.5px; }

.bubble {
  max-width: 78%; min-width: 0; padding: 9px 13px 7px;
  border-radius: 16px; background: var(--surface-2); border: 1px solid var(--line-soft);
}
.msg.them .bubble { border-bottom-left-radius: 5px; }
.msg.mine .bubble {
  border-bottom-right-radius: 5px; text-align: right;
  background: color-mix(in srgb, var(--gold) 16%, var(--surface-2));
  border-color: var(--line);
}
.bubble .who { display: block; font-family: var(--serif); font-size: 13px; font-weight: 600; color: var(--gold-ink); margin-bottom: 2px; }
.bubble p { margin: 0; font-size: 15px; line-height: 1.42; }
.bubble time { display: block; margin-top: 3px; font-size: 10.5px; color: var(--ink-faint); }

/* A side bet somebody wrote reads as a message from them, with the bet's own
   card inside the bubble the way a shared post or a payment request does in
   every other messenger. It carries a full-width gold button, so it gets more
   of the line than a sentence of talk does and the card inside drops its own
   frame — one border round a thing, not two. */
.chalmsg { align-items: flex-start; }
.chalmsg .bubble { max-width: 88%; padding: 10px 12px 8px; }
.chalmsg .chal { border: 0; background: none; box-shadow: none; padding: 0; margin: 0; border-radius: 0; }
.chalmsg .chal-head b { font-size: 14.5px; }
.chalmsg .chal-text { margin: 7px 0 10px; font-size: 15px; }
.chalmsg .chal-head .badge { margin-left: 0; }
.msg.mine.chalmsg .bubble { text-align: left; }
/* The read-back before the points move sits inside the bubble, so its rule
   above it is the bubble's own width rather than the card's old padding. */
.chalmsg .confirmbet { margin-top: 12px; padding-top: 11px; }

/* A line the app wrote: what happened, with the mark of what kind it was. */
.sysline {
  display: flex; gap: 9px; align-items: flex-start; justify-content: center;
  margin: 14px auto; padding: 0 8px; max-width: 92%; color: var(--ink-dim); text-align: center;
}
.sysline .icn { width: 14px; height: 14px; color: var(--gold-ink); flex: none; }
/* Nudged down only in the plain note, whose text starts at the top of the row;
   the tappable variant centres everything, so it needs no undo. */
div.sysline .icn { margin-top: 3px; }
.sysline p { margin: 0; font-size: 13.5px; line-height: 1.4; }
/* The ones that go somewhere: a side bet being taken or called is news about a
   card that has scrolled out of sight, so the note about it is the way back to
   it. A button rather than a link, and it keeps the quiet centred shape of
   every other system line — the chevron on the end is the whole of what marks
   it as tappable, because a system line that shouted would stop being one. */
button.sysline {
  width: 100%; max-width: none; align-items: center;
  padding: 8px 12px; border-radius: var(--r-md); border: 1px solid transparent;
  background: none; font: inherit; text-align: center;
}
button.sysline p { flex: 1; }
button.sysline:active:not(:disabled) { background: var(--surface-2); border-color: var(--line-soft); }
button.sysline .icn:last-child { color: var(--ink-faint); }
.sysline time { display: block; margin-top: 2px; font-size: 10.5px; color: var(--ink-faint); }

/* The conversation runs down the page and the composer sits under it in the
   dock, so a short thread belongs at the bottom by the box rather than floating
   at the top over a void. */
/* The extra room at the foot is the keyboard's: without it the newest line has
   nowhere to scroll to and stays under the raised composer. */
.screen.chat { display: flex; flex-direction: column; padding-bottom: calc(12px + var(--kb, 0px)); }
.chat .stream { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; }
.chat .rule { margin-top: 20px; }

/* The quiet centred chip that opens a day. The app is open all week, so a guest
   scrolling back can always tell which day they are reading. */
.daysep { display: flex; justify-content: center; margin: 20px 0 12px; }
.daysep span {
  padding: 5px 13px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line-soft);
  font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint);
}

/* The composer: a bar at the foot of the screen, not a card in the page. */
.composer {
  padding: 10px var(--gut-r) 10px var(--gut-l);
  border-top: 1px solid var(--line-soft);
  background: var(--bg);
}
.composer .hint { margin: 6px 2px 0; }

/* ---------- Guess Who ---------------------------------------------------- */

/* One card per question, on the shared card shell above. The two photographs
   are the ends of the gauge AND the two buttons that pick a side: one control
   that says which side it is and takes the tap, which is the whole shape of the
   game on a phone.

   `--gw` is whichever of the two people a part of the card belongs to, set once
   on each side's elements so the ring, the band and the chip borders below are
   one rule each rather than a matched pair. */
.gw { padding: 15px 15px 14px; }
/* What playing is worth, at the very top of the tab and above everything else
   on it — the reveal card included. Drawn as `.dbl-note` is, because it is the
   same kind of thing: a rule about points that has to be read before the taps
   under it, not a trophy. Flat, left-aligned and one line of gold-ink text, so
   it cannot be mistaken for the `.crown-reveal` total card it sits above. */
.gw-prize {
  display: flex; align-items: flex-start; gap: 8px; margin: 0 0 14px;
  padding: 11px 13px; border-radius: var(--r-md);
  background: rgba(212,175,55,.12); border: 1px solid var(--line);
  color: var(--gold-ink); font-size: 13.5px; line-height: 1.42;
}
.gw-prize b { font-weight: 700; }
.gw-prize .icn { flex: none; margin-top: 2px; color: var(--gold); }
.gw-face.paul, .gw-side.paul { --gw: var(--gw-paul); }
.gw-face.emily, .gw-side.emily { --gw: var(--gw-emily); }
.gw-q { margin: 0 0 13px; font-size: 16.5px; line-height: 1.32; font-weight: 600; }
/* The number off the paper card, so a guest reading the two together can find
   their place. Quiet: the question is the thing, not its index. */
.gw-q .n, .gw-key .n {
  display: inline-grid; place-items: center; width: 22px; height: 22px; margin-right: 9px;
  border-radius: 50%; border: 1px solid var(--line-soft); vertical-align: 1px;
  font: 700 11.5px/1 var(--sans); color: var(--ink-faint); font-variant-numeric: tabular-nums;
}

.gw-pick { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; }

/* The photograph, round, in a ring that says what it is: their own colour
   before you have picked, gold once you have, and — after the couple answer —
   gold with a tick on the side they said, with the other one stepped back. */
.gw-face {
  --press: var(--press-sm);
  width: 62px; height: 62px; flex: none; padding: 0; position: relative;
  border-radius: 50%; display: block;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--gw) 55%, transparent);
  transition: box-shadow var(--m) var(--m-ease), opacity var(--m) var(--m-ease),
              transform var(--m-press) var(--m-ease);
}
.gw-face .ph { display: block; width: 100%; height: 100%; border-radius: 50%; overflow: hidden; }
.gw-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gw-face.picked, .gw-face.won { box-shadow: 0 0 0 3px var(--gold), 0 8px 22px -12px rgba(212,175,55,.9); }
.gw-face.lost { opacity: .42; }
/* The couple's answer, marked on the face itself rather than only in the line
   underneath — the photograph is what the eye lands on. */
.gw-face .tick {
  position: absolute; right: -4px; bottom: -4px; width: 24px; height: 24px;
  border-radius: 50%; display: grid; place-items: center;
  background: linear-gradient(160deg, var(--gold-lit), var(--gold));
  color: #17120A; border: 2px solid var(--surface);
}
.gw-face .tick .icn { width: 13px; height: 13px; stroke-width: 2.6; }

/* The gauge. The fill runs from Paul's end, so a full bar is the whole room on
   him; it eases to its new width when an answer lands — public/motion.js ->
   slide. The rail behind it is Emily's colour, so the bar reads as two shares
   of one line rather than as a progress bar. */
.gw-track {
  height: 12px; border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--gw-emily) 70%, transparent);
}
/* Full width and slid left rather than a narrow bar grown, so the move the
   room's answers cause is a transform the phone composites — public/guesswho.js
   -> track. The track's own rounding clips whatever goes past its left edge. */
.gw-fill { display: block; width: 100%; height: 100%; border-radius: 999px; background: var(--gw-paul); }
/* Nothing answered yet: half and half, and quiet, so it does not read as a
   fifty-fifty result nobody voted for. */
.gw-track.quiet { opacity: .3; }

.gw-legend {
  display: flex; justify-content: space-between; gap: 10px; margin-top: 9px;
  font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint);
}
.gw-legend span { display: inline-flex; align-items: baseline; gap: 6px; }
.gw-legend b { font-size: 15px; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.gw-legend span:first-child b { color: var(--gw-paul); }
.gw-legend span:last-child b { color: var(--gw-emily); }

/* How many picked each side, under the gauge. The split is public — that is
   the game — but only ever as a NUMBER: no guest's name is on this board, and
   none is sent to it. Two bands, one per side, sitting under the photograph
   they belong to and tinted in that side's own colour, so which number is
   whose is read off where it sits rather than off a label. */
.gw-counts { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.gw-side {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 11px; border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--gw) 9%, transparent);
}
/* Emily's band leans the way her end of the gauge does. */
.gw-side.emily { flex-direction: row-reverse; }
/* A head count, not a percentage — the line above is already the percentages,
   and one small figure says which of the two numbers this is without a word. */
.gw-side .icn { width: 13px; height: 13px; flex: none; color: var(--ink-faint); opacity: .75; }
.gw-side b {
  font-size: 17px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--gw);
}
/* The side this guest picked. The gold ring on the photograph is the first
   answer to "which one is mine"; this is the same claim carried down onto the
   number, so it survives a glance at the bottom half of the card. */
.gw-side.me {
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 55%, transparent);
}
.gw-side.me .icn { color: var(--gold-ink); opacity: 1; }

/* What the couple said, once they have said it. */
.gw-said {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
.gw-said b { font-family: var(--serif); font-size: 16px; font-weight: 600; color: var(--gold-ink); flex: 1; min-width: 0; }

/* The card's own "Total: ___", in the shape the crown already uses. */
.gw-total { margin-bottom: 14px; }
.gw-total .c .icn { color: var(--gold); }

/* The admin's answer key: sixteen rows, two buttons each. */
.gw-key { padding: 11px 0; border-bottom: 1px solid var(--line-soft); }
.gw-key:last-of-type { border-bottom: 0; }
.gw-key p { margin: 0 0 8px; font-size: 14.5px; line-height: 1.35; }
.gw-key-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.gw-key-pick .btn { min-height: 44px; font-size: 15px; padding: 8px 10px; }

/* ---------- couple view ---------------------------------------------------- */

/* The laurel sits in the middle of its own band: equal air above and below,
   between the words and the Greek key. */
.couple-hello { text-align: center; padding: 22px var(--gut-r) 0 var(--gut-l); }
.couple-hello img { width: 92px; height: 92px; border-radius: 50%; object-fit: cover; object-position: 42% 30%; border: 2px solid var(--gold); }
.couple-hello h2 { font-size: 25px; margin-top: 12px; }
.couple-hello p { color: var(--ink-dim); font-size: 14.5px; margin: 6px 0 0; }
.couple-hello .olive { margin: 18px auto; }

.result-row { display: flex; gap: 12px; padding: 14px 0; border-bottom: 1px solid var(--line-soft); align-items: center; }
.result-row .what { flex: 1; min-width: 0; }
.result-row b { display: block; font-weight: 600; font-size: 15.5px; line-height: 1.35; }
/* The line under the title — which way you went, or who the side bet was
   against. Scoped to that half of the row: the badge on the right side is a
   span too, and it carries its own colour. */
.result-row .what span { color: var(--gold-ink); font-size: 14px; }

/* ---------- misc ---------------------------------------------------------- */

.empty { text-align: center; padding: 40px 22px; color: var(--ink-dim); }
/* Every empty state wears the same plate, so an app with nothing in it yet
   still looks like it was designed rather than like something failed. */
.empty .ic, .steps .ic {
  display: grid; place-items: center; width: 62px; height: 62px; margin: 0 auto 15px;
  border-radius: 50%; color: var(--gold-ink);
  background: color-mix(in srgb, var(--gold) 10%, transparent);
  border: 1px solid var(--line);
}
.empty .ic .icn { width: 27px; height: 27px; stroke-width: 1.6; }
.empty b { display: block; font-family: var(--serif); font-size: 19px; color: var(--ink); margin-bottom: 6px; font-weight: 600; }
.empty p { margin: 0; font-size: 14.5px; }

.signout { margin-top: 26px; }

/* Just above the foot of the screen: the tab bar alone on most tabs, the tab bar
   plus the composer on the chat, and both of them lifted by an open keyboard.
   `--foot` is published by public/app.js -> dock; the fallback is the tab bar on
   its own, for the moment before the first one is measured. */
#toast {
  position: fixed; left: 50%; transform: translate(-50%, 20px);
  bottom: calc(var(--foot, calc(59px + var(--sab))) + var(--kb, 0px) + 18px);
  z-index: 60; max-width: min(420px, calc(100vw - 32px - var(--sal) - var(--sar))); padding: 13px 18px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line); color: var(--ink); box-shadow: var(--shadow);
  font-size: 15px; text-align: center; opacity: 0; pointer-events: none;
  transition: opacity var(--m) var(--m-ease), transform var(--m) var(--m-ease);
}
#toast.on { opacity: 1; transform: translate(-50%, 0); }
#toast.bad { border-color: color-mix(in srgb, var(--loss) 55%, transparent); }

/* ---------- the welcome ---------------------------------------------------- */

/* Once per account on the way in, and again whenever the You tab asks for it
   back. It takes the whole screen — there is nothing behind it worth reading
   yet — and it follows the phone's theme, unlike the robbery, which is a
   blackout on purpose. Under the robbery in the stack (90): a takeover landing
   while somebody is still reading this still wins. */
.splash {
  position: fixed; inset: 0; z-index: 80; overflow-y: auto; background: var(--bg);
  padding: calc(28px + var(--sat)) var(--gut-r) calc(28px + var(--sab)) var(--gut-l);
  display: flex; justify-content: center; align-items: flex-start;
}
.splash-in { width: 100%; max-width: var(--col); margin: auto 0; text-align: center; }
.splash h1 { font-family: var(--serif); font-size: 30px; line-height: 1.15; margin: 0 0 6px; text-wrap: balance; }
.splash .olive { margin: 6px auto 0; }
.splash-lead { color: var(--ink-dim); font-size: 15px; line-height: 1.5; margin: 14px 2px 22px; }
.splash .btn { margin-top: 6px; }
.splash-note { margin: 12px 0 0; font-size: 13px; color: var(--ink-faint); }

/* Four steps, each one line of what and one of why. Left-aligned inside a
   centred column: a list of four centred paragraphs has no edge for the eye to
   run down. */
.steps { list-style: none; margin: 0 0 22px; padding: 0; text-align: left; }
.steps li { display: flex; gap: 13px; align-items: flex-start; padding: 12px 0; border-top: 1px solid var(--line-soft); }
.steps li:first-child { border-top: none; }
/* The same gold plate an empty state wears, at the size a list row wants. */
.steps .ic { flex: none; width: 38px; height: 38px; margin: 0; }
.steps .ic .icn { width: 19px; height: 19px; stroke-width: 1.7; }
.steps b { display: block; font-size: 15.5px; font-weight: 650; line-height: 1.3; }
.steps p { margin: 3px 0 0; font-size: 13.5px; line-height: 1.45; color: var(--ink-dim); }


/* ---------- the 69,000-point robbery ------------------------------------ */

/* Full screen, every phone, four seconds. It carries `.midnight` as well, so it
   keeps the dark palette whatever the phone is set to: this is a blackout
   moment, and a light-mode version of it would be a different, smaller joke. */
.heist {
  position: fixed; inset: 0; z-index: 90; display: grid; place-items: center;
  padding: calc(24px + var(--sat)) calc(24px + var(--sar)) calc(24px + var(--sab)) calc(24px + var(--sal));
  background: radial-gradient(120% 90% at 50% 0%, rgba(212,175,55,.26), transparent 62%), var(--midnight);
  color: var(--ink); text-align: center; overflow: hidden;
  opacity: 0; transition: opacity .3s ease;
}
.heist.on { opacity: 1; }
.heist-in { position: relative; z-index: 1; max-width: 340px; }
.heist .mask .icn { width: 96px; height: 96px; color: var(--gold); stroke-width: 1.3; }
.heist.on .mask { animation: heist-mask .7s cubic-bezier(.2,1.4,.4,1) both; }
.heist h2 { font-size: 46px; line-height: 1; letter-spacing: .06em; color: var(--gold-ink); margin: 16px 0 10px; text-shadow: 0 6px 34px rgba(212,175,55,.5); }
.heist.on h2 { animation: heist-word .5s ease both .12s; }
.heist .who { font-family: var(--serif); font-size: 25px; margin: 0; overflow-wrap: anywhere; }
.heist .took { font-size: 19px; font-weight: 700; color: var(--gold-ink); margin: 4px 0 12px; font-variant-numeric: tabular-nums; }
.heist .split { font-size: 15px; line-height: 1.45; color: var(--ink-dim); margin: 0; overflow-wrap: anywhere; }
.heist .tap { font-size: 12px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-dim); opacity: .6; margin: 26px 0 0; }
.loot { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
.coin { position: absolute; width: 11px; height: 11px; border-radius: 50%; background: linear-gradient(160deg, var(--gold-lit), var(--gold)); animation: loot 1.5s ease-out both; }
@keyframes heist-mask { from { transform: scale(.3) rotate(-16deg); opacity: 0; } }
@keyframes heist-word { from { transform: scale(1.5); opacity: 0; } }
@keyframes loot { from { transform: translate(0,0) scale(.6); opacity: 1; } to { transform: translate(var(--dx), var(--dy)) scale(1); opacity: 0; } }

.smash { position: fixed; inset: 0; z-index: 70; display: grid; place-items: center; pointer-events: none; }
.smash .opa { font-family: var(--serif); font-size: 15vw; color: var(--gold-ink); text-shadow: 0 6px 40px rgba(212,175,55,.55); animation: opa 1.5s ease both; }
@keyframes opa { 0% { transform: scale(.3) rotate(-8deg); opacity: 0; } 25% { transform: scale(1.08); opacity: 1; } 70% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.1); opacity: 0; } }
.shard { position: absolute; width: 10px; height: 10px; background: var(--gold); animation: shard 1.2s ease-out both; }
@keyframes shard { from { transform: translate(0,0) rotate(0); opacity: 1; } to { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; } }

/* ---------- motion ------------------------------------------------------- */

/* One system, used everywhere. The screen is rebuilt from scratch on every
   change in the room, so an entrance has to be told when it is genuinely an
   entrance — public/motion.js -> entering decides, and these classes are what
   it puts on. Everything here is opacity and transform, which the phone can
   composite without touching layout; the one exception is the expand below,
   which is deliberately scoped to a single panel at a time. */
@keyframes m-in { from { opacity: 0; transform: translateY(6px); } }
@keyframes m-pop { from { opacity: 0; transform: scale(.97); } }
@keyframes m-fade { from { opacity: 0; } }
@keyframes m-out { to { opacity: 0; transform: scale(.98); } }

/* Something arriving in a list — a chat line, a side bet, a ticker item. */
.m-in { animation: m-in var(--m) var(--m-ease) both; }
/* Something arriving as a block — a panel, a card, a banner. */
.m-pop { animation: m-pop var(--m) var(--m-ease) both; }
.m-fade { animation: m-fade var(--m) var(--m-ease) both; }
/* A whole screen: the tab you just moved to, the splash, the log-in door.
   Opacity alone on purpose — a transform on this element would make it the
   containing block for the Bets tab's sticky section headings. */
.m-screen { animation: m-fade var(--m-slow) var(--m-ease) both; }
/* On its way out, and only ever something this phone dismissed —
   public/motion.js -> leave, which runs the callback either way. */
.leaving { animation: m-out var(--m-fast) var(--m-ease-in) both; pointer-events: none; }

/* A panel folding open, and the same run backwards. Height from nothing to
   whatever it turns out to be, with no measuring and no fixed number to keep
   in step with the content: the grid row is what animates. An opened one drops
   its class at the end so the panel is a plain box again and nothing stays
   clipped — public/motion.js.

   The panel sits inside a plain box of its own, because a track asked for 0fr
   still cannot be shorter than the PADDING AND BORDER of what is in it: a bet
   card used to snap its first 31px open and start growing from there. The
   inner box carries no padding, so the fold really does begin at nothing.

   `--fold` is how far in the fold already is, written on the wrapper by
   public/motion.js as a NEGATIVE delay — which is how a CSS animation is told
   to start part-way through. The screen is rebuilt on every change in the room,
   so a fold in flight is handed to a new node several times over; without that
   delay it would start again each time, and starting again is the stutter. */
.m-open, .m-close { display: grid; animation-duration: var(--m); animation-delay: var(--fold, 0ms); animation-fill-mode: forwards; }
.m-open { grid-template-rows: 0fr; animation-name: m-open; animation-timing-function: var(--m-ease); }
/* Shutting is its own pair rather than the open played backwards: `reverse`
   would reverse the curve with it, and an easing that settles gently on the way
   open lands hard on the way shut — the list underneath snapping up at the end
   is the one thing a fold is here to stop. Same duration, same curve, mirrored
   keyframes. */
.m-close { grid-template-rows: 1fr; animation-name: m-close; animation-timing-function: var(--m-ease); }
.m-open > *, .m-close > * { min-height: 0; overflow: hidden; }
@keyframes m-open { from { grid-template-rows: 0fr; opacity: 0; } to { grid-template-rows: 1fr; opacity: 1; } }
@keyframes m-close { from { grid-template-rows: 1fr; opacity: 1; } to { grid-template-rows: 0fr; opacity: 0; } }

/* The chevron on a bet card turns as the card opens rather than snapping, and
   turns back as it shuts. Both ride the fold's own delay, so the arrow and the
   panel beside it are always at the same point of the same move. */
@keyframes m-chev { from { transform: rotate(0); } }
@keyframes m-chev-back { from { transform: rotate(90deg); } }
.m-turn .chev { animation: m-chev var(--m) var(--m-ease) both; }
.m-turn-back .chev { animation: m-chev-back var(--m) var(--m-ease) both; }
.m-turn .chev, .m-turn-back .chev { animation-delay: var(--fold, 0ms); }

/* Press feedback on everything you can tap. One rule, one feel: the control
   gives under the thumb and comes straight back. Three sizes of give, chosen
   by the control rather than invented per rule. */
button { transition: transform var(--m-press) var(--m-ease), filter var(--m-fast) var(--m-ease), opacity var(--m-fast) var(--m-ease); }
button:active:not(:disabled) { transform: scale(var(--press)); }
.iconbtn, .textbtn, .stakes button, .tabs button { --press: var(--press-sm); }
.btn.wide, .opt, .picker-list button, .filters button { --press: var(--press-wide); }
/* The card head answers with its own plate instead — it fills the card, and a
   card that shrinks inside its own border reads as the border moving. */
.bet-head:active:not(:disabled) { transform: none; }

/* One press, one request. A control that is still waiting on the server dims
   and stops answering, so a tap that has not landed yet cannot be sent again —
   public/dom.js -> guarded, which puts this on and takes it off. It picks no
   duration of its own: the dim rides the opacity transition every button above
   already has, which is `--m-fast`. */
button[data-pending] { opacity: .5; }
[data-pending] { cursor: progress; }

/* Motion is a courtesy, never a mechanism: every state in this app lands on
   the tap, not on the end of an animation. Turned off, the whole app works
   exactly as it does with it on — public/motion.js checks the same query
   before the handful of moves it drives from script. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
