/* ==========================================================================
   ezoplay — cream-white game UI
   ==========================================================================
   Flat by design: no glows, no neon, no shiny gradients. Depth comes from a
   solid bottom edge on pressable things and a plain soft drop shadow, nothing
   else.

   Your brand colours are light tints, and a light tint on cream is unreadable as
   text (#70D6FF on #FBF8F1 is about 1.4:1). So each accent exists twice: the
   bright version is a FILL, and a darkened "ink" version is used for text and
   icons. Every text pairing here clears 4.5:1.
   ========================================================================== */

:root {
  --cyan: #70d6ff;
  --lavender: #b8c0ff;
  --mint: #7dffc4;

  --cyan-ink: #0c6b87;
  --lavender-ink: #4e56a6;
  --mint-ink: #0f7a52;

  --cream: #fbf8f1;
  --cream-2: #f3eee3;
  --card: #ffffff;
  --line: #e7e0d2;
  --line-2: #d5cdba;

  --ink: #1a1d28;
  --ink-2: #4a4f5e;
  --dim: #6e6a63;

  --warn-ink: #8a5a00;
  --warn: #ffd479;
  --danger-ink: #a32a45;
  --danger: #ff9eb5;

  --r-sm: 12px;
  --r: 18px;
  --r-lg: 24px;

  --shadow-sm: 0 1px 3px rgba(26, 29, 40, .08);
  --shadow: 0 4px 14px rgba(26, 29, 40, .09);

  --ui: "Segoe UI Variable Display", "Segoe UI", Inter, system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  /* Display type, for the one headline that is meant to be looked at rather than read.
     Falls back to the UI stack, so a font that fails to load costs a look, not a layout. */
  --display: Audex, var(--ui);

  --topbar-h: 58px;
  --appbar-h: 64px;
}

/* Audex by GGBotNet, under the SIL Open Font License 1.1 - see public/fonts/OFL.txt, which
   is served alongside the font because the licence requires it to travel with the files.
   Regular only: the hero is one line and never asks for italic or a second weight.
   `swap` so the headline is readable immediately in the fallback and upgrades when the font
   arrives - the alternative is a hero that is blank for the first moment of every visit. */
@font-face {
  font-family: Audex;
  src: url("/fonts/audex-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

/* An element with the `hidden` attribute is hidden. Full stop, no exceptions.
 *
 * The browser's own rule is `[hidden] { display: none }`, which is an ATTRIBUTE selector and
 * therefore loses to any CLASS selector that sets display. So every time a rule like
 * `.race-countdown { display: grid }` was written, the matching element stopped responding to
 * `el.hidden = true` - silently, and only in one state.
 *
 * It cost this project twice in one afternoon: `.action-bar { display: grid }` in arena mode
 * resurrected Classic's "who is the liar?" prompt and the duel's HUMAN/MACHINE buttons inside a
 * typing race, and then `.race-countdown { display: grid }` left "60 seconds. the same words for
 * both of you" sitting over the words for the whole race. There were already EIGHT hand-written
 * `.thing[hidden] { display: none }` patches in this file, one per element that had hit the same
 * trap - which is the sign that the general rule was missing rather than that the elements were
 * unusual. Those eight are gone now; this replaces all of them.
 *
 * `!important` because winning against every future class selector is the entire point. */
[hidden] { display: none !important; }

html { height: 100%; overflow-x: hidden; }

body {
  margin: 0;
  min-height: 100%;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* Flat wash. No animated grid, no bloom. */
.bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(700px 460px at 10% -14%, rgba(112, 214, 255, .16), transparent 66%),
    radial-gradient(620px 420px at 92% 2%, rgba(184, 192, 255, .14), transparent 68%);
}

/* --- app bar --------------------------------------------------------------
   Logo and name pinned top-left, settings top-right, on every screen except the
   match itself (where the game topbar already carries both).
   -------------------------------------------------------------------------- */

/* THE MARK IS THE MENU.
   The bar used to be a row of chips: logo, support, gear, profile. It is now one round mark in
   the middle with everything else folded behind it - which is what leaves the top of the page
   free for art rather than for controls.
   No background and no border: the bar is a rail the mark hangs from, so whatever the landing
   page becomes shows through it. */
.app-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 6;
  height: var(--appbar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Clicks land on the mark and the sheet, never on the empty rail either side of them. */
  pointer-events: none;
}
.app-bar > * { pointer-events: auto; }

.nav-mark {
  position: relative;
  width: 52px; height: 52px;
  padding: 0;
  /* THE STROKE IS TEAL, and both of the obvious choices were wrong.
     Black (the --ink border the rest of the site uses) is too hard here: around a circle holding a
     logo it reads as a sticker stuck onto the page rather than a mark floating above it.
     White is worse - the logo is half white, so a white ring merges into the artwork and the
     circle loses its edge exactly where it needs one.
     --cyan-ink is the site's own dark accent, already used for focus rings and links. It is dark
     enough to draw the circle, coloured enough not to read as an outline, and it belongs to the
     blue the logo is already made of. */
  border: 2.5px solid var(--cyan-ink);
  border-radius: 50%;
  background: var(--card);
  cursor: pointer;
  display: grid; place-items: center;
  box-shadow: 0 5px 18px rgba(26, 29, 40, .17), 0 1px 3px rgba(26, 29, 40, .1);
  /* The drift. Slow and small on purpose - it should read as alive rather than as animated, and
     anything faster than this becomes the thing you look at instead of the page.
     `translate` rather than `transform`, so :active can scale it without the two fighting - two
     rules setting transform means one of them silently does nothing. */
  animation: nav-drift 4.2s ease-in-out infinite;
  transition: box-shadow .16s, scale .12s;
}
.nav-mark-art { width: 36px; height: 36px; display: block; }
.nav-mark:hover { box-shadow: 0 9px 26px rgba(26, 29, 40, .24), 0 1px 3px rgba(26, 29, 40, .1); }
.nav-mark:active { scale: .94; }
.nav-mark:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 4px; }
/* OPEN IN PLACE. It stops drifting so the menu hanging off it does not wobble - and it does NOT
   move: it used to shift down 3px, which together with the sheet dropping in from above made the
   whole thing feel like it respawned somewhere else rather than opened where you pressed. */
.nav-mark.open { animation: none; translate: 0 0; }

@keyframes nav-drift {
  0%, 100% { translate: 0 -3px }
  50%      { translate: 0 3px }
}

/* A streak, riding on the mark so it is visible without opening anything. */
.nav-pip {
  position: absolute;
  top: -3px; right: -3px;
  min-width: 19px; height: 19px;
  padding: 0 4px;
  display: grid; place-items: center;
  font-size: 10.5px; font-weight: 800;
  border-radius: 999px;
  background: var(--warn);
  color: var(--warn-ink);
  border: 2px solid var(--ink);
}

/* Slides out of the mark. Anchored to the bar rather than the mark itself so the mark's drift
   does not drag the menu around with it. */
.nav-sheet {
  position: absolute;
  top: calc(var(--appbar-h) - 6px);
  left: 50%;
  translate: -50% 0;
  /* Bigger than it was. At 260px with 9px rows it read as a tooltip that happened to have
     buttons in it - this is the only navigation on the site, and it should look like somewhere you
     meant to go rather than something that popped up. */
  width: min(330px, calc(100vw - 28px));
  display: grid;
  gap: 5px;
  padding: 12px;
  background: var(--card);
  /* A hairline, not the ink border the rest of the site uses. This hangs in mid-air under a
     floating mark; a heavy black outline turns it into a box sitting on the page. */
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  box-shadow: 0 16px 40px rgba(26, 29, 40, .2), 0 2px 6px rgba(26, 29, 40, .08);
  /* IT UNFOLDS FROM THE MARK, in place. It used to slide down from above, which read as the menu
     arriving from somewhere else - "respawning" rather than opening where you pressed. Scaling from
     its own top edge is the whole difference: the movement is the panel growing out of the button,
     and the button no longer moves at all.
     translate/scale rather than transform, so the centring translate is not something the
     animation has to remember to reapply on every frame. */
  transform-origin: top center;
  animation: nav-open .15s cubic-bezier(.2, .8, .25, 1) both;
}
@keyframes nav-open {
  from { opacity: 0; scale: .94 }
  to   { opacity: 1; scale: 1 }
}

.nav-row {
  display: flex; align-items: center; gap: 13px;
  width: 100%;
  font: inherit; font-size: 15.5px; font-weight: 750;
  text-align: left; text-decoration: none;
  color: var(--ink);
  /* A comfortable 46px tall. Also the minimum a thumb hits reliably, which the old 34px row was
     under - so this is legibility and reach at the same time. */
  padding: 12px 14px;
  border: 0; border-radius: var(--r-sm);
  background: none; cursor: pointer;
  transition: background .12s;
}
.nav-row:hover { background: var(--cream-2); }
.nav-row:active { background: var(--line); }
.nav-ico { font-size: 19px; width: 22px; text-align: center; }

/* The profile chip is the first row of the menu, and the biggest thing in it - it is the row people
   are looking for. A hairline under it separates who you are from what you can do. */
.nav-sheet .me-chip {
  width: 100%;
  justify-content: flex-start;
  gap: 11px;
  padding: 11px 13px;
  margin-bottom: 5px;
  border: 0;
  border-bottom: 1.5px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--cream-2);
  font-size: 15px;
}
.nav-sheet .me-chip .avatar { width: 40px; height: 40px; }
.nav-sheet .me-chip .edit { font-size: 10.5px; margin-left: auto; }

@media (prefers-reduced-motion: reduce) {
  .nav-mark { animation: none; }
  .nav-sheet { animation: none; }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  margin-right: auto;
  padding: 4px 8px 4px 4px;
  background: none;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
  transition: background .14s;
}
.brand:hover { background: var(--cream-2); }
.brand:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }

.brand-mark { height: 40px; width: auto; display: block; }

.brand-name {
  font-size: 17px;
  font-weight: 860;
  letter-spacing: -.02em;
  line-height: 1;
  white-space: nowrap;
}
.brand-name em { font-style: normal; color: var(--cyan-ink); }

/* Larger, deliberately prominent settings button. */
.icon-btn.gear-lg {
  width: 46px;
  height: 46px;
  font-size: 22px;
  border-radius: 13px;
}

/* --- view scaffolding -----------------------------------------------------
   Scrolling views (menus, lobby, result) grow as tall as their content and the
   page scrolls normally. The game view is the one exception: it is pinned to
   the viewport so the chat scrolls inside itself instead of the page moving.
   -------------------------------------------------------------------------- */

.view {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--appbar-h) + 26px) 18px 40px;
}

/* The match view is pinned to the viewport and supplies its own header. */
.view-game { padding: 0; }

h1, h2, h3 { font-weight: 780; letter-spacing: -.01em; margin: 0; }
.center { text-align: center; }

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

.btn {
  --fill: #ffffff;
  --edge: var(--line-2);
  --fg: var(--ink);
  font-family: inherit;
  font-size: 14px;
  font-weight: 750;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 13px 22px;
  border: 0;
  border-radius: var(--r-sm);
  background: var(--fill);
  color: var(--fg);
  cursor: pointer;
  box-shadow: 0 4px 0 var(--edge);
  transition: transform .07s ease, box-shadow .07s ease, background .14s ease;
  user-select: none;
}
.btn:hover:not(:disabled) { background: color-mix(in srgb, var(--fill) 90%, #000 10%); }
.btn:active:not(:disabled) { transform: translateY(4px); box-shadow: 0 0 0 var(--edge); }
.btn:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn-primary { --fill: var(--cyan); --edge: #3e93b0; --fg: #06222e; }
.btn-lav { --fill: var(--lavender); --edge: #7b83c9; --fg: #191d38; }
.btn-mint { --fill: var(--mint); --edge: #3ab989; --fg: #05301f; }
.btn-warn { --fill: var(--warn); --edge: #c79b32; --fg: #3a2a00; }
.btn-danger { --fill: var(--danger); --edge: #c26480; --fg: #46101f; }

/* Ko-fi, in their own brand teal (#30c9c1) so it reads as an external action
   rather than part of the game's own button set. */
.btn-kofi {
  --fill: #30c9c1;
  --edge: #1f8f89;
  --fg: #06282a;
}

/* Anchors styled as buttons need these; <button> gets them for free. */
a.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  justify-content: center;
  text-decoration: none;
}
.kofi-cup { font-size: 14px; line-height: 1; }

.btn-ghost {
  background: transparent;
  color: var(--ink-2);
  box-shadow: inset 0 0 0 1.5px var(--line-2);
}
.btn-ghost:hover:not(:disabled) { background: var(--cream-2); color: var(--cyan-ink); box-shadow: inset 0 0 0 1.5px var(--cyan-ink); }
.btn-ghost:active:not(:disabled) { transform: translateY(2px); }

.btn-sm { padding: 9px 14px; font-size: 12px; }
.btn-lg { padding: 17px 30px; font-size: 15.5px; }
.btn-block { width: 100%; }
.row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

/* --- inputs --------------------------------------------------------------- */

input {
  font-family: inherit;
  font-size: 15px;
  padding: 13px 16px;
  border: 1.5px solid var(--line-2);
  border-radius: var(--r-sm);
  background: #fff;
  color: var(--ink);
  outline: none;
  min-width: 0;
  transition: border-color .14s;
}
input:focus { border-color: var(--cyan-ink); }
input::placeholder { color: #a09a90; }

/* --- panels --------------------------------------------------------------- */

.panel {
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  padding: 20px;
  box-shadow: var(--shadow);
}
.panel h2 {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .15em;
  color: var(--lavender-ink);
  margin-bottom: 14px;
}
.count { color: var(--dim); }
.hint { color: var(--dim); font-size: 12.5px; margin: 12px 0 0; }
.hint.center { margin: 0; }

/* --- avatars -------------------------------------------------------------- */

.avatar {
  display: block;
  border-radius: 50%;
  background: var(--cream-2);
  object-fit: cover;
  flex: none;
}
.avatar.ring { box-shadow: 0 0 0 2.5px currentColor; }

/* A turning ring of colour around an avatar.
 *
 * Drawn with PADDING and a conic-gradient BACKGROUND rather than a pseudo-element, because
 * avatarEl() returns an <img> and a replaced element has no ::before to hang a ring on. The image
 * fills the content box; the gradient shows through the padding around it. box-sizing is content-box
 * so the picture keeps its size and the ring is added outside it, rather than eating 6px of face.
 *
 * The turn comes from animating a registered custom property: a gradient cannot be animated
 * directly, but its starting angle can once the browser knows the value is an <angle>. Where
 * @property is unsupported this degrades to a still rainbow ring, which is a fine thing to degrade
 * to - and `rotate` is not an option here, because rotating the element would rotate the face
 * inside it too. */
@property --aura-turn {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.avatar.aura-rainbow {
  box-sizing: content-box;
  padding: 3px;
  background: conic-gradient(
    from var(--aura-turn),
    #ff5d5d, #ffab4d, #ffe95c, #63ff8f, #4de3ff, #5b83ff, #c46bff, #ff5d5d
  );
  animation: aura-turn 3.2s linear infinite;
}
/* Beats .avatar.ring on specificity rather than on source order, so the two cannot stack into a
   muddy double ring depending on which rule happens to come last. */
.avatar.ring.aura-rainbow { box-shadow: none; }

@keyframes aura-turn { to { --aura-turn: 360deg } }

@media (prefers-reduced-motion: reduce) {
  .avatar.aura-rainbow { animation: none; }
}

/* --- home ----------------------------------------------------------------- */

.home { width: 100%; max-width: 780px; text-align: center; }

.tagline {
  color: var(--ink-2);
  font-size: 15.5px;
  margin: 0 auto 22px;
  max-width: 30em;
}

/* --- the hero -------------------------------------------------------------
   Everything in here arrives on load, in order, roughly as fast as an eye moves down
   the page. Nothing loops: a landing page that keeps moving after you have read it is
   just noise. See @media (prefers-reduced-motion) at the end of this block. */
.hero { display: grid; justify-items: center; gap: 0; margin-bottom: 28px; }
.hero-copy { display: grid; justify-items: center; }

/* Deliberately not larger: the top of the shelf should still be visible on a laptop
   without scrolling, so the hero has a height budget. */
.hero-mark {
  width: 84px;
  height: 84px;
  margin-bottom: 11px;
  filter: drop-shadow(0 8px 18px rgba(24, 30, 52, .16));
  animation: hero-in .5s cubic-bezier(.2, .8, .25, 1) both;
}

.hero-title {
  font-family: var(--display);
  /* Audex runs wider than the UI face, so the same clamp would overflow a phone. */
  font-size: clamp(27px, 5.6vw, 46px);
  font-weight: 400; /* the display face has one weight; asking for 780 would fake-bold it */
  line-height: 1.08;
  letter-spacing: -.005em;
  margin: 0 0 12px;
  max-width: 17em;
  animation: hero-in .5s cubic-bezier(.2, .8, .25, 1) .06s both;
}
/* The second half of the line is the punchline, so it is said quieter. */
.hero-title em { font-style: normal; color: var(--cyan-ink); }

/* The line the headline types into. See headline.js.
   Two rules here stop the animation from shoving the page around, which is the failure mode of
   every typewriter headline on the web:
     min-height keeps the line's full height while it is empty, so the buttons below do not slide
     up at the start and back down at the end;
     nowrap keeps a growing phrase on one line - allowed to wrap it would add a whole line mid-word
     and knock everything down with it. */
.type-swap {
  display: inline-flex;
  align-items: baseline;
  min-height: 1.08em;
  white-space: nowrap;
}
.type-caret {
  /* Sized from the text, so it stays a cursor on a phone and on a monitor. */
  width: .07em;
  min-width: 2px;
  height: .95em;
  margin-left: .06em;
  background: var(--cyan-ink);
  border-radius: 1px;
  opacity: 0; /* nothing to point at until the typing starts */
  transform: translateY(.04em);
}
.type-caret.on { animation: caret-blink 1.05s steps(1, end) infinite; }
/* Left where it stopped, no longer blinking: a headline still holding a live cursor reads as
   unfinished. `done` also covers reduced motion, which never gets a blink at all. */
.type-caret.done { opacity: .32; animation: none; }
@keyframes caret-blink { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }

.hero .tagline {
  margin-bottom: 20px;
  animation: hero-in .5s cubic-bezier(.2, .8, .25, 1) .12s both;
}
.hero .tagline b { color: var(--ink); }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  animation: hero-in .5s cubic-bezier(.2, .8, .25, 1) .18s both;
}

@keyframes hero-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

/* --- the round that plays itself ------------------------------------------
   Fixed height, always. The bubbles arrive one at a time, and a panel that grew with each
   one would shove the whole page down four times a loop. */
.hero-demo {
  width: 100%;
  max-width: 380px;
  text-align: left;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  padding: 12px 14px 14px;
  animation: hero-in .5s cubic-bezier(.2, .8, .25, 1) .28s both;
}

.demo-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
/* Says what it is, in the one place somebody might mistake it for a live room. */
.demo-tag {
  font-size: 9.5px; font-weight: 850; text-transform: uppercase; letter-spacing: .16em;
  color: var(--lavender-ink); background: #eeefff;
  border-radius: 999px; padding: 3px 8px;
}
.demo-what { font-size: 11px; font-weight: 700; color: var(--dim); }

.demo-log {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 8px;
  /* Sized so the longest round fits whole. A test computes every round's height from these
     numbers and this box, and fails if one of them does not fit. */
  height: 186px;

  /* THE AVATAR RINGS WERE BEING SHAVED HERE.
     `.avatar.ring` is a box-shadow, which paints OUTSIDE the element's box. A plain
     `overflow: hidden` clips at the content edge, so it took 2.5px off the left of every
     ring and off the bottom of the last row - the flat edge you can see on the circles.
     `clip` with a margin clips in the same place but lets that overhang paint, which is
     exactly what this property is for. `hidden` first so an engine that does not know
     `clip` still contains a long round instead of spilling it. */
  overflow: hidden;
  overflow: clip;
  overflow-clip-margin: 6px;

  transition: opacity .45s ease;
}
.demo-log.fading { opacity: 0; }

/* The game talking rather than a player: a dealt word, nightfall, a body in the morning.
   Centred and quiet so it is never mistaken for somebody's line. */
.demo-note {
  align-self: center;
  text-align: center;
  font-size: 11.5px;
  line-height: 1.35;
  font-weight: 750;
  color: var(--dim);
  background: var(--cream-2);
  border-radius: 999px;
  padding: 5px 12px;
  animation: demo-line-in .34s cubic-bezier(.2, .8, .25, 1) both;
}

.demo-line {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 28px;
  animation: demo-line-in .34s cubic-bezier(.2, .8, .25, 1) both;
}
.demo-line .avatar { width: 26px; height: 26px; flex: none; }
/* Its own size and leading rather than the inherited 15px/1.5, which built a line box
   taller than the text in it and pushed the two halves of the row out of step. */
.demo-body { min-width: 0; font-size: 13px; line-height: 1.4; }
.demo-who {
  font-size: 11px;
  font-weight: 850;
  margin-right: 6px;
  /* The avatar palette is pale by design - readable as a fill, not as 11px text on white. */
  filter: brightness(.7) saturate(1.35);
}
.demo-said { font-size: 13px; color: var(--ink-2); overflow-wrap: anywhere; }

@keyframes demo-line-in {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}

/* Somebody is typing. Its own dots rather than the chat's, so restyling one never quietly
   restyles the other. The row around them is a normal .demo-line, avatar included, so it is
   exactly the height of the line it turns into. */
.demo-dots { display: inline-flex; align-items: center; gap: 4px; height: 19px; }
.demo-dots span {
  width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; opacity: .45;
  animation: demo-blink 1.1s infinite;
}
.demo-dots span:nth-child(2) { animation-delay: .18s; }
.demo-dots span:nth-child(3) { animation-delay: .36s; }
@keyframes demo-blink {
  0%, 60%, 100% { opacity: .3; transform: none; }
  30% { opacity: 1; transform: translateY(-2px); }
}

.demo-verdict {
  margin-top: 2px;
  font-size: 12px;
  line-height: 1.35;
  font-weight: 800;
  color: var(--danger-ink);
  background: #ffe9ef;
  border-radius: 10px;
  padding: 8px 11px;
  opacity: 0;
  transform: scale(.94);
  transition: opacity .3s ease, transform .3s cubic-bezier(.2, .8, .25, 1);
}
.demo-verdict.in { opacity: 1; transform: none; }
/* The room got it right. Both outcomes are in the rotation on purpose - a demo where the
   liar always loses would be a worse advert than one where sometimes they walk. */
.demo-verdict.good { color: var(--mint-ink); background: #e3fff2; }

/* Stacked, below 900px. The demo needs air: without it the panel sits directly under the buttons
   and reads as part of the hero rather than as a thing of its own. */
@media (max-width: 899px) {
  .hero-demo { margin-top: 26px; }
}

/* Side by side once there is room, which costs the page no extra height at all - the demo
   is shorter than the words next to it. Below this it stacks and everything centres. */
@media (min-width: 900px) {
  .home { max-width: 880px; }
  .hero {
    grid-template-columns: minmax(0, 1fr) 380px;
    align-items: center;
    gap: 30px;
    text-align: left;
  }
  .hero-copy { justify-items: start; text-align: left; }
  .hero-copy .tagline { margin-left: 0; margin-right: 0; }
  .hero-title { max-width: none; }
  .hero-actions { justify-content: flex-start; }
}

/* ==========================================================================
   WATCHING
   A friend's game, read-only. The bar is loud on purpose: being a spectator and
   thinking you are a player is the one confusion worth preventing outright.
   ========================================================================== */
.watch-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: none;
  padding: 7px 14px;
  background: var(--lavender);
  border-bottom: 2px solid var(--lavender-ink);
  color: var(--lavender-ink);
  font-size: 12.5px;
  font-weight: 800;
}
.watch-bar #watch-who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.watch-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--danger-ink);
  animation: watch-live 1.6s ease-in-out infinite;
}
@keyframes watch-live { 50% { opacity: .35 } }

/* Nothing in a spectated room is a target. The server refuses it all anyway - a spectator has no
   seat and every handler looks one up - so this is about not offering it. */
.view-game.spectating .roster li,
.view-game.spectating .msg .bubble,
.view-game.spectating .msg .who { cursor: default; }
.view-game.spectating .msg .bubble { pointer-events: none; }

.watch-btn { flex: none; }

@media (prefers-reduced-motion: reduce) { .watch-dot { animation: none } }

/* ==========================================================================
   THE BOARDS
   Weekly, so they stay winnable. Hidden entirely until somebody is on one - an
   empty leaderboard announces that nobody is here.
   ========================================================================== */
.boards-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.boards-head h2 { flex: 1; min-width: 0; }

/* A two-state switch. Not a <select>: there are exactly two options and both fit. */
.seg { display: inline-flex; border: 2px solid var(--ink); border-radius: 999px; overflow: hidden; }
.seg-btn {
  font: inherit; font-size: 11.5px; font-weight: 800;
  padding: 5px 13px; border: 0; cursor: pointer;
  background: var(--card); color: var(--ink-2);
}
.seg-btn.on { background: var(--ink); color: var(--cream); }

.boards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}
.board {
  background: var(--cream-2);
  border: 2px solid var(--line-2);
  border-radius: var(--r);
  padding: 12px 13px;
}
.board-title {
  font-size: 9.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .14em; color: var(--dim); margin-bottom: 9px;
}
.board-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.board-row {
  display: grid;
  grid-template-columns: 20px auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  padding: 3px 5px;
  border-radius: 8px;
}
/* Your own row, so you can find yourself without reading. */
.board-row.mine { background: var(--card); box-shadow: inset 0 0 0 1.5px var(--cyan-ink); font-weight: 800; }
.board-rank { font-size: 11px; font-weight: 800; color: var(--dim); text-align: right; }
/* Only the top three get colour. Medals for everybody is just a list in colour. */
.board-rank.r1 { color: #a06800; }
.board-rank.r2 { color: #6b7280; }
.board-rank.r3 { color: #8a5a2b; }
/* text-align stated, because the home panel is centre-aligned and an unset name drifts into the
   middle of its column - which turns a scannable list into a ragged one. */
.board-name { text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-score { text-align: right; }
.board-score { font-family: var(--display); font-weight: 400; font-size: 15px; }
.board-you { margin: 9px 0 0; font-size: 11px; font-weight: 700; color: var(--cyan-ink); }

@media (max-width: 640px) {
  .boards-grid { grid-template-columns: 1fr; gap: 9px; }
  .board { padding: 10px 11px; }
}

/* ==========================================================================
   TITLES AND PROFILES
   A title is a badge you can wear, so it goes wherever a name goes. A profile is
   what opens when you tap one.
   ========================================================================== */
.title-pip {
  display: inline-block;
  font-size: 9px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: 1.5px 6px;
  border-radius: 999px;
  background: var(--lavender);
  color: var(--lavender-ink);
  border: 1px solid var(--lavender-ink);
  vertical-align: 1px;
  /* One line, always. A long title wrapping inside a chat name pushes the bubble around. */
  white-space: nowrap;
}

/* Anything you can tap to find out who somebody is. Deliberately subtle: a roster of six
   obvious buttons reads as a menu rather than as a list of people. */
.tappable { cursor: pointer; }
.who.tappable:hover { text-decoration: underline; text-underline-offset: 2px; }
.roster li.tappable:hover { background: var(--cream-2); }
.person .avatar.tappable:hover { filter: brightness(1.06); }

.profile-sheet { max-width: 460px; }
.who-head { display: flex; align-items: center; gap: 13px; margin-bottom: 4px; }
.who-face { flex: none; }
.who-id { min-width: 0; }
.who-id .sheet-title { margin: 0; overflow-wrap: anywhere; }
.who-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-top: 4px; }
.who-handle { font-size: 12px; color: var(--dim); font-weight: 700; }
.who-title { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .1em;
  padding: 2px 8px; border-radius: 999px; background: var(--lavender); color: var(--lavender-ink);
  border: 1px solid var(--lavender-ink); }
/* Your own title is the button that changes it, because your own card is where you look at it. */
.who-title.editable { cursor: pointer; border-style: dashed; }
.who-title.editable:hover { background: var(--cyan); color: var(--cyan-ink); border-color: var(--cyan-ink); }

.who-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(76px, 1fr));
  gap: 8px;
  margin: 12px 0 10px;
}
.who-stat {
  text-align: center;
  padding: 9px 4px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
}
.who-stat b { display: block; font-size: 19px; font-family: var(--display); font-weight: 400; }
.who-stat span { font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); }

.who-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.who-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; font-weight: 700;
  padding: 4px 9px;
  background: var(--card);
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
}
.who-badge .bdg-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--mint); border: 1px solid var(--mint-ink); }

.title-list { display: grid; gap: 6px; margin: 8px 0 12px; max-height: 46vh; overflow-y: auto; }
.title-row {
  display: grid; gap: 1px;
  text-align: left; font: inherit; color: var(--ink);
  padding: 9px 12px; cursor: pointer;
  background: var(--card);
  border: 2px solid var(--line-2);
  border-radius: var(--r-sm);
}
.title-row:hover { border-color: var(--ink); }
.title-row.on { border-color: var(--lavender-ink); background: #f2f3ff; }
.title-row b { font-size: 13.5px; }
.title-row span { font-size: 11.5px; color: var(--dim); }

@media (max-width: 640px) {
  .who-stats { grid-template-columns: repeat(auto-fit, minmax(66px, 1fr)); gap: 6px; }
  .who-stat b { font-size: 17px; }
  .title-pip { font-size: 8.5px; }
}

/* ==========================================================================
   REACTIONS
   Six, fixed. A tap of a bubble opens the picker; a tap of a pill toggles.
   ========================================================================== */
.reacts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }

.react-pill {
  display: inline-flex; align-items: center; gap: 3px;
  font: inherit; font-size: 11px; font-weight: 700;
  padding: 2px 7px;
  cursor: pointer;
  background: var(--card);
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-2);
  /* Emoji only render in colour on some platforms if the font stack allows it, and the UI face
     does not carry them - so the system emoji font is named first. */
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', var(--ui);
  transition: border-color .12s, transform .07s;
}
.react-pill:hover { border-color: var(--ink); }
.react-pill:active { transform: scale(.94); }
/* Yours, so a second tap is obviously a takeback rather than another vote. */
.react-pill.mine { border-color: var(--cyan-ink); background: #eaf7fd; color: var(--cyan-ink); }

.react-picker {
  display: flex;
  gap: 2px;
  margin-top: 5px;
  padding: 4px;
  width: max-content;
  background: var(--card);
  border: 2px solid var(--ink);
  border-radius: 999px;
  box-shadow: 0 6px 16px rgba(26, 29, 40, .16);
  animation: react-pop .13s cubic-bezier(.2, .8, .25, 1) both;
}
.react-picker button {
  font-size: 18px;
  line-height: 1;
  /* 34px, because this is a phone-first target and anything smaller gets mis-tapped. */
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 0; background: none; cursor: pointer; border-radius: 50%;
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', var(--ui);
  transition: background .12s, transform .07s;
}
.react-picker button:hover { background: var(--cream-2); transform: scale(1.12); }
.react-picker button:active { transform: scale(.95); }

@keyframes react-pop { from { opacity: 0; transform: translateY(-3px) scale(.96) } to { opacity: 1 } }

/* My own messages sit on the right, so their reactions and picker line up with them.
   justify-content rather than margin-left:auto - .reacts is a full-width flex row, and an auto
   margin has no free space to push into, so the pills stayed pinned to the bubble's left edge. */
.msg.mine .reacts { justify-content: flex-end; }
.msg.mine .react-picker { align-self: flex-end; }

/* The bubble is tappable now, so it should say so - but only on a pointer device, where a
   cursor change is readable. */
@media (hover: hover) { .msg .bubble { cursor: pointer; } }

@media (prefers-reduced-motion: reduce) {
  .react-picker { animation: none; }
  .react-picker button:hover, .react-pill:active, .react-picker button:active { transform: none; }
}

/* The one line of shared history under a friend's name. Quiet on purpose - it is a reason to
   scroll the list, not a thing to read first. */
.h2h {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--dim);
  margin-top: 2px;
  /* One line. A long pair of names wrapping to three lines would push the next friend off
     screen, and the list has to stay scannable more than it has to be complete. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ==========================================================================
   THE REEL
   The part of the result screen worth screenshotting. Both halves are facts the
   round already had and nobody could see: the line that reads most like a machine,
   and who voted for whom.
   ========================================================================== */
.reel {
  display: grid;
  gap: 12px;
  margin: 14px 0 4px;
  text-align: left;
}
.reel-block {
  padding: 12px 14px;
  background: var(--cream-2);
  border: 2px solid var(--line-2);
  border-radius: var(--r);
}
.reel-label {
  font-size: 9.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: .16em; color: var(--dim); margin-bottom: 7px;
}

.reel-quote {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 7px;
  font-size: 15px; line-height: 1.4;
}
.reel-who { font-size: 11px; font-weight: 800; color: var(--dim); text-transform: lowercase; }
.reel-text { font-weight: 700; overflow-wrap: anywhere; }
.reel-tell { margin: 7px 0 0; font-size: 12.5px; color: var(--ink-2); }

/* One row per vote. Two columns rather than a sentence: the point is scanning it, and
   "who did YOU get" is the first thing anybody looks for - hence .mine. */
.reel-vote-list { display: grid; gap: 5px; }
.reel-vote {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  font-size: 13px;
  padding: 4px 8px;
  border-radius: 8px;
}
.reel-vote.mine { background: var(--card); box-shadow: inset 0 0 0 1.5px var(--line-2); font-weight: 800; }
/* Both stated explicitly: the result panel is centre-aligned, so an unset .v-to inherits that
   and long names drift away from the arrow while short ones sit under it. */
.reel-vote .v-from { text-align: right; overflow-wrap: anywhere; }
.reel-vote .v-to { text-align: left; overflow-wrap: anywhere; }
.reel-vote .v-arrow { color: var(--dim); font-size: 12px; }
/* A machine, on either side of the arrow. Same mint the reveal card uses, so the colour
   already means "this was the machine" by the time you get here. */
.reel-vote .was-ai { color: var(--mint-ink); font-weight: 800; }

@media (max-width: 640px) {
  .reel-block { padding: 10px 11px; }
  .reel-quote { font-size: 14px; }
  .reel-vote { font-size: 12px; gap: 6px; }
}

/* ==========================================================================
   THE DAILY
   The one thing here playable alone. It sits above the shelf because a stranger
   who arrives to an empty site needs something to do before they need a lobby.
   ========================================================================== */
/* THE ORB. This was a full-width card above the shelf, which spent a slab of the landing page
   on one button. It drifts in the corner now: same reach, a fraction of the room.
   Fixed rather than in the flow, because it belongs to the site rather than to a screen - which
   also means whichever view is up decides whether it is there at all (setDailyVisible). */
.daily-orb {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 5;
  width: 62px; height: 62px;
  padding: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 0;
  cursor: pointer;
  font: inherit;
  /* Same reasoning as the mark, and the ring follows the orb's own state below - a teal ring on a
     mint orb would look borrowed. */
  border: 2.5px solid var(--cyan-ink);
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, #ffffff, var(--cyan));
  color: #05303d;
  box-shadow: 0 8px 20px rgba(26, 29, 40, .2);
  /* Two animations at once: a slow bob, and a longer sideways sway - together they read as
     drifting rather than as bouncing on a spring. */
  animation: orb-bob 3.4s ease-in-out infinite, orb-sway 9s ease-in-out infinite;
  transition: box-shadow .16s;
}
.daily-orb:hover { box-shadow: 0 12px 28px rgba(26, 29, 40, .3); }
.daily-orb:active { animation: none; transform: scale(.93); }
.daily-orb:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 3px; }

/* Played it: it stops shouting. Still there, still opens the answer.
   The ring changes with it, so each state is one colour family rather than a teal edge round a
   mint circle. */
.daily-orb.got {
  background: radial-gradient(circle at 32% 28%, #ffffff, var(--mint));
  color: #05301f;
  border-color: var(--mint-ink);
}
.daily-orb.missed {
  background: radial-gradient(circle at 32% 28%, #ffffff, var(--danger));
  color: #46101f;
  border-color: var(--danger-ink);
}
.daily-orb.got, .daily-orb.missed { animation-duration: 5.5s, 14s; }

.orb-face { font-family: var(--display); font-size: 21px; line-height: 1; }
.orb-label {
  font-size: 7.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .12em;
  opacity: .72; margin-top: 1px;
}
.orb-streak {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 21px; height: 21px;
  padding: 0 4px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800;
  border-radius: 999px;
  background: var(--warn);
  color: var(--warn-ink);
  border: 2px solid var(--ink);
}

@keyframes orb-bob {
  0%, 100% { translate: 0 -5px }
  50%      { translate: 0 5px }
}
/* `rotate` rather than another translate, so the two animations compose instead of one winning:
   translate/rotate/scale are separate properties, and two @keyframes both animating `transform`
   would mean only the last one applies. */
@keyframes orb-sway {
  0%, 100% { rotate: -5deg }
  50%      { rotate: 5deg }
}

@media (max-width: 640px) {
  /* Clear of the friends tab on the left and of a phone's home gesture area at the bottom. */
  .daily-orb { right: 12px; bottom: max(14px, env(safe-area-inset-bottom)); width: 56px; height: 56px; }
  .orb-face { font-size: 19px; }
}

@media (prefers-reduced-motion: reduce) {
  .daily-orb { animation: none; }
}

/* --- the puzzle sheet --- */
.daily-sheet { max-width: 520px; }
.daily-lines { display: grid; gap: 8px; margin: 4px 0 12px; }

.daily-line {
  display: flex; align-items: flex-start; gap: 10px;
  width: 100%; text-align: left; font: inherit; color: var(--ink);
  padding: 9px 11px;
  background: var(--card);
  border: 2px solid var(--line-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color .12s, transform .07s, background .12s;
}
.daily-line:hover:not(:disabled) { border-color: var(--ink); transform: translateX(2px); }
.daily-line:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }
.daily-line:disabled { cursor: default; }
.daily-line-body { flex: 1; min-width: 0; }
.daily-line .who { font-size: 11px; font-weight: 800; margin-bottom: 2px; }
.daily-line .bubble { font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }

/* After the answer. The machine's line is marked in mint whether or not you picked it, and
   your wrong pick is marked separately - seeing BOTH is what teaches you anything. */
.daily-line.was-machine { border-color: var(--mint-ink); background: #e9fff5; }
.daily-line.was-wrong { border-color: var(--danger-ink); background: #fff1f5; }
.daily-tag {
  flex: none; align-self: center;
  font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .12em;
  padding: 3px 8px; border-radius: 999px;
  background: var(--mint); color: #05301f; border: 1.5px solid var(--mint-ink);
}

.daily-after { display: grid; gap: 8px; }
.daily-verdict { font-family: var(--display); font-size: 22px; line-height: 1 }
.daily-verdict.good { color: var(--mint-ink); }
.daily-verdict.bad { color: var(--danger-ink); }
.daily-tell { margin: 0; font-size: 13.5px; color: var(--ink-2); }

@media (max-width: 640px) {
  /* The sheet, not the orb - the orb has its own block up where it is defined. */
  .daily-sheet { max-width: none; }
}

/* Panels below the shelf rise in as they are scrolled to, once each. Set by an
   IntersectionObserver in app.js; without one, nothing is ever hidden. */
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .5s ease, transform .5s cubic-bezier(.2, .8, .25, 1); }
.reveal.shown { opacity: 1; transform: none; }

/* Profile chip. Lives in the app bar on the right, next to support and settings. */
.me-chip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 5px 13px 5px 5px;
  background: var(--card);
  border: 1.5px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink);
  transition: border-color .14s, transform .07s;
}
.me-chip:hover { border-color: var(--cyan-ink); }
.me-chip:active { transform: translateY(2px); }
.me-chip:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }
.me-chip b { font-weight: 750; }
.me-chip .me-text { display: inline-flex; align-items: center; gap: 6px; }
.me-chip .edit { color: var(--cyan-ink); font-size: 10px; text-transform: uppercase; letter-spacing: .12em; font-weight: 800; }
.me-chip .pid { color: var(--dim); font-family: var(--mono); font-size: 11px; }

/* --- the shelf -----------------------------------------------------------
   Covers, sized by the grid rather than by the image, so art of the wrong
   dimensions is cropped instead of breaking the row.
   ------------------------------------------------------------------------ */

.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 18px;
  text-align: left;
}

.cover-card {
  padding: 0;
  border: 1.5px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow);
  overflow: hidden;
  cursor: pointer;
  font-family: inherit;
  color: var(--ink);
  text-align: left;
  display: flex;
  flex-direction: column;
  transition: transform .16s cubic-bezier(.2, .8, .25, 1), border-color .14s ease, box-shadow .18s ease;
  /* The stagger index, set by renderShelf() so the covers land one after another
     instead of all at once. */
  animation: cover-in .48s cubic-bezier(.2, .8, .25, 1) calc(var(--i, 0) * 55ms) both;
}
.cover-card:hover { border-color: var(--cyan-ink); transform: translateY(-5px); box-shadow: 0 14px 28px rgba(24, 30, 52, .13); }
.cover-card:active { transform: translateY(-1px); }
.cover-card:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 3px; }
.cover-card.soon { cursor: default; }
.cover-card.soon:hover { transform: none; border-color: var(--line); box-shadow: var(--shadow); }

@keyframes cover-in {
  from { opacity: 0; transform: translateY(20px) scale(.97); }
  to { opacity: 1; transform: none; }
}

/* Square, held by aspect-ratio so the row stays even before the images decode.
   The art is an isolated object on transparency and several pieces are near-white, so
   the tile carries the game's accent behind it - on cream those would disappear. */
.cover-art {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--cream-2);
  overflow: hidden;
}
.cover-art img {
  width: 100%;
  height: 100%;
  /* contain, not cover: cropping an icon cuts the drawing, and on a square tile with
     square art the two are identical anyway. */
  object-fit: contain;
  padding: 7%;
  box-sizing: border-box;
  display: block;
  transition: transform .3s cubic-bezier(.2, .8, .25, 1);
}
/* The art leans in when you point at it. 3% is enough to feel and not enough to notice
   the padding shrinking. */
.cover-card:not(.soon):hover .cover-art img { transform: scale(1.045); }

/* A light band crosses the art on hover - one pass, left to right. It is a pseudo-element
   on the frame rather than a filter so it never touches the drawing itself. */
.cover-art::after {
  content: "";
  position: absolute;
  top: -20%;
  bottom: -20%;
  left: -60%;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .5), transparent);
  transform: skewX(-14deg);
  pointer-events: none;
  opacity: 0;
}
.cover-card:not(.soon):hover .cover-art::after { animation: sheen .62s ease-out; }
@keyframes sheen {
  from { left: -60%; opacity: .9; }
  to { left: 130%; opacity: 0; }
}
.accent-cyan .cover-art, .accent-cyan .play-art { background: var(--cyan); }
.accent-lav .cover-art, .accent-lav .play-art { background: var(--lavender); }
.accent-mint .cover-art, .accent-mint .play-art { background: var(--mint); }

/* Shown when a cover is missing or fails to load, so a game with no art yet still
   looks like a deliberate card rather than a broken image. */
.cover-fallback {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  padding: 18px;
  text-align: center;
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .02em;
}
.cover-art.no-art .cover-fallback { display: grid; }
.cover-art.no-art img { display: none; }
.accent-cyan .cover-fallback { background: var(--cyan); color: #06222e; }
.accent-lav .cover-fallback { background: var(--lavender); color: #191d38; }

.cover-label { padding: 13px 15px 15px; border-top: 1.5px solid var(--line); }
.cover-label .who,
.play-info .who { font-size: 10.5px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; }
.accent-cyan .who { color: var(--cyan-ink); }
.accent-lav .who { color: var(--lavender-ink); }
.cover-label h3 { font-size: 17px; margin: 3px 0 4px; }
.cover-label p { margin: 0; font-size: 12.5px; color: var(--ink-2); }
.cover-badge {
  display: inline-block; margin-top: 8px;
  font-size: 10px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 999px;
  background: var(--cream-2); color: var(--dim);
}

/* --- play sheet ----------------------------------------------------------- */

.play-sheet { width: 100%; max-width: 780px; display: grid; gap: 16px; }

.play-card { display: grid; grid-template-columns: 240px 1fr; gap: 22px; align-items: start; }
.play-art {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--r);
  overflow: hidden;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
}
.play-art img {
  width: 100%; height: 100%;
  object-fit: contain;
  padding: 6%;
  box-sizing: border-box;
  display: block;
}
.play-art.no-art .cover-fallback { display: grid; }
.play-art.no-art img { display: none; }

.play-info h2 { font-size: 26px; margin: 4px 0 10px; text-transform: none; letter-spacing: -.01em; color: var(--ink); }
.play-info p { color: var(--ink-2); font-size: 14px; margin: 0 0 18px; }
.play-info .actions { display: grid; gap: 9px; }
.play-info .join-row { display: flex; gap: 8px; }
.play-info .join-row input { flex: 1; }

#input-code {
  text-align: center;
  letter-spacing: .3em;
  text-indent: .3em;
  text-transform: uppercase;
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 700;
  color: var(--cyan-ink);
}

.rules { margin-top: 20px; text-align: left; }
.rules summary { cursor: pointer; color: var(--lavender-ink); font-size: 12px; letter-spacing: .12em; text-transform: uppercase; font-weight: 750; }
.rules summary:hover { color: var(--cyan-ink); }
.rules ol { color: var(--ink-2); font-size: 13.5px; padding-left: 20px; margin: 12px 0 4px; }
.rules li { margin-bottom: 8px; }

/* Per-game rules on the play sheet: same block, no panel around it. */
.play-info .rules { margin-top: 16px; border-top: 1.5px solid var(--line); padding-top: 14px; }

/* How you actually win. The one line people scan for, so it gets its own box. */
.win-line {
  margin: 10px 0 0;
  padding: 10px 13px;
  border-radius: var(--r-sm);
  background: #e9fff6;
  border: 1.5px solid var(--mint-ink);
  color: #06392a;
  font-size: 13px;
  font-weight: 650;
}
.rules b { color: var(--ink); }

/* --- profile editor ------------------------------------------------------- */

.profile-edit { width: 100%; max-width: 540px; display: grid; gap: 15px; }

.avatar-preview { display: grid; place-items: center; gap: 12px; }
.avatar-preview .avatar { width: 112px; height: 112px; }
/* Sixteen wide characters is a legal name and wider than the column, so it wraps rather
   than pushing the panel out. */
.avatar-preview .nm { font-size: 20px; font-weight: 800; max-width: 100%; overflow-wrap: anywhere; text-align: center; }

/* --- the profile screen, in two columns -----------------------------------
   Choosing a skin or a colour is worthless if you cannot see what it did, and the pickers
   sit far enough down the page that the preview used to scroll away exactly when it
   mattered. So the preview gets its own column on the left and sticks to the top of it.
   It is sticky rather than fixed so it still scrolls out of the way at the end of the
   page, where there is nothing left to preview. */
.profile-split { max-width: 900px; }
.profile-main { display: grid; gap: 15px; min-width: 0; }
.profile-side { min-width: 0; }

@media (min-width: 800px) {
  .profile-split {
    grid-template-columns: 236px minmax(0, 1fr);
    gap: 18px;
    align-items: start;
  }
  .profile-side {
    position: sticky;
    /* Clears the fixed app bar, which would otherwise sit on top of it. */
    top: calc(var(--appbar-h) + 18px);
  }
  .avatar-preview .avatar { width: 132px; height: 132px; }
}

/* On a phone there is one column and no room for a 112px portrait pinned to the top, so
   the same panel turns into a slim bar: avatar beside name instead of above it. */
@media (max-width: 799px) {
  .profile-side {
    position: sticky;
    top: calc(var(--appbar-h) + 8px);
    z-index: 3;
  }
  .profile-side .avatar-preview {
    grid-template-columns: auto 1fr;
    place-items: center start;
    gap: 14px;
    padding: 12px 16px;
  }
  .avatar-preview .avatar { width: 60px; height: 60px; }
  .avatar-preview .nm { font-size: 17px; }
}

.mood-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }

.tile {
  background: var(--cream);
  border: 2px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 6px 6px;
  cursor: pointer;
  display: grid;
  justify-items: center;
  gap: 5px;
  font-family: inherit;
  font-size: 10.5px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: .06em;
  font-weight: 700;
  transition: border-color .14s, transform .07s, background .14s;
}
.tile:hover { border-color: var(--cyan); }
.tile:active { transform: translateY(2px); }
.tile .avatar { width: 50px; height: 50px; }
.tile.on { border-color: var(--cyan-ink); background: #eaf8ff; color: var(--cyan-ink); }

/* 30+ swatches, so a dense grid rather than a row */
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(34px, 1fr)); gap: 8px; }
.swatch {
  aspect-ratio: 1;
  border-radius: 50%;
  border: 3px solid transparent;
  box-shadow: inset 0 0 0 1.5px rgba(26, 29, 40, .12);
  cursor: pointer;
  padding: 0;
  transition: transform .09s;
}
.swatch:hover { transform: scale(1.12); }
.swatch.on { border-color: var(--ink); }

/* --- settings ------------------------------------------------------------- */

.toggle {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  cursor: pointer;
  font-size: 14.5px;
  font-weight: 650;
  padding: 4px 0;
}
.toggle > span { flex: 1; display: grid; gap: 3px; }
.toggle em {
  font-style: normal;
  font-size: 12.5px;
  font-weight: 400;
  color: var(--dim);
  line-height: 1.4;
}

/* Checkbox drawn as a switch, flat like everything else. */
.toggle input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 46px;
  height: 27px;
  margin: 2px 0 0;
  padding: 0;
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
  background: var(--cream-2);
  cursor: pointer;
  position: relative;
  transition: background .16s, border-color .16s;
}
.toggle input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 19px; height: 19px;
  border-radius: 50%;
  background: #fff;
  border: 1.5px solid var(--line-2);
  transition: transform .16s;
}
.toggle input[type="checkbox"]:checked {
  background: var(--cyan);
  border-color: var(--cyan-ink);
}
.toggle input[type="checkbox"]:checked::after {
  transform: translateX(19px);
  border-color: var(--cyan-ink);
}
.toggle input[type="checkbox"]:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }

.rule { height: 1.5px; background: var(--line); margin: 14px 0; }

/* --- searching ------------------------------------------------------------ */

.searching { text-align: center; display: grid; gap: 18px; justify-items: center; }
.searching h2 { font-size: 22px; text-transform: none; letter-spacing: -.01em; }
.pulse-ring {
  width: 84px; height: 84px;
  border-radius: 50%;
  border: 5px solid var(--line-2);
  border-top-color: var(--cyan-ink);
  animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --- role reveal ---------------------------------------------------------- */

.role-card { width: 100%; max-width: 480px; text-align: center; padding: 30px; }
.role-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: .18em; color: var(--dim); font-weight: 750; }
.role-name { font-size: clamp(28px, 8vw, 44px); font-weight: 870; letter-spacing: -.02em; margin: 8px 0 4px; }
.role-name.imposter { color: var(--danger-ink); }
.role-name.guesser { color: var(--cyan-ink); }
.role-brief { color: var(--ink-2); font-size: 15px; margin: 14px 0 6px; }
.role-brief b { color: var(--ink); }
.role-rules { text-align: left; display: grid; gap: 8px; margin: 18px 0 22px; }
.role-rules div {
  background: var(--cream); border: 1.5px solid var(--line);
  border-radius: var(--r-sm); padding: 10px 14px; font-size: 13.5px; color: var(--ink-2);
}
.role-rules b { color: var(--ink); }
.role-countdown { font-family: var(--mono); font-size: 13px; color: var(--dim); }

/* --- lobby ---------------------------------------------------------------- */

.lobby { width: 100%; max-width: 470px; display: grid; gap: 14px; }

.code-card { text-align: center; }
.code-label { color: var(--lavender-ink); font-size: 10.5px; text-transform: uppercase; letter-spacing: .2em; font-weight: 750; }
.code {
  font-family: var(--mono);
  font-size: 48px;
  font-weight: 800;
  letter-spacing: .16em;
  text-indent: .16em;
  color: var(--cyan-ink);
  margin: 6px 0 12px;
}

.player-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.player-list li {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 13px;
  background: var(--cream);
  border: 1.5px solid var(--line);
  border-radius: 13px;
  font-size: 14px;
}
.player-list .avatar { width: 36px; height: 36px; }
.player-list .nm { font-weight: 700; }
.player-list .pid { color: var(--dim); font-family: var(--mono); font-size: 11px; }
.player-list .tag { margin-left: auto; font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: .1em; font-weight: 750; }
.player-list li.me { border-color: var(--cyan-ink); background: #f2fbff; }
.player-list li.off { opacity: .5; }
/* Seats whose identity the server is withholding until the round starts. */
.player-list li.anon { color: var(--dim); border-style: dashed; background: #fff; }
.player-list li.anon .dot {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--cream-2); color: var(--dim);
  display: grid; place-items: center; font-size: 15px; font-weight: 800; flex: none;
}
.player-list li.anon .nm { font-weight: 650; }

.slot {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 13px;
  border: 1.5px dashed var(--line-2);
  border-radius: 13px;
  color: var(--dim);
  font-size: 13px;
}
.slot .dot {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--cream-2);
  display: grid; place-items: center;
  font-size: 16px;
}

.slider { display: block; font-size: 11.5px; color: var(--dim); margin-bottom: 16px; text-transform: uppercase; letter-spacing: .1em; font-weight: 700; }
.slider output { color: var(--cyan-ink); float: right; font-family: var(--mono); font-weight: 800; }
.slider input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 7px; margin-top: 9px; padding: 0; border: 0;
  border-radius: 999px;
  background: var(--cream-2);
  box-shadow: inset 0 0 0 1.5px var(--line-2);
}
.slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--cyan); border: 3px solid #fff;
  box-shadow: 0 0 0 1.5px var(--line-2);
  cursor: pointer;
}
.slider input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--cyan); border: 3px solid #fff; cursor: pointer;
}

/* --- game ----------------------------------------------------------------
   The only pinned view: exactly one viewport tall, so the chat scrolls inside
   itself and the page never moves.
   ------------------------------------------------------------------------ */

.view-game {
  align-items: stretch;
  flex-direction: column;
  padding: 0;
  /* dvh, not svh. svh is the SMALLEST viewport height and never changes, which is right for a
     page you scroll and wrong for this one: when a phone keyboard opens, dvh shrinks and the
     column reflows so the composer stays above it. Paired with interactive-widget=resizes-content
     in the viewport meta - either one alone does nothing. */
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
  /* The anchor for #vote-screen, which covers the match and nothing else - the app bar and
     the settings sheet stay reachable above it. */
  position: relative;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1.5px solid var(--line);
  background: #fff;
  flex: none;
  min-height: var(--topbar-h);
}
.topbar .mark { height: 30px; width: auto; }

.phase-pill {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .14em;
  padding: 6px 12px; border-radius: 999px;
  color: var(--cyan-ink); background: #e4f6ff;
}
.phase-pill.vote { color: var(--warn-ink); background: #fff3d9; }

.role-pill {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .14em;
  padding: 6px 12px; border-radius: 999px;
}
.role-pill.imposter { color: var(--danger-ink); background: #ffe9ef; }
.role-pill.guesser { color: var(--cyan-ink); background: #e4f6ff; }

.timer { font-family: var(--mono); font-size: 21px; font-weight: 800; font-variant-numeric: tabular-nums; }
.timer.low { color: var(--danger-ink); }

.you-chip { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--dim); }
.you-chip .avatar { width: 28px; height: 28px; }
.you-chip b { color: var(--ink); }

.icon-btn {
  flex: none; width: 36px; height: 36px;
  display: grid; place-items: center; padding: 0;
  font-size: 15px; border-radius: 10px;
  background: #fff; color: var(--ink-2);
  border: 1.5px solid var(--line-2);
  cursor: pointer;
  transition: color .14s, border-color .14s, background .14s;
}
.icon-btn:hover { color: var(--cyan-ink); border-color: var(--cyan-ink); }
.icon-btn.off { color: #a09a90; }
.icon-btn.leave:hover { color: var(--danger-ink); border-color: var(--danger-ink); }

.game-grid { flex: 1; display: grid; grid-template-columns: 214px 1fr; min-height: 0; }
.game-grid.solo { grid-template-columns: 1fr; }

.roster { border-right: 1.5px solid var(--line); padding: 14px 12px; overflow-y: auto; background: #fff; }
.roster h3 { font-size: 10.5px; text-transform: uppercase; letter-spacing: .15em; color: var(--lavender-ink); margin-bottom: 11px; }
.roster ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.roster li {
  font-size: 13px; padding: 7px 9px; border-radius: 11px;
  background: var(--cream); border: 1.5px solid var(--line);
  display: flex; align-items: center; gap: 9px;
}
.roster li.me { border-color: var(--cyan-ink); background: #f2fbff; }
.roster li .avatar { width: 30px; height: 30px; }
.roster li .nm { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roster li.voted::after { content: "✓"; margin-left: auto; color: var(--warn-ink); font-weight: 800; }

.chat-col { display: flex; flex-direction: column; min-height: 0; }

.chat {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 16px;
  display: flex; flex-direction: column; gap: 10px;
  scrollbar-width: thin;
}
.chat::-webkit-scrollbar { width: 10px; }
.chat::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 9px; border: 3px solid var(--cream); }

.msg { display: flex; gap: 10px; align-items: flex-end; }
.msg .avatar { width: 32px; height: 32px; }
.msg .stack { max-width: min(74%, 520px); }
.msg .who { font-size: 11.5px; font-weight: 750; margin-bottom: 3px; padding-left: 2px; }
.msg .bubble {
  background: #fff;
  border: 1.5px solid var(--line);
  border-radius: 16px 16px 16px 5px;
  padding: 9px 14px;
  word-break: break-word;
}
.msg.mine { flex-direction: row-reverse; }
.msg.mine .stack { text-align: right; }
.msg.mine .who { padding-right: 2px; }
.msg.mine .bubble {
  background: #dff4ff;
  border-color: #a5dcf2;
  border-radius: 16px 16px 5px 16px;
  text-align: left;
}

.msg.sys { justify-content: center; color: var(--lavender-ink); font-size: 12px; font-weight: 650; padding: 4px 0; }
.msg.sys span { background: #f0f1ff; border: 1.5px solid #dcdffb; border-radius: 999px; padding: 5px 15px; }

.typing-row { height: 22px; padding: 0 16px; color: var(--dim); font-size: 12px; flex: none; }
.typing-row .dots span { display: inline-block; animation: blink 1.3s infinite; }
.typing-row .dots span:nth-child(2) { animation-delay: .18s; }
.typing-row .dots span:nth-child(3) { animation-delay: .36s; }
@keyframes blink { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }

.composer {
  display: flex; gap: 10px; padding: 12px 16px;
  border-top: 1.5px solid var(--line);
  background: #fff;
  flex: none;
}
.composer input { flex: 1; }

/* ==========================================================================
   THE VOTE SCREEN
   Voting used to be a strip under the chat: the biggest moment in the game looked
   like a form at the bottom of a page, and you picked between people by reading a
   26px roster. This takes the screen.
   ========================================================================== */
.vote-screen {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Bottom padding reserves the peek button's lane, so it never sits on the sheet. */
  padding: 16px 16px 56px;
  /* Not fully opaque: the chat showing through underneath says what this is on top of,
     which is why the peek button is discoverable at all. */
  background: rgba(26, 29, 40, .55);
  backdrop-filter: blur(3px);
  animation: vote-in .22s cubic-bezier(.2, .8, .25, 1) both;
}

/* Held out of the way while you re-read the chat, rather than unmounted - a display:none
   here would drop the grid and replay every entrance animation on the way back.
   The overlay stops catching clicks so the chat underneath is scrollable, and the button
   opts back in; it is outside .vote-sheet precisely so this fade cannot reach it. */
.vote-screen.peeking { background: transparent; backdrop-filter: none; pointer-events: none; }
.vote-screen.peeking .vote-sheet { opacity: 0; }

.vote-peek {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  pointer-events: auto;
  --fill: var(--card);
  --edge: var(--ink);
  --fg: var(--ink);
  font-size: 12.5px;
  padding: 7px 15px;
  z-index: 2;
}

@keyframes vote-in { from { opacity: 0 } to { opacity: 1 } }

.vote-sheet {
  width: min(680px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: var(--cream);
  border: 2.5px solid var(--ink);
  border-radius: var(--r-lg);
  box-shadow: 0 18px 44px rgba(26, 29, 40, .3);
  padding: 18px;
  animation: vote-rise .26s cubic-bezier(.2, .8, .25, 1) both;
}
@keyframes vote-rise { from { transform: translateY(14px) scale(.985) } to { transform: none } }

.vote-head { display: flex; align-items: center; gap: 12px; }
.vote-title {
  font-family: var(--display);
  font-size: clamp(19px, 4.2vw, 27px);
  font-weight: 400;
  line-height: 1.1;
  margin: 0;
  flex: 1;
  min-width: 0;
}
/* The clock is the loudest thing here on purpose - it is the only reason to hurry. */
.vote-clock {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 22px;
  font-weight: 800;
  min-width: 2.4em;
  text-align: center;
  padding: 5px 10px;
  border-radius: var(--r-sm);
  background: var(--card);
  border: 2px solid var(--line-2);
  color: var(--ink);
}
.vote-clock.soon { border-color: var(--danger); color: var(--danger-ink); background: #fff1f5; }
.vote-clock.now { border-color: var(--danger-ink); color: #fff; background: var(--danger-ink); animation: vote-pulse .9s ease-in-out infinite; }
@keyframes vote-pulse { 50% { transform: scale(1.06) } }

.vote-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(122px, 1fr));
  gap: 10px;
  overflow-y: auto;
  min-height: 0;
  /* ROOM FOR THE TICK, which overhangs its card by 9px (7px offset + a 2px border).
     Setting only overflow-y still makes overflow-x compute to auto rather than visible - the
     spec does not let one axis scroll while the other overflows - so the badge on any card in
     the last column was sliced in half. This box has to scroll, so overflow-clip-margin is not
     available here the way it is for the demo log; padding is. */
  padding: 10px;
  margin: -10px;
}

/* One person, big enough to actually look at. */
.vote-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 13px 8px 11px;
  background: var(--card);
  border: 2.5px solid var(--line-2);
  border-radius: var(--r);
  cursor: pointer;
  font: inherit;
  color: var(--ink);
  text-align: center;
  transition: border-color .14s, transform .08s, box-shadow .14s;
}
.vote-card:hover:not(:disabled) { border-color: var(--ink); transform: translateY(-2px); }
.vote-card:active:not(:disabled) { transform: translateY(0) scale(.98); }
.vote-card:focus-visible { outline: 3px solid var(--cyan-ink); outline-offset: 2px; }
.vote-card .av { width: 54px; height: 54px; border-radius: 50%; background: var(--cream-2); }
.vote-card .nm {
  font-size: 13px; font-weight: 800; line-height: 1.2;
  overflow-wrap: anywhere;
}
.vote-card .sub { font-size: 10.5px; color: var(--dim); font-weight: 700; }

/* Your pick. Deliberately unmistakable - a vote you are not sure you cast is worse
   than no feedback at all. */
.vote-card.picked {
  border-color: var(--danger-ink);
  background: #fff1f5;
  box-shadow: 0 0 0 3px var(--danger);
}
.vote-card.picked .sub { color: var(--danger-ink); }

/* Locked in, WITHOUT saying who they picked. The counts stay secret until the reveal;
   only the fact that somebody has decided is public. */
.vote-card.done::after {
  content: '✓';
  position: absolute;
  top: -7px; right: -7px;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 800;
  color: #05301f;
  background: var(--mint);
  border: 2px solid var(--ink);
  border-radius: 50%;
}
.vote-card:disabled { cursor: default; opacity: .55; }
.vote-card.is-me { cursor: default; opacity: .8; border-style: dashed; }

.vote-slot { display: grid; gap: 12px; overflow-y: auto; min-height: 0; }

.vote-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.vote-tally { flex: 1; min-width: 0; font-size: 12.5px; font-weight: 700; color: var(--dim); }

@media (max-width: 640px) {
  .vote-screen { padding: 10px; }
  .vote-sheet { padding: 13px; gap: 11px; border-radius: var(--r); }
  .vote-grid { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; }
  .vote-card { padding: 10px 5px 9px; }
  .vote-card .av { width: 42px; height: 42px; }
  .vote-card .nm { font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .vote-screen, .vote-sheet { animation: none; }
  .vote-clock.now { animation: none; }
}

.action-bar {
  border-top: 1.5px solid var(--warn);
  background: #fffaf0;
  padding: 13px 16px;
  flex: none;
  max-height: 34vh;
  overflow-y: auto;
}
.action-bar-title { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .15em; color: var(--warn-ink); margin-bottom: 10px; }
.vote-options { display: flex; gap: 9px; flex-wrap: wrap; }
.vote-chip {
  display: flex; align-items: center; gap: 8px;
  background: #fff; border: 2px solid var(--line-2);
  border-radius: 999px; padding: 6px 15px 6px 6px;
  cursor: pointer; font-family: inherit; font-size: 13.5px; font-weight: 700;
  color: var(--ink);
  transition: border-color .14s, transform .07s, background .14s;
}
.vote-chip .avatar { width: 30px; height: 30px; }
.vote-chip:hover { border-color: var(--warn); }
.vote-chip:active { transform: translateY(2px); }
.vote-chip.picked { border-color: var(--warn-ink); background: #fff3d6; }

.verdict-row { display: flex; gap: 12px; }
.verdict-row .btn { flex: 1; }
.waiting-note { color: var(--warn-ink); font-size: 13px; font-weight: 700; }

/* --- result --------------------------------------------------------------- */

.result { max-width: 560px; width: 100%; text-align: center; padding: 28px; }

.verdict-big { font-size: clamp(24px, 6.4vw, 36px); font-weight: 870; letter-spacing: -.02em; }
.verdict-big.win { color: var(--mint-ink); }
.verdict-big.lose { color: var(--danger-ink); }

.reveal { color: var(--ink-2); font-size: 14.5px; margin: 12px 0 20px; }
.reveal b { color: var(--ink); }

.reveal-card {
  display: inline-flex; align-items: center; gap: 13px;
  background: var(--cream); border: 1.5px solid var(--line);
  border-radius: var(--r); padding: 12px 20px 12px 12px;
  margin: 0 0 20px;
}
.reveal-card .avatar { width: 54px; height: 54px; }
.reveal-card .nm { font-size: 18px; font-weight: 820; text-align: left; }
.reveal-card .role { font-size: 10.5px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; text-align: left; }
.reveal-card .role.ai { color: var(--danger-ink); }
.reveal-card .role.human { color: var(--mint-ink); }

.tally { display: grid; gap: 9px; margin-bottom: 22px; text-align: left; }
.tally-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.tally-row .name { width: 130px; flex: none; color: var(--ink-2); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tally-row .bar { height: 11px; border-radius: 6px; min-width: 6px; background: var(--cyan); }
.tally-row.was-ai .name { color: var(--danger-ink); }
.tally-row.was-ai .bar { background: var(--danger); }

.result-actions { display: grid; gap: 10px; justify-items: center; }

.transcript {
  margin-top: 20px; text-align: left; max-height: 240px; overflow-y: auto;
  border-top: 1.5px solid var(--line); padding-top: 14px;
  font-size: 12.5px; display: grid; gap: 5px;
}
.transcript .who { font-weight: 750; }
.transcript .ai-line { background: #fff0f4; border-left: 3px solid var(--danger); border-radius: 5px; padding: 3px 8px; }

/* --- module games: roles, targets, panels ---------------------------------
   Shared by every game that runs on a game module, so a new game inherits the
   whole look without adding CSS.
   ------------------------------------------------------------------------- */

.phase-pill.night { color: #2e3566; background: #e7e9ff; }

.role-tag {
  font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .1em;
  padding: 3px 7px; border-radius: 999px;
  background: var(--cream-2); color: var(--dim);
  flex: none;
}
/* Pushed to the far end of a roster row - unless a score is there, which goes last. */
.roster li .role-tag { margin-left: auto; }
.roster li .role-tag:not(:last-child) { margin-left: auto; margin-right: 0; }

/* A player's running score, beside the player it belongs to. */
.score-pip {
  margin-left: auto;
  flex: none;
  min-width: 22px;
  text-align: center;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 2px 7px;
  border-radius: 999px;
  background: #e4f6ff;
  color: var(--cyan-ink);
}
.roster li.me .score-pip { background: var(--cyan); color: #06222e; }
.roster li.dead .score-pip, .roster li.off .score-pip { background: var(--cream-2); color: var(--dim); }
.role-mafia { color: var(--danger-ink); background: #ffe9ef; }
.role-doctor { color: var(--mint-ink); background: #e3fff2; }
.role-detective { color: var(--cyan-ink); background: #e4f6ff; }
.role-citizen { color: var(--dim); background: var(--cream-2); }

.role-pill.role-mafia { color: var(--danger-ink); background: #ffe9ef; }
.role-pill.role-doctor { color: var(--mint-ink); background: #e3fff2; }
.role-pill.role-detective { color: var(--cyan-ink); background: #e4f6ff; }
.role-pill.role-citizen { color: var(--dim); background: var(--cream-2); }

.role-name.role-mafia { color: var(--danger-ink); }
.role-name.role-doctor { color: var(--mint-ink); }
.role-name.role-detective { color: var(--cyan-ink); }
.role-name.role-citizen { color: var(--ink); }

.role-partners { margin-top: 16px; padding-top: 14px; border-top: 1.5px solid var(--line); }
.partners-title { font-size: 10.5px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; color: var(--dim); margin-bottom: 6px; }
.role-partners .partner { font-weight: 800; margin-right: 12px; }

.roster li.dead { opacity: .45; }
.roster li.dead .nm { text-decoration: line-through; }
/* Knocked out, and it should read that way at a glance: faded, struck through, and no
   longer wearing a colour. The colour is what makes a player look present. */
.roster li.dead .avatar { filter: grayscale(1); }

/* A place in the leaderboard. Rendered only where the order is a real ranking, so the
   number always means what it looks like it means. */
.rank {
  flex: none;
  width: 16px;
  text-align: center;
  font-size: 11px;
  font-weight: 800;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.roster li:first-child .rank { color: var(--warn-ink); }
.roster li.me .rank { color: var(--cyan-ink); }

.game-panel { display: grid; gap: 10px; }
.panel-title { font-size: 11px; text-transform: uppercase; letter-spacing: .15em; font-weight: 800; color: var(--lavender-ink); }
.panel-note { margin: 0; font-size: 12.5px; color: var(--dim); }

.target-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.target {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 13px 7px 7px;
  font-family: inherit; font-size: 13px; font-weight: 650;
  background: #fff; color: var(--ink);
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
  cursor: pointer;
  transition: border-color .14s, background .14s, transform .07s;
}
.target:hover:not(:disabled) { border-color: var(--cyan-ink); }
.target:active:not(:disabled) { transform: translateY(2px); }
.target:disabled { opacity: .4; cursor: not-allowed; }
.target.picked { border-color: var(--danger-ink); background: #ffe9ef; }
.target .role-tag { margin-left: 2px; }
.count-pip {
  min-width: 20px; text-align: center;
  font-family: var(--mono); font-size: 11.5px; font-weight: 800;
  padding: 2px 6px; border-radius: 999px;
  background: var(--warn); color: #3a2a00;
}

.findings { border-top: 1.5px solid var(--line); padding-top: 10px; display: grid; gap: 5px; }
.findings-title { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; color: var(--dim); }
.finding { display: flex; gap: 10px; font-size: 12.5px; }
.finding .nm { font-weight: 750; }
.finding.hit { color: var(--danger-ink); }
.finding.clear { color: var(--mint-ink); }

/* A line from a private channel. Tinted so it can never be mistaken for public. */
.msg.private .bubble { background: #fff0f4; border-color: var(--danger); }
.composer.private input { border-color: var(--danger); background: #fff8fa; }
.composer.muted input { background: var(--cream-2); }

.reveal-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 9px; margin: 4px 0 16px; }
.reveal-seat {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 11px; border-radius: 14px;
  background: var(--cream); border: 1.5px solid var(--line);
  text-align: left;
}
.reveal-seat.dead { opacity: .55; }
.reveal-seat .nm { font-weight: 750; font-size: 13.5px; }
.reveal-seat .role { font-size: 10px; text-transform: uppercase; letter-spacing: .12em; font-weight: 800; }
.reveal-seat.role-mafia { border-color: var(--danger); background: #fff0f4; }
.reveal-seat.role-mafia .role { color: var(--danger-ink); }
.reveal-seat.role-doctor .role { color: var(--mint-ink); }
.reveal-seat.role-detective .role { color: var(--cyan-ink); }
.reveal-seat.role-citizen .role { color: var(--dim); }

.timeline summary { cursor: pointer; color: var(--lavender-ink); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; font-weight: 800; }
.timeline-list { margin-top: 10px; display: grid; gap: 4px; font-size: 12.5px; color: var(--ink-2); text-align: left; }
.transcript .private-line { background: #fff0f4; border-left: 3px solid var(--danger); border-radius: 5px; padding: 3px 8px; }

/* Fake Insider's last move: four words, one of them real. */
.choice-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 9px; }
.btn.choice { text-transform: none; letter-spacing: 0; font-size: 16px; }

.guess-line {
  margin: -6px 0 14px;
  font-size: 13.5px;
  font-weight: 750;
  text-align: center;
}
.guess-line.right { color: var(--mint-ink); }
.guess-line.wrong { color: var(--dim); }

/* --- answers, statements, scoreboards -------------------------------------
   The submission games (Fake Insider, Bluff Masters, Unique Mind) and Alibi all
   draw from these, so they look like one site rather than four.
   ------------------------------------------------------------------------- */

.answer-box { display: grid; gap: 8px; }
.answer-form { display: flex; gap: 8px; }
.answer-form input { flex: 1; }
.answer-locked {
  padding: 12px 16px; border-radius: var(--r-sm);
  background: #eaf8ff; border: 1.5px solid var(--cyan-ink);
  color: var(--cyan-ink); font-weight: 750;
}

/* The prompt word, or the word that was being hidden. Deliberately huge. */
.big-word {
  font-size: clamp(30px, 7vw, 54px);
  font-weight: 880;
  letter-spacing: -.02em;
  line-height: 1.05;
  text-align: center;
  margin: 6px 0 14px;
  color: var(--ink);
}

.turn-order { display: flex; flex-wrap: wrap; gap: 6px; }
.turn-chip {
  font-size: 12px; font-weight: 700;
  padding: 5px 10px; border-radius: 999px;
  background: var(--cream-2); color: var(--dim);
}
.turn-chip.now { background: var(--cyan); color: #06222e; }
.turn-chip.done { opacity: .5; text-decoration: line-through; }

.hint-list, .answer-list { display: grid; gap: 5px; }
.hint-row, .answer-row {
  display: flex; align-items: center; gap: 10px;
  font-size: 13.5px;
  padding: 6px 10px; border-radius: 10px;
  background: var(--cream); border: 1.5px solid var(--line);
}
.hint-row .nm, .answer-row .nm { font-weight: 750; min-width: 84px; }
.hint-row.insider { border-color: var(--danger); background: #fff0f4; }
.answer-word { font-weight: 750; flex: 1; }
.answer-row .role-tag { margin-left: auto; }
.answer-row.good { border-color: var(--mint); background: #e9fff6; }
.answer-row.dup { border-color: var(--warn); background: #fff8e8; }
.answer-row.bad { border-color: var(--danger); background: #fff0f4; }

.fact-card {
  padding: 14px 16px; border-radius: var(--r-sm);
  background: #e9fff6; border: 1.5px solid var(--mint-ink);
  color: #06392a; font-size: 14.5px; font-weight: 650;
}

.card-list { display: grid; gap: 8px; }
.statement {
  display: flex; align-items: center; gap: 10px; text-align: left;
  width: 100%; padding: 12px 14px;
  font-family: inherit; font-size: 14px; line-height: 1.4;
  background: #fff; color: var(--ink);
  border: 1.5px solid var(--line-2); border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color .14s, background .14s, transform .07s;
}
.statement:hover:not(:disabled) { border-color: var(--cyan-ink); }
.statement:active:not(:disabled) { transform: translateY(2px); }
.statement:disabled { cursor: default; }
.statement.flat { cursor: default; }
.statement.picked { border-color: var(--cyan-ink); background: #eaf8ff; }
.statement.mine { border-style: dashed; opacity: .8; }
.statement.real { border-color: var(--mint-ink); background: #e9fff6; }
.statement-text { flex: 1; }

.scoreboard { border-top: 1.5px solid var(--line); padding-top: 10px; display: grid; gap: 4px; }
.score-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.score-row .nm { font-weight: 700; }
.score-row .score { margin-left: auto; font-family: var(--mono); font-weight: 800; }
.score-row.me { font-weight: 800; }
.score-row.out { opacity: .45; text-decoration: line-through; }

.checklist { display: grid; gap: 4px; font-size: 13px; color: var(--ink-2); }
.checklist div::before { content: "· "; color: var(--cyan-ink); font-weight: 800; }

.story-box {
  display: grid; gap: 4px; text-align: left;
  padding: 12px 14px; margin: 4px 0 14px;
  font-size: 13px; color: var(--ink-2);
  background: var(--cream); border: 1.5px solid var(--line); border-radius: var(--r-sm);
}

/* Badges over the cover art. */
.cover-solo {
  position: absolute;
  left: 8px;
  font-size: 9.5px;
  font-weight: 850;
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: 4px 8px;
  border-radius: 999px;
}
.cover-solo {
  top: 8px;
  background: rgba(255, 255, 255, .92);
  color: var(--ink);
}

/* --- the guest pitch and the idea box ------------------------------------ */

.pitch {
  display: flex;
  align-items: center;
  gap: 18px;
  text-align: left;
  margin-top: 18px;
}
.pitch h3 { font-size: 17px; margin-bottom: 5px; }
.pitch p { font-size: 13.5px; color: var(--ink-2); margin: 0 0 12px; }
.pitch-art { display: flex; flex: none; }
/* Overlapped, so four avatars read as a set rather than a row of buttons. */
.pitch-art .avatar { margin-right: -12px; color: var(--cyan); }
.pitch-art .avatar:last-child { margin-right: 0; }

.idea-box { margin-top: 16px; text-align: left; }
.idea-box h2 { color: var(--lavender-ink); }

textarea {
  font-family: inherit;
  font-size: 14px;
  line-height: 1.5;
  padding: 12px 14px;
  border: 1.5px solid var(--line-2);
  border-radius: var(--r-sm);
  background: #fff;
  color: var(--ink);
  outline: none;
  resize: vertical;
  width: 100%;
}
textarea:focus { border-color: var(--cyan-ink); }

/* The level, on the profile chip in the app bar. */
.level-pip {
  flex: none;
  min-width: 22px;
  text-align: center;
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 850;
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--cyan);
  color: #06222e;
}

/* --- friends -------------------------------------------------------------
   A note that slides out of the right edge, with its own tab. One column: the
   list, and the conversation on top of it. A drawer this narrow has no room for
   a sidebar - and it means nobody has to be told to "pick a friend on the left".
   ------------------------------------------------------------------------ */

/* The tab, attached to the left edge of the note and OUTSIDE it, so it travels with the
   paper - pull the tab, the sheet comes with it. It sits in the app bar's place because the
   app bar is hidden inside a match, which is exactly when you want to invite somebody. */
.friends-tab {
  position: absolute;
  top: 50%;
  left: -40px;
  transform: translateY(-50%);
  width: 40px;
  height: 54px;
  display: grid;
  place-items: center;
  padding: 0;
  font-size: 19px;
  cursor: pointer;
  color: var(--ink-2);
  /* Same paper as the sheet, and no right border, so the two read as one piece. */
  background: #fffdf8;
  border: 1.5px solid var(--line-2);
  border-right: 0;
  border-radius: 14px 0 0 14px;
  box-shadow: -4px 0 14px rgba(26, 29, 40, .12);
  transition: color .14s, border-color .14s;
}
.friends-tab:hover { color: var(--cyan-ink); border-color: var(--cyan-ink); }
.friends-tab .unread-pip { position: absolute; top: -5px; left: -7px; }

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 41;
  background: rgba(26, 29, 40, .22);
}

.friends-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 42;
  width: min(360px, 100vw);
  display: flex;
  flex-direction: column;
  /* Paper: a warm white, a ruled edge, and lines behind the list. Flat - no gloss. */
  background:
    linear-gradient(var(--line) 0 0) no-repeat 26px 0 / 1.5px 100%,
    repeating-linear-gradient(#fffdf8 0 27px, #f6f1e6 27px 28px);
  border-left: 1.5px solid var(--line-2);
  box-shadow: -10px 0 30px rgba(26, 29, 40, .16);
  transform: translateX(100%);
  transition: transform .26s cubic-bezier(.2, .8, .2, 1);
  /* NOT overflow:hidden - that would clip the tab hanging off the left edge. The panes
     handle their own scrolling instead. */
}
.friends-drawer.open { transform: translateX(0); }

/* Closed, the sheet is off-screen but still in the layout, because the tab lives inside it.
   Hiding the panes keeps their inputs out of the tab order while it is shut. */
.friends-drawer:not(.open) .drawer-pane { visibility: hidden; }

.drawer-pane { display: flex; flex-direction: column; min-height: 0; height: 100%; padding: 16px 16px 14px 40px; gap: 11px; overflow: hidden; }

.drawer-head { display: flex; align-items: center; gap: 9px; }
.drawer-head h2 {
  flex: 1;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .15em;
  color: var(--lavender-ink);
  margin: 0;
}
.drawer-head .icon-btn { flex: none; width: 30px; height: 30px; font-size: 14px; }
.drawer-scroll { flex: 1; overflow-y: auto; display: grid; gap: 11px; align-content: start; padding-right: 2px; }

.friends-drawer .join-row { display: flex; gap: 8px; }
.friends-drawer .join-row input { flex: 1; padding: 10px 13px; font-size: 14px; }

.people { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.person, .request {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .82);
  border: 1.5px solid var(--line);
  text-align: left;
}
.person { cursor: pointer; transition: border-color .14s, background .14s; }
.person:hover { border-color: var(--cyan-ink); background: #fff; }
.person.on { border-color: var(--cyan-ink); background: #f2fbff; }
.person-text { min-width: 0; flex: 1; }
.person .nm, .request .nm { font-weight: 750; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.request { flex-wrap: wrap; }
.request .btn { flex: none; }

/* Presence. The colour carries the same meaning as the words, for a glance. */
.status { font-size: 11px; font-weight: 700; }
.on-playing { color: var(--danger-ink); }
.on-waiting { color: var(--warn-ink); }
.on-online { color: var(--mint-ink); }
.on-offline { color: var(--dim); font-weight: 600; }

.level-pip.sm { font-size: 9.5px; padding: 1px 5px; min-width: 0; }

.unread-pip {
  flex: none;
  min-width: 18px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--danger-ink);
  color: #fff;
  font-size: 10.5px;
  font-weight: 850;
  text-align: center;
}

.chat-head { display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0; flex-wrap: wrap; }
.chat-head .person-text { flex: 1; min-width: 90px; }
.chat-head .btn { flex: none; }

.dm-log {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-right: 2px;
}
.dm {
  max-width: 84%;
  padding: 9px 13px;
  border-radius: 16px 16px 16px 5px;
  background: #fff;
  border: 1.5px solid var(--line);
  font-size: 14px;
  line-height: 1.45;
  align-self: flex-start;
  word-break: break-word;
}
.dm.mine {
  align-self: flex-end;
  border-radius: 16px 16px 5px 16px;
  background: var(--cyan);
  border-color: #3e93b0;
  color: #06222e;
}
/* An invite is a message with a button in it. */
.dm.invite { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.friends-drawer .composer { display: flex; gap: 8px; }
.friends-drawer .composer input { flex: 1; padding: 11px 14px; font-size: 14px; }

/* --- arena mode ------------------------------------------------------------
   A game that is only its own board. The shared game view is built for talking - a roster
   down the left, a chat filling the middle, and the game's panel as a bar underneath - which
   is right for Mafia and wrong for a typing race, where the chat is SHUT for the whole minute
   and the roster repeats what the scoreboard already says. Both are dead weight taking two
   thirds of the screen.
   A renderer sets `arena: true` and gets the screen instead. */
.view-game.arena .game-grid { display: none; }
.view-game.arena #game-panel-slot {
  flex: 1;
  max-height: none;
  /* Defensive. A flex item is not squeezed below the min-content width of its contents, and
     this box holds a nowrap row of words plus an input with a twenty-character intrinsic width.
     Measured as fine without it, but one longer word in the bank would change that quietly. */
  min-width: 0;
  display: grid;
  align-content: center; /* the board sits in the middle rather than hugging the top edge */
  /* Not the warning colours the action bar wears when it is a strip at the bottom: as a whole
     screen that reads as an error page. */
  background: var(--cream);
  border-top: 0;
  padding: 20px 18px;
}
.view-game.arena #game-panel-slot > * { max-width: 760px; margin: 0 auto; width: 100%; min-width: 0; }

/* Having taken the screen, use it. The same board sized for a strip at the bottom of the view
   looks lost in the middle of an empty one, and here the words ARE the game. */
.view-game.arena .typing-panel { gap: 20px; }
.view-game.arena .race-count { font-size: 32px; }
.view-game.arena .race-track { height: 13px; }
.view-game.arena .word-strip { padding: 30px 26px; }
.view-game.arena .word-strip-inner { font-size: 30px; gap: 19px; }
.view-game.arena .race-input { font-size: 30px; padding: 0 26px; }

/* The letters, falling. Kept faint on purpose: the pattern is busy and colourful, and this is a
   screen where legibility beats decoration every time. Behind the play area only - the topbar
   has its own surface.
   HOW THE LOOP IS SEAMLESS. The tile is 910px tall and repeats vertically, and it is translated
   down by exactly 910px - one whole tile - so the end state is pixel-identical to the start and
   there is no jump to see. The tile's own top and bottom fade to transparent, which is what hides
   the joins: a letter that would have been sliced at the boundary has already faded out by the
   time it gets there, and letters appear to fade in above and out below, which is what falling
   looks like rather than a sheet sliding past.
   TRANSLATED, not background-position: transform is composited on the GPU. Repainting a
   background twice a second underneath somebody typing at 120 WPM is exactly where you do not
   want the main thread. */
.view-game.arena #game-panel-slot {
  position: relative;
  /* Hidden sideways so the repeating tile cannot widen the page; auto vertically so a short
     window - a phone on its side - can still reach the input rather than having it clipped
     somewhere unreachable. An absolutely positioned box above the top edge adds no scrollable
     overflow, so the tile sitting at top: -910px does not produce a scrollbar. */
  overflow-x: hidden;
  overflow-y: auto;
}
.view-game.arena #game-panel-slot::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* One tile above the box, so there is something to fall INTO view from. */
  top: -910px;
  bottom: 0;
  background: url("/assets/typing-bg.webp") 0 0 / 1280px 910px repeat;
  opacity: .16;
  pointer-events: none;
  animation: letters-fall 44s linear infinite;
  will-change: transform;
}
/* 44 seconds for a full tile. Slow on purpose - somebody is reading text on top of this, and
   anything you can actually watch move is something competing for their eyes. */
@keyframes letters-fall {
  from { transform: translateY(0); }
  to { transform: translateY(910px); }
}
.view-game.arena #game-panel-slot > * { position: relative; } /* above the pattern */

@media (max-width: 640px) {
  .view-game.arena #game-panel-slot { padding: 14px 12px; }
  .view-game.arena .word-strip { padding: 20px 16px; }
  .view-game.arena .word-strip-inner { font-size: 21px; gap: 13px; }
  .view-game.arena .race-input { font-size: 21px; padding: 0 16px; }
  .view-game.arena .race-count { font-size: 26px; }
}

/* --- speed typing ----------------------------------------------------------
   The one panel that has to behave like an instrument. Everything here is sized so the eye
   never has to hunt: the word you are on is the biggest thing on screen, and the opponent's
   bar is the only thing that moves. */

.typing-panel {
  gap: 12px;
  /* minmax(0, 1fr), not the default `auto`: an auto column will not go narrower than its widest
     item's min-content, and min-width on the children does not help because it is the COLUMN
     that has to be allowed to shrink. */
  grid-template-columns: minmax(0, 1fr);
}

.race-board { display: grid; gap: 9px; }
.race-lane { display: grid; gap: 4px; }
.race-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.race-name { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; color: var(--dim); }
.race-lane.mine .race-name { color: var(--cyan-ink); }
.race-count {
  font-size: 22px;
  font-weight: 880;
  font-variant-numeric: tabular-nums; /* the number must not jiggle as it climbs */
  line-height: 1;
}
.race-track {
  height: 10px;
  border-radius: 999px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  overflow: hidden;
}
.race-fill {
  height: 100%;
  background: var(--lavender);
  /* Your own lane is cyan and theirs is lavender, the same pairing the rest of the site uses for
     "you" against "them". Set in CSS rather than inline, so the built DOM can be reused across
     state pushes without restyling it every time. */
  /* Slower than the score, on purpose: the number is the fact and the bar is the feeling. */
  transition: width .3s cubic-bezier(.2, .8, .25, 1);
}
.race-lane.mine .race-fill { background: var(--cyan); }
.race-sub { font-size: 11.5px; color: var(--dim); font-weight: 650; }

/* The words, and the field. They are the SAME box: the input is stretched over the words and
   made transparent, so what you see is your own typing colouring in the word under the caret.
   A separate box below would put the letters you are producing somewhere your eyes are not,
   which costs real words per minute.
   Monospace so a word occupies predictable space and the line does not reflow as you go. */
.word-strip {
  position: relative;
  overflow: hidden;
  padding: 20px 18px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  /* A grid item defaults to min-width: auto and so refuses to be narrower than its content -
     and the content is a nowrap row of words. overflow: hidden does not help with that: it
     clips what the box has already made room for. */
  min-width: 0;
  transition: border-color .15s ease;
}
/* The box takes the focus ring, since the input itself is invisible - without this there is no
   way to tell whether your keystrokes are going anywhere. */
.word-strip:focus-within { border-color: var(--cyan-ink); }

.word-strip-inner {
  display: flex;
  align-items: baseline;
  gap: 14px;
  font-family: var(--mono);
  font-size: 21px;
  white-space: nowrap;
  /* The words run past the right edge on purpose - reading ahead is how typing works - but a
     hard clip slices the last one through the middle of a letter and reads as a rendering fault.
     Fading says "there is more" instead.
     On the INNER row, not the box: a mask applies to an element's background and border too, so
     masking the box made its whole right edge dissolve. */
  -webkit-mask-image: linear-gradient(to right, #000 76%, transparent 98%);
  mask-image: linear-gradient(to right, #000 76%, transparent 98%);
}

.strip-word { color: var(--dim); }
/* The word you are on. Grey to start with, because its letters GO green as you get them right -
   if the untyped letters were already ink there would be nothing to see happen. The bar
   underneath is what makes the eye lock on without a background that would fight the red of a
   wrong letter. */
.strip-word.now {
  /* --dim, not the fainter --line-2 that was here first: this is the word you are about to type
     and therefore the most important text on the screen. --line-2 measured 1.58:1 against the
     card, which is decoration rather than text. Being the CURRENT word is carried by the weight
     and the bar underneath instead of by being pale. */
  color: var(--dim);
  font-weight: 700;
  box-shadow: inset 0 -3px 0 var(--cyan);
  border-radius: 4px;
  padding: 0 3px 3px;
  margin: 0 -3px;
}
/* Words behind you, coloured by what actually happened. Not faded to grey: a red word two back
   is the signal that you are going too fast, and it is the only feedback a typing test can give
   you mid-race without stopping you. */
.strip-word.done.hit { color: var(--mint-ink); opacity: .75; }
.strip-word.done.miss { color: var(--danger-ink); opacity: .8; text-decoration: line-through; }

/* One span per character of the word being typed. One colour language across the whole strip:
   GREEN is right, RED is wrong, GREY is not yet - the same three states a finished word shows,
   so nothing has to be learned twice. */
.ch { position: relative; transition: color .06s linear; }
.ch.ok { color: var(--mint-ink); }
.ch.bad { color: var(--danger-ink); background: #ffe0e9; border-radius: 3px; }
.ch.over { color: var(--danger-ink); background: #ffe0e9; border-radius: 3px; text-decoration: line-through; }

/* The caret. Drawn on the character you are about to type rather than as a separate element, so
   it cannot drift out of step with the text. */
.ch.at::before,
.ch.end::before {
  content: "";
  position: absolute;
  left: -3px;
  top: -2px;
  bottom: -2px;
  width: 2.5px;
  border-radius: 2px;
  background: var(--cyan-ink);
  animation: caret 1s steps(1) infinite;
}
.ch.end::before { left: 1px; }
@keyframes caret { 0%, 45% { opacity: 1 } 50%, 100% { opacity: 0 } }

/* Invisible, and deliberately not display:none or visibility:hidden - it has to be focusable to
   raise a phone keyboard, and it has to receive the keystrokes. Stretched over the whole box so
   a tap anywhere lands in it. */
.race-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
  opacity: 0;
  border: 0;
  background: transparent;
  font-family: var(--mono);
  font-size: 21px;
  padding: 0 18px;
  /* Safari zooms the page when a field smaller than 16px takes focus; 21px is safe. */
  caret-color: transparent;
  cursor: text;
}
.race-input:focus { outline: none; }

/* The countdown sits OVER the words, in the middle, so the number is where the eyes already are
   and the first words are readable behind it. Same layout as the race - nothing moves when the
   clock starts, which is the worst possible moment for the screen to shift. */
.race-stage { position: relative; }
.race-countdown {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 2px;
  /* Enough to read the number against, not enough to hide the words behind it. */
  background: rgba(251, 248, 241, .82);
  border-radius: var(--r);
}
.race-count-big {
  font-family: var(--display);
  font-size: clamp(56px, 12vw, 104px);
  line-height: 1;
  font-weight: 400;
  color: var(--cyan-ink);
  font-variant-numeric: tabular-nums;
}
/* A pop per number. Restarted by hand each second - re-adding a class does not replay an
   animation unless the element is reflowed in between. */
.race-count-big.pulse { animation: count-pop .32s cubic-bezier(.2, .8, .25, 1); }
@keyframes count-pop {
  from { transform: scale(.7); opacity: .4; }
  to { transform: none; opacity: 1; }
}
.race-count-note { font-size: 12.5px; color: var(--dim); font-weight: 650; text-align: center; }
/* Extra time. Loud, because it has to explain in one glance why the race did not just end. */
.race-extra {
  font-family: var(--display);
  font-size: 15px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--danger-ink);
  background: #ffe0e9;
  border-radius: 999px;
  padding: 5px 14px;
  margin-bottom: 4px;
}

.race-ready { margin: 0; font-size: 14.5px; color: var(--ink-2); font-weight: 650; }
.race-hint { margin: 0; font-size: 11.5px; color: var(--dim); font-weight: 650; text-align: center; }

/* The final board. */
.race-final { display: grid; gap: 7px; }
.race-final-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: baseline;
  gap: 12px;
  padding: 10px 13px;
  border-radius: var(--r-sm);
  background: var(--cream-2);
  border: 1.5px solid var(--line);
}
.race-final-row.won { border-color: var(--mint-ink); background: #e3fff2; }
.race-final-row .nm { font-weight: 750; }
.race-final-words { font-size: 20px; font-weight: 880; font-variant-numeric: tabular-nums; }
.race-final-sub { font-size: 11.5px; color: var(--dim); font-weight: 650; }

@media (max-width: 640px) {
  .word-strip-inner { font-size: 16px; gap: 9px; }
  .race-input { font-size: 17px; }
}

/* --- the server is full ----------------------------------------------------
   Deliberately calm. Somebody arriving from a stream has no idea whether the site is broken or
   busy, and red panic styling answers that question the wrong way. */
.busy-retry {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 16px 0 4px;
  padding: 11px 14px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums; /* so the countdown does not jitter as digits change */
}
.busy-dots { display: inline-flex; gap: 4px; flex: none; color: var(--cyan-ink); }
.busy-dots span {
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; opacity: .4;
  animation: demo-blink 1.1s infinite;
}
.busy-dots span:nth-child(2) { animation-delay: .18s; }
.busy-dots span:nth-child(3) { animation-delay: .36s; }

/* --- friend groups --------------------------------------------------------
   One thread, several friends, and one room code instead of five. Drawn in the same drawer
   as the friend list because it is the same question: who can I play with right now. */

.drawer-section { display: grid; gap: 6px; }
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.section-head .btn { flex: none; }

/* Three faces, overlapped, instead of a list of names. Reversed row direction so the FIRST
   avatar ends up on top without any of them needing a z-index. */
.faces { display: flex; flex-direction: row-reverse; flex: none; }
.faces .avatar { margin-left: -9px; box-shadow: 0 0 0 2px var(--card); }
.faces .avatar:last-child { margin-left: 0; }
.person.group .faces { margin-right: 2px; }

/* Who is in it. A panel between the head and the log, shown only when asked for, so the
   thread is not permanently paying screen space for a list that rarely changes. */
.member-panel {
  display: grid;
  gap: 6px;
  padding: 10px;
  margin-bottom: 8px;
  max-height: 45%;
  overflow-y: auto;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
}
.member-panel .request { background: #fff; }
.member-panel .role-tag { margin-left: 6px; }

/* Who said it. A group bubble without a name on it is unreadable. */
.dm-who {
  display: block;
  font-size: 11px;
  font-weight: 800;
  margin-bottom: 2px;
  filter: brightness(.72) saturate(1.3); /* the avatar colours are pale by design */
}

/* Picking who is in a new group. */
.pick-list {
  display: grid;
  gap: 6px;
  max-height: 210px;
  overflow-y: auto;
  padding: 4px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  border-radius: var(--r-sm);
}
.pick {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border-radius: 10px;
  background: #fff;
  border: 1.5px solid var(--line);
  cursor: pointer;
  font-size: 13.5px;
  font-weight: 700;
}
.pick:hover { border-color: var(--cyan-ink); }
.pick input { width: 17px; height: 17px; accent-color: var(--cyan-ink); flex: none; }
.pick .avatar { width: 24px; height: 24px; flex: none; }
.pick:has(input:checked) { border-color: var(--cyan-ink); background: #f2fbff; }

/* --- levels, badges, unlocks --------------------------------------------- */

.level-row { display: flex; align-items: center; gap: 14px; }

.level-badge {
  flex: none;
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border-radius: 16px;
  background: var(--cyan);
  color: #06222e;
  font-size: 21px; font-weight: 880;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 4px 0 #3e93b0;
}

.level-meter { flex: 1; min-width: 0; }
.level-bar {
  height: 12px;
  border-radius: 999px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  overflow: hidden;
}
.level-bar span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--cyan);
  border-radius: 999px;
  transition: width .5s cubic-bezier(.2, .8, .2, 1);
}
.level-text { font-size: 12px; color: var(--dim); margin-top: 6px; font-variant-numeric: tabular-nums; }

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(74px, 1fr)); gap: 8px; }
.stat {
  text-align: center;
  padding: 10px 6px;
  border-radius: var(--r-sm);
  background: var(--cream);
  border: 1.5px solid var(--line);
}
.stat b { display: block; font-size: 19px; font-weight: 850; font-variant-numeric: tabular-nums; }
.stat span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .1em; color: var(--dim); }

.badge-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
.badge {
  padding: 9px 11px;
  border-radius: var(--r-sm);
  background: var(--cream);
  border: 1.5px solid var(--line);
  text-align: left;
}
.badge b { display: block; font-size: 12.5px; font-weight: 800; }
.badge span { font-size: 11px; color: var(--dim); }
.badge.earned { background: #e9fff6; border-color: var(--mint-ink); }
.badge.earned b { color: #06392a; }

.unlock-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 9px; }
.tile.unlock { position: relative; font-size: 11.5px; }
.tile.unlock .avatar { width: 52px; height: 52px; }
/* A locked avatar is shown, not hidden: you cannot want something you cannot see. */
.tile.unlock.locked { opacity: .55; cursor: not-allowed; background: var(--cream-2); }
.tile.unlock.locked .avatar { filter: grayscale(1) contrast(.85); }
.lock-note {
  font-size: 9.5px; text-transform: uppercase; letter-spacing: .1em; font-weight: 800;
  color: var(--dim);
}

/* --- rewards ------------------------------------------------------------- */

.reward-sheet { text-align: center; }
.reward-sheet .who { color: var(--mint-ink); }
.reward-xp { font-family: var(--mono); font-size: 15px; font-weight: 800; color: var(--cyan-ink); margin: 0 0 12px; }
.reward-item {
  display: flex; align-items: center; gap: 12px; text-align: left;
  padding: 11px; margin-bottom: 8px;
  border-radius: var(--r-sm);
  background: #e9fff6; border: 1.5px solid var(--mint-ink);
}
.reward-item .nm { font-weight: 800; }
.reward-note { font-size: 12px; color: var(--ink-2); }
.reward-sheet .badge-grid { text-align: left; margin-top: 6px; }
.reward-sheet .row { justify-content: center; }

/* The fireworks canvas is only in the document while something is going off.
   It is a popover so it lands in the browser's top layer, ABOVE an open modal dialog -
   a plain z-index, however high, loses to showModal() every time. */
.fireworks {
  position: fixed;
  inset: 0;
  z-index: 200; /* the fallback, for browsers without popover */
  pointer-events: none;
}
/* A popover comes with UA styles that would shrink this to fit-content and give it a
   border and a background. All of that has to go. */
.fireworks:popover-open {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  padding: 0;
  background: transparent;
  overflow: hidden;
}
/* Fireworks going off behind a 38% dark backdrop are fireworks nobody sees. The reward
   card gets a lighter one so the celebration reads through it. */
.reward-sheet::backdrop { background: rgba(26, 29, 40, .18); }

.warn-note { color: var(--warn-ink); background: #fff8e8; border: 1.5px solid var(--warn); border-radius: var(--r-sm); padding: 9px 12px; }
#invite-link { font-size: 12.5px; font-family: var(--mono); }

/* --- toast --------------------------------------------------------------- */

.toast {
  position: fixed; top: 16px; left: 50%;
  transform: translateX(-50%);
  z-index: 99;
  background: #fff;
  border: 1.5px solid var(--danger);
  color: var(--danger-ink);
  padding: 12px 20px;
  border-radius: 12px;
  font-size: 13px;
  font-weight: 650;
  box-shadow: var(--shadow);
}
/* Confirmations are not warnings - the default red styling would read as an error. */
.toast.good { border-color: var(--mint); color: var(--mint-ink); }

/* --- account row (settings) ---------------------------------------------- */

.account-row { display: flex; align-items: center; gap: 12px; }
.account-row .nm { font-weight: 750; }
.account-row .pid { font-size: 11.5px; color: var(--dim); font-family: var(--mono); }
.account-row .btn { margin-left: auto; }

/* --- report dialog ------------------------------------------------------- */

.sheet {
  width: min(460px, calc(100vw - 32px));
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  padding: 22px;
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow);
  font-family: var(--ui);
}
.sheet::backdrop { background: rgba(26, 29, 40, .38); }
.sheet h2 {
  font-size: 11px; text-transform: uppercase; letter-spacing: .15em;
  color: var(--danger-ink); margin-bottom: 8px;
}
/* The rules dialog leads with the game's name, so its heading is a title rather than
   the small uppercase label the report dialog uses. */
.sheet .sheet-title {
  font-size: 22px; text-transform: none; letter-spacing: -.01em;
  color: var(--ink); margin: 2px 0 4px;
}
.sheet .who { font-size: 10.5px; text-transform: uppercase; letter-spacing: .14em; font-weight: 800; color: var(--cyan-ink); }
.rules-body { margin-top: 6px; }
.rules-body ol { margin-top: 0; }
.sheet .hint { margin: 0 0 16px; }
.sheet .row { justify-content: flex-start; margin-top: 20px; }

.field { display: grid; gap: 8px; margin-bottom: 16px; }
.field-label { font-size: 11px; text-transform: uppercase; letter-spacing: .13em; font-weight: 750; color: var(--dim); }
.field-label em { font-style: normal; text-transform: none; letter-spacing: 0; }

.pick-row { display: flex; flex-wrap: wrap; gap: 7px; }
.pick {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 12px;
  font-family: inherit; font-size: 13px; font-weight: 650;
  background: var(--cream);
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color .14s, background .14s, color .14s;
}
.pick:hover { border-color: var(--danger-ink); color: var(--danger-ink); }
.pick.on { border-color: var(--danger-ink); background: #ffe9ef; color: var(--danger-ink); }
.pick .avatar { width: 22px; height: 22px; }

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

@media (prefers-reduced-motion: reduce) {
  .pulse-ring, .typing-row .dots span { animation: none; }
  /* The landing page arrives all at once instead. Dropping the animation leaves each of
     these at its natural state - visible, unmoved - because the keyframes only apply
     while one is running. */
  .hero-mark, .hero-title, .hero .tagline, .hero-actions, .cover-card,
  .hero-demo, .demo-line, .demo-note, .demo-dots span { animation: none; }
  .cover-card:not(.soon):hover .cover-art::after { animation: none; }
  .cover-card:not(.soon):hover .cover-art img { transform: none; }
  .reveal, .reveal.shown { opacity: 1; transform: none; transition: none; }
  /* A background that never stops moving is precisely what this setting is asking about. It
     stays, it just stops falling. */
  .view-game.arena #game-panel-slot::before { animation: none; }
  /* demo.js draws the whole exchange at once in this mode, so the log is a static block
     and the verdict has to be visible without the transition that normally reveals it. */
  .demo-log { height: auto; justify-content: flex-start; }
  .demo-verdict { opacity: 1; transform: none; transition: none; }
}

/* Set by the in-game "reduce motion" switch, on top of the OS preference. */
.no-motion *,
.no-motion *::before,
.no-motion *::after {
  animation-duration: .001s !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001s !important;
}

@media (max-width: 720px) {
  .mood-grid { grid-template-columns: repeat(3, 1fr); }
  /* Cover beside text stops working around here; stack it and cap the art height so
     the buttons stay above the fold. */
  .play-card { grid-template-columns: 1fr; }
  .play-art { max-width: 210px; margin: 0 auto; }
}

@media (max-width: 640px) {
  :root { --appbar-h: 58px; }
  /* Room for the tab to stay on screen when the sheet is out. */
  .friends-drawer { width: calc(100vw - 46px); }
  .view { padding: calc(var(--appbar-h) + 18px) 14px 32px; }
  .brand-mark { height: 34px; }
  .brand-name { font-size: 15px; }
  .icon-btn.gear-lg { width: 42px; height: 42px; font-size: 20px; }
  /* THE CHIP NO LONGER COLLAPSES ON A PHONE, and the rules that made it are gone.
     They existed because it sat in a crowded top bar with no room for its name: padding: 4px,
     gap: 0, the name and "edit" hidden, and the level moved to a corner badge because with gap: 0
     the pill was jammed against the picture and read as damage.
     The chip now lives inside the menu - 260px of nothing but rows - so there is nothing to
     collapse for. Keeping the rules there left a beige box with an avatar in it and no name, no
     level and no "edit", which reads as a broken row rather than as a compact one. The level sits
     beside the name again, which is where it always wanted to be. */
  .app-bar .btn-kofi { padding: 9px 11px; font-size: 0; gap: 0; }
  .app-bar .btn-kofi .kofi-cup { font-size: 17px; }

  .game-grid { grid-template-columns: 1fr; }
  /* Who you are up against has to be readable, which it was not: a 64px strip with names cut to
     68px told you almost nothing in a game that is entirely about the other people. Taller, with
     room for a whole name and the score beside it. */
  .roster {
    border-right: 0;
    border-bottom: 1.5px solid var(--line);
    padding: 7px 10px;
    max-height: 78px;
  }
  .roster h3 { display: none; }
  .roster ul { grid-auto-flow: column; grid-auto-columns: max-content; overflow-x: auto; gap: 7px; }
  .roster li { padding: 5px 9px 5px 6px; gap: 7px; }
  .roster li .nm { max-width: 118px; font-size: 12.5px; }
  .roster li .avatar { width: 26px; height: 26px; }
  .roster li .status { font-size: 10px; }
  .timer { font-size: 18px; }
  .topbar { gap: 7px; }
  /* Four icon buttons plus a timer does not fit a 360px topbar. The logo goes, the
     buttons shrink, and the "you are <name>" chip goes too - it is the one thing on
     that bar the player already knows. */
  .topbar .mark, .you-chip { display: none; }
  .topbar .icon-btn { width: 32px; height: 32px; font-size: 14px; }
  /* The hidden chip was what pushed the buttons right; the rules button takes over. */
  .topbar #btn-rules { margin-left: auto; }
  .topbar .icon-btn.gear-lg { width: 36px; height: 36px; font-size: 17px; }
  .phase-pill, .role-pill { padding: 5px 9px; letter-spacing: .1em; }
  /* Five buttons, a timer and two pills do not fit a phone. The pills give way rather
     than the controls: a long role name ellipsises, and below 480px the phase pill goes
     entirely - the panel under the chat already names the phase in bigger type. */
  .phase-pill, .role-pill { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .shelf { grid-template-columns: 1fr 1fr; gap: 12px; }
  .cover-label { padding: 10px 11px 12px; }
  .cover-label h3 { font-size: 15px; }
  .cover-label p { font-size: 11.5px; }
  .code { font-size: 38px; }
  .result { padding: 20px; }
  .tally-row .name { width: 94px; }
  .msg .stack { max-width: 82%; }
}

/* Narrow phones. The match topbar is the tightest thing on the site, so the phase pill
   gives way here - the panel under the chat already names the phase in bigger type. */
@media (max-width: 480px) {
  .topbar { gap: 6px; padding: 10px; }
  .topbar .phase-pill { display: none; }
}
