/* Ludoo prototype client.
 *
 * Palette, materials and layout follow docs/phase-1/11-visual-art-direction.md.
 * This is not the premium 3D table — that is Unity, Phase 3 — but it is the only
 * surface that runs today, so it uses the real tokens and the real composition
 * rather than a second set that would have to be unlearned.
 *
 * Three rules from the direction shape most of what follows:
 *   §3.1  The board is the hero. Controls stay quiet so eyes go to the table.
 *   §4.2  Brass is an interface material; yellow is a player colour. Seat identity
 *         is colour PLUS shape and position, never colour alone.
 *   §3.5  Premium, not casino. Aged brass, no chips, no crowns, no betting.
 */

/* ==================================================================== tokens */

:root {
  --ink-950: #140d05;
  --ink-900: #221609;
  --ink-800: #33220f;
  --glass-smoke: #3a2a16;
  --brass-500: #c79537;
  --brass-300: #e4c276;
  --cream-100: #f2e7ce;

  --line: #3b2b15;
  --line-lit: #5c4526;
  --muted: #b5a084;

  --south: #a44735;   /* clay-600 */
  --west:  #1e6a3e;   /* forest-600 */
  --north: #28567e;   /* indigo-600 */
  --east:  #d5a520;   /* gold-seat */

  /* Lifted for text: the seat tokens are surface colours and fall below a
     readable contrast at small sizes on ink-950. */
  --south-ink: #d98c79;
  --west-ink:  #5fb183;
  --north-ink: #6fa3d1;
  --east-ink:  #e8c25c;

  --action: #1e6a3e;
  --action-lit: #2a8b53;

  --wood-900: #241708;
  --wood-800: #35220d;
  --wood-700: #4a3116;
  --wood-600: #5d3e1d;

  --board-cell: #342310;
  --board-line: #5c4526;

  --r-sm: 9px;
  --r-md: 14px;
  --r-lg: 20px;

  --shell: 560px;      /* phone-shaped column width */
  --ui-scale: 1;
}

* { box-sizing: border-box; }

html { font-size: calc(16px * var(--ui-scale)); }

body {
  margin: 0;
  background:
    radial-gradient(120% 80% at 50% -10%, #2d1d0c 0%, var(--ink-950) 55%),
    var(--ink-950);
  min-height: 100vh;
  color: var(--cream-100);
  font: 0.94rem/1.5 ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

.hidden { display: none !important; }

/* ================================================================== typography */

/* §8.1: rounded, substantial letterforms with enough mass to read as an object.
   A real wordmark is a commission; this is a stand-in that holds the right
   weight and proportion. The accessible name stays exactly "Ludoo". */
.wordmark {
  font-family: "Georgia", "Iowan Old Style", serif;
  font-weight: 700;
  font-size: 3.1rem;
  letter-spacing: -0.01em;
  line-height: 1;
  background: linear-gradient(175deg, #fdf6e4 0%, var(--cream-100) 45%, #d8bd88 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 2px 0 #00000055);
}
.wordmark.small { font-size: 1.45rem; }

.section-label {
  color: var(--muted);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 600;
  margin-bottom: 0.55rem;
}

.note { color: var(--muted); font-size: 0.76rem; line-height: 1.55; margin: 0.5rem 0 0; }
.error { color: var(--south-ink); font-size: 0.78rem; min-height: 1.2em; margin-top: 0.5rem; }

/* ==================================================================== screens */

.screen { min-height: 100dvh; }

/* A phone-shaped column, centred. Desktop screens override where a wider
   arrangement is genuinely better rather than merely bigger. */
.screen > .topbar,
.home-body, .lobby-body, .profile-body, .settings-body {
  width: min(100%, var(--shell));
  margin-inline: auto;
}

/* ===================================================================== panels */

.panel {
  background: linear-gradient(180deg, #2a1b0b 0%, var(--ink-900) 100%);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 0.95rem 1rem;
  box-shadow: 0 1px 0 #ffffff08 inset, 0 8px 22px #00000040;
}

/* ==================================================================== buttons */

.btn {
  font: inherit;
  font-weight: 600;
  border-radius: var(--r-sm);
  padding: 0.7rem 1.05rem;
  cursor: pointer;
  border: 1px solid transparent;
  color: var(--cream-100);
  background: var(--ink-800);
  transition: transform 90ms ease, box-shadow 90ms ease, background 120ms ease;
  min-height: 44px;                  /* §11.1 touch target */
}
.btn.block { width: 100%; }

/* §7.1: the primary action should feel pressed, lit and physical.
   §10.1: 2–4% depression, 80–120 ms down. */
.btn.primary {
  background: linear-gradient(180deg, var(--action-lit) 0%, var(--action) 100%);
  border-color: #2f9d5e55;
  box-shadow: 0 2px 0 #10442663, 0 8px 18px #00000055,
              0 1px 0 #ffffff33 inset;
}
.btn.primary:hover:not(:disabled) { box-shadow: 0 2px 0 #104426, 0 10px 24px #00000066, 0 1px 0 #ffffff33 inset; }
.btn.primary:active:not(:disabled) {
  transform: translateY(2px) scale(0.985);
  box-shadow: 0 0 0 #104426, 0 3px 8px #00000055;
}
.btn.primary:disabled { background: var(--ink-800); color: var(--muted); box-shadow: none; cursor: not-allowed; }

.btn.ghost { background: var(--ink-800); border-color: var(--line); }
.btn.ghost:hover { border-color: var(--brass-500); }
.btn.quiet { background: transparent; border-color: var(--line); color: var(--muted); }
.btn.quiet:hover { color: var(--cream-100); border-color: var(--line-lit); }

/* The hero action. Brass-edged and wide, because §7.1 gives it top billing. */
.btn.play {
  width: 100%;
  font-size: 1.5rem;
  letter-spacing: 0.14em;
  padding: 1.05rem;
  border-radius: 999px;
  border: 2px solid var(--brass-500);
  box-shadow: 0 3px 0 #104426, 0 12px 30px #00000066,
              0 0 0 1px #00000066, 0 1px 0 #ffffff33 inset;
  text-shadow: 0 2px 0 #0b3a20;
}
.btn.play .chev { color: var(--brass-300); opacity: 0.85; font-size: 1.1rem; vertical-align: 0.12em; }
.btn.play:active { transform: translateY(3px) scale(0.99); box-shadow: 0 0 0 #104426, 0 4px 10px #00000055; }

.btn-icon {
  display: inline-grid; place-items: center;
  width: 1.15rem; height: 1.15rem; margin-right: 0.45rem;
  border-radius: 3px; background: var(--cream-100); color: #17140f;
  font-weight: 800; font-size: 0.78rem; vertical-align: -0.18em;
}

.icon-btn {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--ink-800);
  border: 1px solid var(--line);
  color: var(--cream-100);
  font: inherit; font-size: 1rem;
  cursor: pointer;
}
.icon-btn:hover { border-color: var(--brass-500); }
.icon-btn.ghosted { background: none; border-color: transparent; pointer-events: none; }

.rule { display: grid; grid-template-columns: 1fr auto 1fr; gap: 0.7rem; align-items: center; margin: 0.2rem 0; }
.rule::before, .rule::after { content: ""; height: 1px; background: var(--line); }
.rule span { color: var(--muted); font-size: 0.72rem; }

input[type="text"], .select {
  background: var(--ink-950);
  border: 1px solid var(--line);
  color: var(--cream-100);
  border-radius: var(--r-sm);
  padding: 0.7rem 0.8rem;
  font: inherit;
  width: 100%;
  min-height: 44px;
}
input[type="text"]:focus, .select:focus { outline: 2px solid var(--brass-500); outline-offset: 1px; }

/* ====================================================================== topbar */

.topbar {
  display: flex; align-items: center; gap: 0.7rem;
  padding: max(0.9rem, env(safe-area-inset-top)) 1rem 0.7rem;
}
.topbar .wordmark, .topbar-title { flex: 1; }
.topbar-title { font-weight: 650; font-size: 1rem; text-align: center; }

.avatar {
  width: 40px; height: 40px; flex: none;
  border-radius: 50%;
  border: 2px solid var(--brass-500);
  background: linear-gradient(160deg, #4a3319, #2d1d0c);
  color: var(--cream-100);
  font: inherit; font-weight: 700;
  display: grid; place-items: center;
  cursor: pointer;
}
.avatar.big { width: 84px; height: 84px; font-size: 1.9rem; margin: 0 auto 0.7rem; }

/* ======================================================================= auth */

.auth {
  display: grid;
  align-content: center;
  gap: 1.6rem;
  padding: 2rem 1.1rem calc(2rem + env(safe-area-inset-bottom));
}
.auth-brand { text-align: center; }
.tagline { color: var(--brass-300); margin: 0.6rem 0 0; font-size: 0.95rem; }
.auth-points {
  list-style: none; margin: 1.1rem auto 0; padding: 0;
  max-width: 34ch; text-align: left; color: var(--muted); font-size: 0.82rem;
}
.auth-points li { padding: 0.3rem 0 0.3rem 1.2rem; position: relative; }
.auth-points li::before {
  content: "◆"; position: absolute; left: 0; color: var(--brass-500); font-size: 0.62rem; top: 0.55rem;
}

.auth-card {
  width: min(100%, 420px);
  margin-inline: auto;
  background: linear-gradient(180deg, #2a1b0b 0%, var(--ink-900) 100%);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 1.4rem;
  box-shadow: 0 1px 0 #ffffff0d inset, 0 20px 50px #00000066;
}
.auth-title { margin: 0; font-size: 1.25rem; font-weight: 700; }
.auth-sub { color: var(--muted); font-size: 0.82rem; margin: 0.35rem 0 1.1rem; }
.auth-actions { display: grid; gap: 0.6rem; }
.auth-note { color: var(--muted); font-size: 0.74rem; margin: 1rem 0 0; }

@media (min-width: 900px) {
  .auth {
    grid-template-columns: 1fr 420px;
    align-items: center;
    gap: 4rem;
    max-width: 1000px;
    margin-inline: auto;
  }
  .auth-brand { text-align: left; }
  .auth-points { margin-inline: 0; }
  .wordmark { font-size: 4.4rem; }
}

/* ======================================================================= home */

.home-body { padding: 0.4rem 1rem 6rem; display: grid; gap: 1rem; }

.identity { display: grid; gap: 0.15rem; }
.identity-label { color: var(--muted); font-size: 0.78rem; }
.identity-name { font-size: 1.35rem; font-weight: 700; }
.identity-meta { color: var(--brass-300); font-size: 0.76rem; margin-top: 0.2rem; }

.join .join-row { display: flex; gap: 0.5rem; }
.join-row input { text-transform: uppercase; letter-spacing: 0.22em; font-weight: 600; }
.join-row .btn { flex: none; }

.mode-list { display: grid; gap: 0.6rem; }

/* §7.1: one strong material panel, and mode cards that read as objects. */
.mode {
  display: grid;
  grid-template-columns: 44px 1fr auto;
  align-items: center;
  gap: 0.8rem;
  text-align: left;
  background: linear-gradient(180deg, #2c1e0e 0%, var(--ink-900) 100%);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 0.8rem 0.9rem;
  color: var(--cream-100);
  font: inherit;
  cursor: pointer;
  min-height: 60px;
  transition: transform 120ms ease, border-color 120ms ease;
}
.mode:hover { border-color: var(--line-lit); transform: translateY(-1px); }
.mode.selected { border-color: var(--brass-500); box-shadow: 0 0 0 1px #c7953733, 0 8px 22px #c7953718; }
.mode-emblem {
  width: 44px; height: 44px; border-radius: 11px;
  display: grid; place-items: center;
  background: linear-gradient(160deg, #3d2d17, #2d1d0c);
  border: 1px solid var(--line-lit);
  color: var(--brass-300); font-size: 1.15rem;
}
.mode-text { display: grid; gap: 0.1rem; }
.mode-name { font-weight: 650; font-size: 0.92rem; }
.mode-blurb { color: var(--muted); font-size: 0.76rem; }
.mode-go { color: var(--muted); }

@media (min-width: 900px) {
  .home-body {
    width: min(100%, 940px);
    grid-template-columns: 1fr 1fr;
    align-items: start;
    gap: 1.4rem;
    padding-bottom: 2rem;
  }
  .home-left { display: grid; gap: 1rem; align-content: start; }
  .home-right { display: grid; gap: 0; align-content: start; }
  .screen > .topbar { width: min(100%, 940px); }
}

/* ============================================================= the nav items
   Shared by the one menu: the pill styles the bar, these style what is in it. */

.nav-item {
  flex: 1;
  display: grid; justify-items: center; gap: 0.15rem;
  background: none; border: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 0.62rem;
  text-transform: uppercase; letter-spacing: 0.07em;
  padding: 0.35rem 0; min-height: 44px;
}
.nav-item .nav-icon { font-size: 1.05rem; }
.nav-item.on { color: var(--brass-300); }


/* ====================================================================== lobby */

.lobby-body { padding: 0.4rem 1rem 2rem; display: grid; gap: 1rem; }
.code-panel { text-align: center; }
.code {
  font-size: 2.3rem; font-weight: 800; letter-spacing: 0.26em;
  font-family: ui-monospace, "SF Mono", monospace;
  color: var(--brass-300);
  text-indent: 0.26em;
}
.code-share { color: var(--muted); font-size: 0.74rem; margin-top: 0.4rem; }
.url { color: var(--brass-300); font-family: ui-monospace, monospace; }

.seat-list { display: grid; gap: 0.45rem; }
.seat-row {
  display: flex; align-items: center; gap: 0.6rem;
  background: var(--ink-800); border: 1px solid var(--line);
  border-radius: 11px; padding: 0.6rem 0.7rem; font-size: 0.85rem;
}
.seat-row.you { border-color: var(--brass-500); }
.seat-glyph { font-size: 0.7rem; width: 1em; }
.seat-row.south .seat-glyph { color: var(--south-ink); }
.seat-row.west  .seat-glyph { color: var(--west-ink); }
.seat-row.north .seat-glyph { color: var(--north-ink); }
.seat-row.east  .seat-glyph { color: var(--east-ink); }
.seat-name { text-transform: capitalize; min-width: 3.2rem; font-weight: 600; }
.seat-who { color: var(--muted); flex: 1; }
.seat-actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }

.chip {
  background: var(--ink-800); border: 1px solid var(--line); color: var(--cream-100);
  border-radius: 999px; padding: 0.3rem 0.7rem; font: inherit; font-size: 0.74rem; cursor: pointer;
}
.chip:hover { border-color: var(--brass-500); }
/* The house shape, in the match as well: anything with a word in it is a pill.
   The die, the board and the pool are not buttons and keep their own shapes. */
.head-btn, .mini, .btn, .chip, .tab, .msg-act { border-radius: 999px; }

.mini-select {
  background: var(--ink-950); border: 1px solid var(--line); color: var(--cream-100);
  border-radius: 999px; padding: 0.25rem 0.55rem; font: inherit; font-size: 0.74rem;
}

/* ====================================================================== match */

.match {
  display: grid; gap: 1rem;
  padding: 0.8rem 0.8rem calc(1rem + env(safe-area-inset-bottom));
  /* The whole screen is the dice table. The felt runs edge to edge and under
     the floating menu; the board is an object resting on it, and the die is
     thrown beside the board on the open surface — which is where dice land on
     a real table. */
  /* Dark wood behind the raised table, giving way to the felt the die is
     thrown across — the reference's two grounds, one surface. */
  background:
    linear-gradient(180deg, #2b1a0a 0%, #241505 22%, #1d130a99 34%, transparent 46%),
    radial-gradient(140% 110% at 50% 0%, #1d5c3a 0%, #164a2f 52%, #0b2718 100%);
}

.table-area { position: relative; display: grid; justify-items: center; }
/* Not a child selector: the stage wrapper between them is display: contents,
   so the frame is the grid item but not the child. */
.table-area .table-frame { order: 1; }

/* --- the table as a physical object ------------------------------------
   §4.1: dark hardwood frame, smoked glass surface. The frame is a real
   border with grain and a routed inner edge, so the playable surface reads
   as something set INTO an object rather than a panel with a board on it. */
.table-frame {
  width: min(100%, 78vh);
  padding: clamp(10px, 2.6vw, 22px);
  border-radius: clamp(14px, 3vw, 26px);
  background:
    repeating-linear-gradient(92deg, #00000000 0 6px, #0000001f 6px 7px),
    linear-gradient(158deg, var(--wood-700) 0%, var(--wood-800) 42%, var(--wood-900) 100%);
  box-shadow:
    0 1px 0 #8a6a3d55 inset,
    0 -1px 0 #00000099 inset,
    0 26px 60px #00000077,
    0 4px 14px #0000005c;
  position: relative;
}
/* The table leans away from the player: a real object seen from a seat, not
   a chart pinned to the wall. The tilt is modest — every square stays
   readable, and pointer hit-testing maps through the transform natively —
   but it is enough that the thrown die's flight up the table and the board's
   own vanishing depth agree about which way "away" is. */
.table-frame {
  transform: perspective(1150px) rotateX(10deg) translateY(0) rotate(var(--board-rot, 0deg)) scale(1);
  transform-origin: 50% 50%;
  transition: transform var(--settle-dur, 0.5s) cubic-bezier(0.33, 0, 0.2, 1), box-shadow 0.5s ease;
}
/* Mid-turn the whole table lifts off the felt: a touch larger, its shadow
   deeper and further away, then it settles back down as the turn finishes.
   The class rides only for the swing; removing it plays the landing. */
.table-frame.turning {
  transform: perspective(1150px) rotateX(10deg) translateY(-9px) rotate(var(--board-rot, 0deg)) scale(1.025);
  transition: transform var(--turn-dur, 1.05s) cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.45s ease;
  box-shadow:
    0 2px 0 #b98f4a44 inset, 0 -8px 18px #00000066 inset,
    0 46px 90px #000000c4, 0 18px 34px #00000090;
}
body.still .table-frame { transition: none; }

/* Routed brass edge inside the frame — §4.1 brass as inlay, not plating. */
.table-frame::after {
  content: "";
  position: absolute; inset: clamp(6px, 1.5vw, 13px);
  border-radius: clamp(10px, 2.2vw, 19px);
  border: 1px solid #b98f4a4d;
  pointer-events: none;
}

.table-glass {
  position: relative;
  border-radius: clamp(8px, 2vw, 16px);
  overflow: hidden;
  background:
    linear-gradient(168deg, #4a3319 0%, var(--glass-smoke) 38%, #2c1d0d 100%);
  box-shadow:
    0 0 0 1px #00000099,
    0 10px 26px #00000066 inset;
}

#board { width: 100%; height: auto; display: block; }

/* §12.6: one transparent surface with a Fresnel-ish edge and a sheen. Cheap
   here, and the same read the URP material has to produce later. */
.glass-sheen {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(122deg, #ffffff14 0%, #ffffff05 26%, transparent 46%),
    radial-gradient(140% 100% at 50% -20%, #ffffff10 0%, transparent 55%);
  box-shadow: 0 0 40px #000000aa inset;
  border-radius: inherit;
}

/* --- board cells ------------------------------------------------------- */
.cell { fill: var(--board-cell); stroke: var(--board-line); stroke-width: 0.045; }
.cell.entry-south { fill: color-mix(in srgb, var(--south) 34%, var(--board-cell)); }
.cell.entry-west  { fill: color-mix(in srgb, var(--west) 34%, var(--board-cell)); }
.cell.entry-north { fill: color-mix(in srgb, var(--north) 34%, var(--board-cell)); }
.cell.entry-east  { fill: color-mix(in srgb, var(--east) 34%, var(--board-cell)); }

.lane-south { fill: color-mix(in srgb, var(--south) 26%, var(--board-cell)); }
.lane-west  { fill: color-mix(in srgb, var(--west) 26%, var(--board-cell)); }
.lane-north { fill: color-mix(in srgb, var(--north) 26%, var(--board-cell)); }
.lane-east  { fill: color-mix(in srgb, var(--east) 26%, var(--board-cell)); }

.yard { fill: #1d1309; stroke: #ffffff1a; stroke-width: 0.04; }

/* The home disc the pieces rest in — the reference's strongest read of "this
   corner belongs to somebody". */
.home-disc { stroke-width: 0.07; }
.home-disc-inner { fill: #1a1108; opacity: 0.55; }
.yard-south { stroke: color-mix(in srgb, var(--south) 55%, transparent); }
.yard-west  { stroke: color-mix(in srgb, var(--west) 55%, transparent); }
.yard-north { stroke: color-mix(in srgb, var(--north) 55%, transparent); }
.yard-east  { stroke: color-mix(in srgb, var(--east) 55%, transparent); }

.finish { fill: #2e1e0d; stroke: #b98f4a88; stroke-width: 0.06; }

.rosette { fill: none; stroke: #c79537; stroke-width: 0.05; opacity: 0.75; pointer-events: none; }
.rosette-core { fill: #c79537; opacity: 0.5; pointer-events: none; }

/* Brass hairline over the track, and a brass star on entry squares. */
.track-inlay { fill: none; stroke: #b98f4a3d; stroke-width: 0.035; pointer-events: none; }
.safe-star { fill: #d9ab5c88; pointer-events: none; }

/* --- seat marks under the glass (ADR-007's four placements) ------------- */
/* No names on the board: the colour is the identity there, and the interface
   speaks names on their colours (.name-chip). The disc carries only state —
   a brass ring for the turn, a lit ring for being addressed. */
.seat-ring {
  fill: none; stroke: transparent; stroke-width: 0.09;
  transition: stroke 0.3s;
  pointer-events: none;
}
.portrait-hit { fill: transparent; cursor: pointer; }
.portrait-group.active .seat-ring {
  stroke: var(--brass-300);
  filter: drop-shadow(0 0 0.18px #d9ab5caa);
}
.portrait-group.addressed .seat-ring { stroke: var(--cream-100); stroke-width: 0.11; }

/* --- pieces ------------------------------------------------------------ */
/* §4.1: resin — saturated, slightly translucent at the edge, with a moulding
   highlight. A flat disc reads as a counter; this reads as an object.
   Position comes from a transform so a move can be animated; see travel(). */
.piece { will-change: transform; }

/* Invisible, finger-sized, and still hit-testable: a transparent fill is not
   painted, so the target has to be declared explicitly. */
.piece-hit { fill: transparent; pointer-events: all; }
.piece:not(.playable) .piece-hit { pointer-events: none; }
.piece-body { stroke: #050c15; stroke-width: 0.045; }
.piece-hi { fill: #ffffff; opacity: 0.42; pointer-events: none; }
.piece-shadow { fill: #000000; opacity: 0.42; pointer-events: none; }

.piece.south .piece-body { fill: url(#gradSouth); }
.piece.west  .piece-body { fill: url(#gradWest); }
.piece.north .piece-body { fill: url(#gradNorth); }
.piece.east  .piece-body { fill: url(#gradEast); }

.piece.playable .piece-body {
  stroke: var(--cream-100); stroke-width: 0.07; cursor: pointer;
  /* Broken ring: there is still a choice to make after the tap. */
  stroke-dasharray: 0.13 0.1;
}
/* Solid ring: this tap plays the piece (a deployment). Keyed to deployment and
   not to "one option", so the ring never announces how many moves a piece has. */
.piece.playable.plays-on-tap .piece-body { stroke-dasharray: none; }
.piece.playable:hover .piece-body { stroke-width: 0.11; }
.piece.selected .piece-body { stroke: var(--brass-300); stroke-width: 0.13; }
.piece.switch-offer .piece-body {
  stroke: var(--brass-300); stroke-width: 0.1; stroke-dasharray: 0.15 0.1;
  animation: pulse 1.15s ease-in-out infinite;
}

.block-count {
  fill: var(--cream-100); font-size: 0.3px; font-weight: 700;
  text-anchor: middle; pointer-events: none;
  paint-order: stroke; stroke: #050c15; stroke-width: 0.06;
}

/* --- selection treatments (§7.5, ADR-009) ------------------------------ */
.cell.tappable, .finish.tappable { cursor: pointer; }

/* Which square the pointer is actually on.
   Destination selection is a tap on a square, and until now the only feedback
   that a square was the one under the cursor was the cursor itself — on a board
   of 52 near-identical cells that is not enough to aim with, and a tap landing
   one square off is exactly the mistake the near-miss correction exists to
   catch. Cheaper to stop it happening.

   Deliberately brightness and not stroke. Stroke is the meaning-carrying
   channel here — .dest, .correcting, .mistapped and .ambiguous all speak in it —
   and hover must say only "your tap lands here", never "this move is legal".
   With help mode off, working the legal squares out is the game (see
   renderSquares), so a hover that tracked legality would quietly hand back the
   thing the mode withholds. Brightness also composes: hovering a marked square
   keeps its ring and lifts the fill, rather than fighting it.

   Fine pointers only. On a touchscreen :hover sticks after the tap that played
   the move, leaving a square lit that no longer means anything. */
@media (hover: hover) and (pointer: fine) {
  .cell.tappable:hover, .finish.tappable:hover {
    filter: brightness(1.22);
    transition: filter 90ms ease-out;
  }
}

.cell.dest, .finish.dest {
  stroke: var(--brass-300); stroke-width: 0.085; stroke-dasharray: 0.22 0.14;
  animation: fadeIn 180ms ease-out;
}
.dest-dot { fill: var(--brass-300); opacity: 0.85; pointer-events: none; animation: fadeIn 180ms ease-out; }

/* The correction. Deliberately not a variant of .dest — with help mode off
   there is no .dest on screen for it to be a variant of, and it has to read
   as a question rather than a result, because nothing has moved. */
.cell.correcting, .finish.correcting {
  stroke: var(--brass-500); stroke-width: 0.125; stroke-dasharray: 0.18 0.12;
  animation: pulse 1.15s ease-in-out infinite;
}
.cell.mistapped, .finish.mistapped {
  stroke: var(--south-ink); stroke-width: 0.085; stroke-dasharray: 0.1 0.1; stroke-opacity: 0.8;
}
.correcting-arrow {
  stroke: var(--brass-500); stroke-width: 0.065; stroke-linecap: round;
  stroke-dasharray: 0.14 0.1; opacity: 0.9; pointer-events: none;
}
.cell.ambiguous, .finish.ambiguous {
  stroke: var(--brass-300); stroke-width: 0.125; stroke-dasharray: 0.16 0.16;
  animation: pulse 1.15s ease-in-out infinite;
}

@keyframes pulse { 0%, 100% { stroke-opacity: 0.55; } 50% { stroke-opacity: 1; } }
@keyframes fadeIn { from { opacity: 0.3; } to { opacity: 1; } }

/* ================================================================ controller */

.controller { display: grid; gap: 0.7rem; align-content: start; }

.controller-top { display: flex; align-items: center; gap: 0.6rem; }
.controller-top .match-conditions { flex: 1; display: flex; gap: 0.4rem; flex-wrap: wrap; justify-content: center; }

/* §11.4: the fixed state strings, given the weight they carry in the
   reference — one dominant line, seat-coloured, unmissable. */
.turn { text-align: center; padding: 0.2rem 0 0.1rem; }
.turn-label {
  font-family: Georgia, serif;
  font-size: 1.75rem; font-weight: 700; letter-spacing: 0.08em;
  line-height: 1.1;
  /* Gold leaf, like the reference's headline — lit top, bronze foot. */
  background: linear-gradient(180deg, #f9e2a4 8%, #e2b563 55%, #b98033 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 2px 3px #00000088);
}
.turn-who {
  margin-top: 0.15rem; font-size: 0.95rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.turn-who .diamond { color: var(--brass-500); opacity: 0.7; margin: 0 0.4rem; font-size: 0.6rem; vertical-align: 0.18em; }
.turn.south .turn-who { color: var(--south-ink); }
.turn.west  .turn-who { color: var(--west-ink); }
.turn.north .turn-who { color: var(--north-ink); }
.turn.east  .turn-who { color: var(--east-ink); }

/* §7.3: one large textured tray with a clear safe launch zone. Felt, not
   Kente — dense cloth under the die costs motion readability. */
.tray {
  background: transparent;
  padding: 0;
  text-align: center;
}
.tray-surface {
  position: fixed; inset: 0; z-index: 5;
  background: transparent;
  pointer-events: none;          /* the board stays tappable; the die takes the finger */
  touch-action: none;
}
.tray-hint {
  color: var(--brass-300); font-size: 0.72rem; text-align: center;
  margin-top: 0.55rem; min-height: 1.4em;
  text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600;
}

/* --- the roll pool, §5 -------------------------------------------------
   The numbers thrown and not yet spent, resting on the felt. Shown as faces
   rather than digits for the same reason the die is: a numeral reads as a
   counter, a face reads as an object you could pick up. */
/* The banked numbers stack beside the die, like dice set aside on the felt —
   at its right hand, one tap from filtering the board to what a number can
   do. Placed by layoutFelt with the die itself, since they travel together. */
.pool {
  position: fixed;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  pointer-events: none; z-index: 6;
}
.pool:empty { display: none; }
.pool-chip {
  pointer-events: auto;
  min-width: 30px; height: 30px; padding: 0 7px;
  border-radius: 8px; border: 1px solid #0a2517;
  background: linear-gradient(155deg, #fdf8ec 0%, #efe3c8 60%, #d9c9a4 100%);
  color: #3c2c16; font-weight: 800; font-size: 0.85rem;
  display: grid; place-items: center; cursor: pointer;
  box-shadow: 0 2px 0 #b9a883, 0 4px 10px #00000055;
  font-variant-numeric: tabular-nums;
}
.pool-chip[aria-pressed="true"] { outline: 2px solid var(--brass-300); outline-offset: 2px; }
/* A chip is a die set aside, so it holds its face and wears its provenance in
   the corner: quiet enough to scan past, there when Kick and Go depends on it. */
.pool-chip { position: relative; }
.chip-face { line-height: 1; }
.chip-mark {
  position: absolute; top: -5px; right: -5px;
  min-width: 13px; height: 13px; padding: 0 2px;
  border-radius: 999px;
  background: var(--brass-300); color: #1a1206;
  font-size: 0.52rem; font-weight: 800; letter-spacing: 0;
  display: grid; place-items: center;
  box-shadow: 0 1px 3px #00000066;
}
.pool-chip.spent .chip-mark { background: #46536a; color: #1d1309; box-shadow: none; }
/* The one the order is waiting on. */
.pool-chip.next { box-shadow: 0 2px 0 #b9a883, 0 4px 10px #00000055, 0 0 0 2px #e8c98a66; }
.pool-chip.spent {
  background: #16283a; color: var(--muted); border-color: #1d1309;
  box-shadow: none; cursor: default; opacity: 0.55; font-weight: 600;
}
.pool-chip.lost { text-decoration: line-through; }
.pool-chip:disabled { cursor: default; }

/* §7. Reserved rather than conditional: a control that appears and disappears
   changes the tray's height mid-turn, and the die must never move under a
   hand that is already reaching for it. */
.stop-throwing {
  display: block; width: 100%; margin-top: 0.5rem;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em;
}
.stop-throwing[hidden] { display: block; visibility: hidden; }

/* §4.1: warm off-white resin with dark inset pips. */
.die {
  position: absolute; left: 50%; top: auto;
  bottom: calc(96px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  pointer-events: auto;
  cursor: grab;
  width: 62px; height: 62px;
  border-radius: 13px;
  background: linear-gradient(155deg, #fdf8ec 0%, #efe3c8 55%, #d9c9a4 100%);
  display: grid; place-items: center;
  cursor: grab;
  user-select: none;
  /* The die is a drag control. Without this the browser may claim the first
     touch as a pan and cancel the pointer, which loses the gesture silently —
     the tray has touch-action: none but that does not cover its children. */
  touch-action: none;
  box-shadow:
    0 3px 0 #b9a883, 0 10px 20px #00000077,
    0 1px 0 #fffdf6 inset, 0 -3px 6px #00000018 inset;
}
.die-face { width: 100%; height: 100%; }
.die-pip { fill: #2a2216; }
.die.disabled { opacity: 0.4; cursor: not-allowed; }
.die.rolling { animation: tumble 420ms ease-out; }

@keyframes tumble {
  0%   { transform: translate(-50%, -50%) rotate(0deg) scale(1); }
  60%  { transform: translate(-50%, -170%) rotate(320deg) scale(0.84); }
  100% { transform: translate(-50%, -50%) rotate(360deg) scale(1); }
}

.flick-guide { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.flick-guide line { stroke: var(--brass-300); stroke-width: 3; stroke-linecap: round; opacity: 0.75; }

/* --- the readout ------------------------------------------------------- */
.readout {
  background: linear-gradient(180deg, #2a1b0b, var(--ink-900));
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 0.7rem 0.85rem;
  font-size: 0.84rem;
  min-height: 52px;
  display: grid; gap: 0.45rem; align-content: start;
}
.readout.quiet { color: var(--muted); font-size: 0.8rem; }
.readout .head { font-weight: 700; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em; }
.readout .body { color: var(--cream-100); font-size: 0.82rem; line-height: 1.45; }
.readout .body em { color: var(--brass-300); font-style: normal; font-weight: 700; }
.readout.correction { border-color: var(--brass-500); }
.readout.correction .head { color: var(--brass-300); }
.readout.refused { border-color: #4a3038; }
.readout.refused .head { color: var(--south-ink); }
.readout.choice { border-color: var(--brass-500); }
.readout.choice .head { color: var(--brass-300); }

.readout .choices { display: flex; gap: 0.45rem; flex-wrap: wrap; }
.choice-btn {
  background: var(--ink-800); border: 1px solid var(--brass-500); color: var(--cream-100);
  border-radius: 999px; padding: 0.45rem 0.85rem; font: inherit; font-size: 0.8rem;
  cursor: pointer; min-height: 40px;
}
.choice-btn:hover { background: #352311; }
.choice-btn:active { transform: translateY(1px) scale(0.98); }
.choice-btn .cap { color: var(--south-ink); font-weight: 700; }

.blocked .b-piece { color: var(--cream-100); font-weight: 600; margin-right: 0.4rem; }

/* --- help-mode badge --------------------------------------------------- */
.help-badge {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.2rem 0.6rem; border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 0.63rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}
.help-badge.on { border-color: var(--brass-500); color: var(--brass-300); }
.help-badge .mark { font-size: 0.55rem; }

/* --- log & meta -------------------------------------------------------- */
.log-wrap {
  background: var(--ink-900); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 0.55rem 0.8rem;
}
.log-wrap summary { cursor: pointer; color: var(--muted); font-size: 0.72rem; }
.log { list-style: none; margin: 0.6rem 0 0; padding: 0; max-height: 180px; overflow-y: auto; font-size: 0.74rem; }
.log li { padding: 0.16rem 0; border-bottom: 1px solid var(--line); color: var(--muted); }
.log li:last-child { border-bottom: 0; }
.log .hl { color: var(--cream-100); font-weight: 600; }

/* Your own attempts, separated from what the match did. A refused input has to
   leave evidence, or "it just didn't move" is the only bug report possible. */
.log .you { color: var(--brass-300); }
.log .you::before { content: ""; }

.meta { color: var(--muted); font-size: 0.68rem; line-height: 1.6; }
.meta code { color: var(--brass-300); }
.badge {
  display: inline-block; padding: 0.05rem 0.45rem; border-radius: 999px;
  border: 1px solid var(--line); font-size: 0.6rem; letter-spacing: 0.05em; text-transform: uppercase;
}

/* --- remarks -----------------------------------------------------------
   Bubbles, wherever they are shown. Remarks arrive as notifications now — the
   strip that used to hold them is gone — but the chat pane and the notification
   stack both dress a remark the same way. */
.bubble {
  background: var(--ink-800); border: 1px solid var(--line); border-left-width: 3px;
  border-radius: var(--r-sm); padding: 0.42rem 0.7rem; font-size: 0.8rem;
  animation: rise 220ms ease-out;
}
.bubble.south { border-left-color: var(--south); }
.bubble.west  { border-left-color: var(--west); }
.bubble.north { border-left-color: var(--north); }
.bubble.east  { border-left-color: var(--east); }
.bubble .who { font-weight: 650; margin-right: 0.35rem; }
.bubble.south .who { color: var(--south-ink); }
.bubble.west .who  { color: var(--west-ink); }
.bubble.north .who { color: var(--north-ink); }
.bubble.east .who  { color: var(--east-ink); }
.bubble .arrow-to { color: var(--muted); margin: 0 0.3rem; }
.bubble .to { font-weight: 650; }
.bubble .to.south { color: var(--south-ink); }
.bubble .to.west  { color: var(--west-ink); }
.bubble .to.north { color: var(--north-ink); }
.bubble .to.east  { color: var(--east-ink); }
.bubble .said { display: block; margin-top: 0.1rem; }
.bubble.taunt-said { background: #2c2010; font-style: italic; }

@keyframes rise { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

.taunt-menu {
  position: absolute; z-index: 20; width: 240px; display: grid; gap: 0.25rem;
  background: var(--ink-900); border: 1px solid var(--line-lit);
  border-radius: 12px; padding: 0.5rem; box-shadow: 0 10px 28px #000a;
  left: 50%; transform: translateX(-50%); top: 20%;
}
.taunt-title {
  color: var(--muted); font-size: 0.62rem; text-transform: uppercase;
  letter-spacing: 0.07em; padding: 0.15rem 0.25rem 0.25rem;
}
.taunt {
  background: var(--ink-800); border: 1px solid var(--line); color: var(--cream-100);
  border-radius: var(--r-sm); padding: 0.42rem 0.65rem; font: inherit; font-size: 0.78rem;
  text-align: left; cursor: pointer; min-height: 38px;
}
.taunt:hover { border-color: var(--brass-500); background: #352311; }

/* --- match layout ------------------------------------------------------ */
/* --- the table as the whole screen ---------------------------------------
   §3.1 says the board is the hero, and on a large screen a 380px sidebar
   taking a third of the width is the loudest thing on it. So the table fills
   the viewport and the controller stops being a column: `display: contents`
   drops its children into the match layer, and each one docks to an edge as
   its own floating card.

   The arrangement is by importance, not by tidiness. The die is the only
   thing a player reaches for on their turn, so it takes the bottom centre —
   under the hand, under the board. Turn ownership is the one fact that must
   never be ambiguous (see the note above `renderTurn`), so it sits top right
   on its own. Everything social or historical goes to a corner.

   Nothing here is reachable below 1000px: the phone layout is the real
   controller relationship and stays a stacked column. */
@media (min-width: 1000px) {
  .match {
    --dock: 0px;            /* the die lives beside the board now, not beneath it */
    /* Relative, not fixed: `.screen` already gives it a full viewport of
       height, and taking it out of flow would collapse the document to zero
       and break anything that measures the page. The HUD children below are
       absolute, so they anchor to this. */
    position: relative;
    height: 100dvh;
    display: block;
    max-width: none; margin: 0; padding: 0;
    overflow: hidden;
  }

  /* The dock is a reserved band, not a suggestion. The die sits in it, and the
     board is sized so it never reaches into it: a tray floating over the south
     home column would cover the squares its owner cares about most, which is a
     worse trade than a slightly smaller table. */
  /* The board's zone. Its own grid cell, centred in it — the surrounding
     interface is arranged around the board, not the other way about. */
  .table-area {
    position: static;
    display: grid; place-items: center;
    padding: 0; min-width: 0; min-height: 0;
  }
  /* Bounded by height AND width so the frame clears the docked cards rather
     than sitting under them. 80vh leaves a lane for the pills top left and
     right; 56vw leaves one for the side card and the meta block. The cards do
     float above the table if a window gets tight, which is the intended
     behaviour of a HUD — but at any ordinary size nothing covers the board. */
  /* Square, and as large as the header and the panel leave room for. */
  .table-frame { width: var(--board-max); }

  .controller { display: contents; }

  /* top left — where you came from, and what rules are in force */
  .controller-top {
    position: absolute; top: 18px; left: 18px; transform: none; z-index: 5;
    width: var(--col); box-sizing: border-box;
    background: linear-gradient(180deg, #241708e6, #170e05e6);
    border: 1px solid var(--line); border-radius: 999px;
    box-shadow: 0 18px 44px #00000066; backdrop-filter: blur(8px);
    padding: 0.4rem 0.9rem 0.4rem 0.5rem;
    gap: 0.7rem;
  }
  .controller-top .match-conditions { flex: 0 1 auto; justify-content: flex-start; }
  .controller-top .icon-btn.ghosted { display: none; }

  /* top right — turn ownership, alone, so it cannot be misread */
  .turn {
    /* In the die's rail, under the avatar: the right-hand column reads
       whose turn it is, then gives the rest of its height to the throw. */
    position: absolute; top: 76px; right: 18px; z-index: 5;
    width: calc(var(--rail) - 36px); box-sizing: border-box;
    text-align: center; padding: 0.55rem 0.9rem;
    background: linear-gradient(180deg, #241708e6, #170e05e6);
    border: 1px solid var(--line); border-radius: var(--r-lg);
    box-shadow: 0 18px 44px #00000066; backdrop-filter: blur(8px);
  }
  .turn-label { font-size: 1.35rem; }
  .turn-who { overflow-wrap: anywhere; }

  /* bottom centre — the die, under the hand */
  .tray {
    position: absolute; bottom: 176px; left: 50%; transform: translateX(-50%);
    z-index: 6; margin: 0;
  }
  .die { bottom: 44px; }
  /* Stacked over the tray, inside the dock. Flanking it looked tidier and was
     not: at 1400px the captions ran straight into the side card and the meta
     block, because the lane either side of a centred tray is only as wide as
     the gutters. Stacked, the only cost is dock height, and it is bounded —
     the blocked list scrolls rather than growing into the board. */
  .readout {
    position: absolute; bottom: 210px; left: 50%;
    transform: translateX(-50%); z-index: 6;
    width: min(440px, 40vw); margin: 0; text-align: center;
  }
  /* Not in the dock. This is the "why can't that piece move" list — reading
     matter, closer in kind to the log than to the throw — and putting it in the
     centre made the dock tall enough to shrink the board for something that is
     usually empty. Stacked above the side card it collides with nothing, in a
     column the eye already goes to for explanation. */


}

/* ==================================================================== profile */

.profile-body, .settings-body { padding: 0.4rem 1rem 6rem; display: grid; gap: 1rem; }
.profile-card { text-align: center; }
.profile-name { font-size: 1.2rem; font-weight: 700; }
.profile-kind { color: var(--muted); font-size: 0.78rem; margin-top: 0.15rem; }
.btn-stack { display: grid; gap: 0.5rem; }

/* =================================================================== settings */

.row-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.6rem 0; border-bottom: 1px solid var(--line); cursor: pointer;
  min-height: 52px;
}
.row-toggle:last-of-type { border-bottom: 0; }
.row-title { display: block; font-size: 0.88rem; font-weight: 600; }
.row-sub { display: block; color: var(--muted); font-size: 0.74rem; margin-top: 0.1rem; }

.row-toggle input[type="checkbox"] {
  appearance: none; flex: none;
  width: 46px; height: 27px; border-radius: 999px;
  background: var(--ink-800); border: 1px solid var(--line);
  position: relative; cursor: pointer; transition: background 140ms ease, border-color 140ms ease;
}
.row-toggle input[type="checkbox"]::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 21px; height: 21px; border-radius: 50%;
  background: var(--muted); transition: transform 140ms ease, background 140ms ease;
}
.row-toggle input[type="checkbox"]:checked { background: #1b4a30; border-color: var(--brass-500); }
.row-toggle input[type="checkbox"]:checked::after { transform: translateX(19px); background: var(--brass-300); }

.row-range { display: grid; grid-template-columns: 1fr auto; gap: 0.4rem 0.8rem; align-items: center; padding: 0.7rem 0; }
.row-range input[type="range"] { grid-column: 1 / -1; accent-color: var(--brass-500); }
.row-value { color: var(--brass-300); font-size: 0.8rem; font-weight: 600; }

/* ============================================================ accessibility */

/* §11.2: a high-contrast board with solid cells and reduced surface detail. */
body.contrast .glass-sheen { display: none; }
body.contrast .table-glass { background: #170e06; }
body.contrast .cell { fill: #231709; stroke: #52657d; stroke-width: 0.05; }
body.contrast .track-inlay { stroke: #8fa5bd88; }
body.contrast .portrait-weave { display: none; }
body.contrast .piece-hi { display: none; }
body.contrast .piece-body { stroke: #ffffff; stroke-width: 0.07; }
body.contrast .cell.dest, body.contrast .finish.dest { stroke: #ffe6ad; stroke-width: 0.12; }
/* The contrast board's cells are darker and flatter, so the same multiplier moves
   them far less in absolute terms. Lift it until the hovered square is as easy to
   pick out as it is on the default board. */
@media (hover: hover) and (pointer: fine) {
  body.contrast .cell.tappable:hover, body.contrast .finish.tappable:hover {
    filter: brightness(1.7);
  }
}

/* §10.1 / §11.3: every decorative motion needs a reduced-motion alternative.
   The correcting pulse is not decorative — it is how an uncommitted state
   reads as uncommitted — so it becomes a static high-contrast edge. */
body.still .cell.correcting, body.still .finish.correcting,
body.still .cell.ambiguous, body.still .finish.ambiguous,
body.still .piece.switch-offer .piece-body {
  animation: none; stroke-opacity: 1;
}
body.still .die.rolling, body.still .bubble,
body.still .cell.dest, body.still .finish.dest, body.still .dest-dot { animation: none; }
body.still .btn, body.still .mode { transition: none; }
/* The hover lift itself stays — it reports where the pointer is, which is not
   decoration. Only the fade to it goes. */
body.still .cell.tappable:hover, body.still .finish.tappable:hover { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .cell.correcting, .finish.correcting, .cell.ambiguous, .finish.ambiguous,
  .piece.switch-offer .piece-body { animation: none; stroke-opacity: 1; }
  .die.rolling, .bubble, .cell.dest, .finish.dest, .dest-dot { animation: none; }
  .btn, .mode { transition: none; }
  .cell.tappable:hover, .finish.tappable:hover { transition: none; }
}

:focus-visible { outline: 2px solid var(--brass-300); outline-offset: 2px; }

/* ==================================================== tabs, chat, leaderboard */

/* A segmented control in the shadcn tabs idiom: one muted full-width track,
   rounded to a pill, the active segment raised out of it. The folder-tab shape
   this replaces belonged to a panel these tabs no longer sit on. */
.panel-tabs {
  display: flex; gap: 0;
  width: 100%;
  background: var(--ink-950);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px;
}
.tab {
  flex: 1; background: transparent; border: 0;
  color: var(--muted); font: inherit; font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em;
  border-radius: 999px; padding: 0.5rem 0.3rem;
  cursor: pointer; min-height: 36px;
}
.tab.on {
  color: var(--cream-100);
  background: var(--ink-800);
  box-shadow: 0 1px 3px #00000066, 0 0 0 1px var(--line-lit);
}

/* Fixed height on purpose: the panel must not grow as talk arrives, or the
   board moves to make room for it — the thing §3.1 says must stay put. */
.tab-body {
  border: 0;
  margin-top: 0.6rem;
  border-radius: var(--r-md);
  background: var(--ink-900);
  height: clamp(190px, 26vh, 280px);
  overflow: hidden;
}
.tab-pane { display: none; height: 100%; flex-direction: column; }
.tab-pane.on { display: flex; }

/* --- chat ----------------------------------------------------------------
   The messenger shape everyone already knows: bubbles left and right with
   soft corners, the author named inside the bubble, replies quoted in a
   tinted block, mentions in the mentioned seat's colour, and one input
   pinned to the foot that never leaves the screen. */
.chat {
  list-style: none; margin: 0; padding: 0.6rem 0.6rem 0.4rem;
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  flex: 1; min-height: 0;
  display: grid; gap: 0.45rem; align-content: start;
}
.chat-empty { color: var(--muted); font-size: 0.78rem; text-align: center; padding: 1.2rem 0.6rem; }

.msg { display: grid; gap: 0.15rem; justify-items: start; max-width: 86%; }
.msg.mine { justify-self: end; justify-items: end; }
.msg-body {
  background: var(--ink-800); border: 1px solid var(--line);
  border-radius: 14px 14px 14px 4px;
  padding: 0.42rem 0.65rem 0.45rem; font-size: 0.8rem;
  box-shadow: 0 1px 2px #00000033;
}
/* Your own bubble sits on the felt's green, one step lit — "mine" reads at a
   glance without a label, the way the messenger's green bubble does. */
.msg.mine .msg-body {
  background: #1d4530; border-color: #2c5e43;
  border-radius: 14px 14px 4px 14px;
}
.msg-text { display: block; margin-top: 0.1rem; overflow-wrap: anywhere; }

/* The quoted line a reply answers, inside the bubble: the original author's
   colour on the bar and their name, the words trimmed to a snippet. */
.msg-quote {
  display: grid; gap: 1px;
  background: #00000038;
  border-left: 3px solid var(--line-lit);
  border-radius: 7px;
  padding: 0.3rem 0.5rem; margin: 0.15rem 0 0.3rem;
  font-size: 0.72rem;
}
.msg-quote.south { border-left-color: var(--south); }
.msg-quote.west  { border-left-color: var(--west); }
.msg-quote.north { border-left-color: var(--north); }
.msg-quote.east  { border-left-color: var(--east); }
.q-who { font-weight: 700; font-size: 0.68rem; color: var(--brass-300); }
.q-who.south { color: var(--south-ink); }
.q-who.west  { color: var(--west-ink); }
.q-who.north { color: var(--north-ink); }
.q-who.east  { color: var(--east-ink); }
.q-text {
  color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}

/* An @mention wears the mentioned seat's ink. */
.mention { font-weight: 700; }
.mention.south { color: var(--south-ink); }
.mention.west  { color: var(--west-ink); }
.mention.north { color: var(--north-ink); }
.mention.east  { color: var(--east-ink); }

.msg-actions { display: flex; gap: 0.25rem; margin-top: 0.15rem; }
.msg-act {
  background: none; border: 0; color: var(--muted);
  font: inherit; font-size: 0.68rem; cursor: pointer; padding: 0.1rem 0.25rem;
  border-radius: 5px;
}
.msg-act:hover { color: var(--brass-300); background: #ffffff0d; }

.reactions { display: flex; gap: 0.2rem; flex-wrap: wrap; margin-top: 0.15rem; }
.reaction {
  background: var(--ink-950); border: 1px solid var(--line);
  border-radius: 999px; padding: 0.05rem 0.4rem; font-size: 0.72rem;
  display: inline-flex; gap: 0.2rem; align-items: center;
}
.reaction .n { color: var(--muted); font-size: 0.64rem; }

/* The composer: always present, pinned under the transcript. position:
   relative anchors the mention popup. */
.chat-compose {
  position: relative; flex: none;
  border-top: 1px solid var(--line);
  background: var(--ink-900);
  padding: 0.45rem 0.5rem;
  display: grid; gap: 0.35rem;
}
.chat-compose[hidden] { display: none; }

/* The reply being answered, quoted above the input in the same dress as the
   in-bubble quote, with its own way out. */
.compose-reply {
  display: flex; align-items: center; gap: 0.5rem;
  background: #00000038;
  border-left: 3px solid var(--brass-500);
  border-radius: 7px;
  padding: 0.3rem 0.5rem;
}
.compose-reply[hidden] { display: none; }
.compose-reply-body { flex: 1; min-width: 0; display: grid; gap: 1px; font-size: 0.72rem; }
.compose-reply-x {
  flex: none; background: none; border: 0; color: var(--muted);
  font: inherit; font-size: 0.8rem; cursor: pointer; padding: 0.15rem 0.3rem;
}
.compose-reply-x:hover { color: var(--cream-100); }

/* Who "@" could mean, floated over the transcript's foot. */
.mention-pop {
  position: absolute; left: 0.5rem; right: 0.5rem; bottom: calc(100% + 4px);
  display: flex; flex-wrap: wrap; gap: 6px;
  background: linear-gradient(180deg, #2e1e0d, #241708);
  border: 1px solid var(--line-lit); border-radius: 12px;
  padding: 0.5rem;
  box-shadow: 0 -8px 24px #00000066;
  z-index: 5;
}
.mention-pop[hidden] { display: none; }
.mention-option { background: none; border: 0; padding: 0; cursor: pointer; }

/* --- leaderboard --------------------------------------------------------- */
.leaderboard-body { padding: 0.4rem 1rem 6rem; display: grid; gap: 0.8rem; }

.mode-tabs { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.mode-tab {
  background: var(--ink-800); border: 1px solid var(--line); color: var(--muted);
  border-radius: 999px; padding: 0.35rem 0.8rem; font: inherit; font-size: 0.74rem;
  cursor: pointer; min-height: 36px;
}
.mode-tab.on { border-color: var(--brass-500); color: var(--brass-300); }

.board-table { overflow-y: auto; height: 100%; }
.board-row {
  display: grid; grid-template-columns: 1.6rem 1fr auto auto;
  gap: 0.55rem; align-items: center;
  padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--line);
  font-size: 0.82rem;
}
.board-row:last-child { border-bottom: 0; }
.board-rank { color: var(--muted); font-size: 0.74rem; text-align: right; }
.board-name { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }
.board-name .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-kind {
  font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 0 0.35rem;
}
.board-played { color: var(--muted); font-size: 0.74rem; }
.board-won { font-weight: 700; color: var(--cream-100); min-width: 1.6rem; text-align: right; }
.board-row.leader { background: #c795370f; }
.board-row.unranked .board-name .who { color: var(--muted); }

/* The crown. Requested explicitly; note §3.5 lists crowns among the casino and
   royal-palace signals to keep out, so it is one token here and nothing else in
   the interface leans on it. */
.crown { color: var(--brass-300); font-size: 0.85rem; flex: none; }
.crown-slot { width: 0.95rem; flex: none; }

.board-note { color: var(--muted); font-size: 0.76rem; padding: 0.8rem 0.6rem; }

/* ------------------------------------------------------------- connection
 *
 * The state of the socket, shown only when it has been down long enough to be
 * felt. It is fixed and out of flow on purpose: a message about the connection
 * must never reflow the board, because the board moving is a far bigger event
 * than the thing being reported. Quiet by §3.1 — this is not an error, it is
 * the client doing its job, and it says so in the same brass the rest of the
 * interface uses for its own voice.
 */
/* AT THE FOOT, CENTRED. It used to sit at the top, where the logo is and where
   a page's own title arrives — a connection warning has no business competing
   with either. At the bottom it is out of the way of the page and still the
   first thing seen when the table goes quiet. */
.link-notice {
  position: fixed;
  left: 50%;
  bottom: calc(14px + env(safe-area-inset-bottom));
  transform: translate(-50%, 0.5rem);
  z-index: 60;
  padding: 0.34rem 0.8rem;
  border: 1px solid var(--line-lit);
  border-radius: 999px;
  background: #221609e6;
  backdrop-filter: blur(6px);
  color: var(--brass-300);
  font-size: 0.76rem;
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 180ms ease, transform 180ms ease, visibility 180ms;
}
.link-notice.on { opacity: 1; visibility: visible; transform: translate(-50%, 0); }

/* A table that has ended is not a wait, and should not read like one. */
.link-notice.stuck { color: var(--south-ink); border-color: var(--south); }

@media (prefers-reduced-motion: reduce) {
  .link-notice { transition: opacity 180ms ease, visibility 180ms; transform: translate(-50%, 0); }
}


/* ======================================================== the board skin
   The match screen as the board survey: every control floating clear of the
   squares, and the board itself flat and numbered so a rule can be read
   straight off it.

   ONE board, every screen. The skin lives here at base, outside any media
   query, so a phone and a desk show exactly the same table — and the board
   sizes itself to its container with CSS alone, no fixed frame and no fitting
   transform. Only the composition around it changes with width: a desk floats
   the controls in the gutters, a phone stacks them in a column.

   Wood and smoked glass read beautifully and cost contrast, which is the one
   thing a numbered board cannot spare. The frame survives as a thin edge so
   the table is still an object; the surface underneath goes quiet. */

.table-frame {
  position: relative;
  padding: clamp(16px, 4.4vw, 26px);
  border-radius: 26px;
  /* --turn-dur / --settle-dur: the rotation's timing, divided by the
     fast-forward setting from the script. */
  /* Warm turned wood, lit from the upper left, worn dark at the foot. */
  background:
    linear-gradient(155deg, #7a5527 0%, #5c3d18 26%, #462c10 58%, #2e1c09 100%);
  box-shadow:
    0 2px 0 #b98f4a44 inset, 0 -8px 18px #00000066 inset,
    0 26px 60px #000000aa, 0 8px 18px #00000088;
}
.table-frame::after {
  inset: clamp(6px, 1.6vw, 11px);
  border-radius: 15px;
  border-color: #b98f4a55;
}

/* Woven-inspired trim inlaid along each edge of the wood — colour-blocked
   stripes in the table's own palette. §5.1: never labelled Kente until a
   cloth is licensed. */
.weave-trim {
  position: absolute; z-index: 1; pointer-events: none;
  border-radius: 3px;
  opacity: 0.92;
  box-shadow: 0 0 0 1px #00000066, 0 1px 3px #00000088;
}
.weave-n, .weave-s {
  left: 16%; right: 16%; height: 7px;
  background: repeating-linear-gradient(90deg,
    #c79537 0 10px, #14100a 10px 13px, #1e6a3e 13px 23px, #14100a 23px 26px,
    #a44735 26px 36px, #14100a 36px 39px, #28567e 39px 49px, #14100a 49px 52px);
}
.weave-e, .weave-w {
  top: 16%; bottom: 16%; width: 7px;
  background: repeating-linear-gradient(180deg,
    #c79537 0 10px, #14100a 10px 13px, #1e6a3e 13px 23px, #14100a 23px 26px,
    #a44735 26px 36px, #14100a 36px 39px, #28567e 39px 49px, #14100a 49px 52px);
}
.weave-n { top: 3px; }
.weave-s { bottom: 3px; }
.weave-e { right: 3px; }
.weave-w { left: 3px; }

.table-glass {
  position: relative;
  border-radius: 10px;
  /* The grout between the parchment squares reads as dark lacquered wood. */
  background: #241708;
  box-shadow: 0 0 0 2px #00000066, 0 4px 12px #00000066 inset;
}
/* The pane over the table: one diagonal sheen, §12.6's cheap Fresnel. */
.table-glass::after {
  content: "";
  position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(118deg, #ffffff1f 0%, #ffffff08 24%, transparent 42%),
    radial-gradient(130% 90% at 82% -10%, #ffffff14 0%, transparent 50%);
}
.glass-sheen { display: none; }

/* Parchment track on the lacquer, the lanes in solid seat colour — the
   reference art's palette, minus its numbers. */
.cell { fill: #e7d7b4; stroke: #a6906244; stroke-width: 0.04; }
.cell.entry-south { fill: color-mix(in srgb, var(--south) 55%, #e7d7b4); }
.cell.entry-west  { fill: color-mix(in srgb, var(--west) 55%, #e7d7b4); }
.cell.entry-north { fill: color-mix(in srgb, var(--north) 55%, #e7d7b4); }
.cell.entry-east  { fill: color-mix(in srgb, var(--east) 55%, #e7d7b4); }
.lane-south { fill: color-mix(in srgb, var(--south) 88%, #f2e7ce); }
.lane-west  { fill: color-mix(in srgb, var(--west) 88%, #f2e7ce); }
.lane-north { fill: color-mix(in srgb, var(--north) 88%, #f2e7ce); }
.lane-east  { fill: color-mix(in srgb, var(--east) 88%, #f2e7ce); }

/* The heart of the table: dark leather, one turned brass pin. */
.finish { fill: #2e1d0d; stroke: #17100733; stroke-width: 0.06; }
.brass-pin { fill: url(#gradBrass); stroke: #4a350f; stroke-width: 0.03; pointer-events: none; }
.brass-pin-hi { fill: #ffffff; opacity: 0.35; pointer-events: none; }

/* The corners as posters: rich colour panels behind the pieces, each with a
   double gold trim like a framed card. */
.yard { stroke: #00000055; stroke-width: 0.045; }
.yard-south { fill: color-mix(in srgb, var(--south) 82%, #1c0a06); }
.yard-west  { fill: color-mix(in srgb, var(--west) 82%, #06180c); }
.yard-north { fill: color-mix(in srgb, var(--north) 82%, #120b04); }
.yard-east  { fill: color-mix(in srgb, var(--east) 74%, #241804); }
.yard-trim { fill: none; stroke: #e4c276aa; stroke-width: 0.06; pointer-events: none; }
.yard-trim-fine { fill: none; stroke: #e4c27655; stroke-width: 0.025; pointer-events: none; }

/* The pieces rest in a shaded well pressed into the poster, not a painted
   disc: the seat's colour already owns the corner. */
.home-disc { fill: #000000; fill-opacity: 0.2; stroke: #00000066; stroke-width: 0.06; }
.home-disc-inner { fill: #000000; opacity: 0.16; }

/* The rosette and the routed inlay stay quiet on this skin. */
.rosette, .track-inlay { display: none; }

/* The stage wrapper is a relic of the fitted-and-transformed board, kept in
   the markup so the DOM the code addresses keeps its shape, and kept out of
   layout on every screen. */
.stage { display: contents; }

/* The desk's canvas: the grid ground, and the board centred in the space the
   dock leaves. Sizing is the container's alone — as tall as that space allows,
   no wider than the card gutters allow. */
@media (min-width: 1000px) {
  #canvas { overflow: visible; }

  /* Square, and as large as the header and the panel leave room for. */
  .table-frame { width: var(--board-max); }
}


/* ================================================== the phone, two squares
   The table and the dice section share the screen, always both visible: the
   square board on top, the dice section beneath it at exactly the board's
   width, and nothing to scroll to reach the die.

   The board pays for this: its side is the smaller of the screen's width and
   whatever height the dice section leaves, so on a short phone the table
   shrinks rather than pushing the die below the fold. The dice rows are fixed;
   the talk-and-log card is the one flexible row, and it absorbs whatever
   remains — down to nothing on the shortest screens, where chat costs a scroll
   inside its own card and the game costs none. */
@media (max-width: 999px) {
  .match {
    height: 100dvh;
    overflow: hidden;
    grid-template-rows: auto minmax(0, 1fr);
    align-content: start;
    /* 24rem holds the turn banner, the state line, the readout, the die's
       resting patch of felt and the menu beneath it. The readout stays in the
       budget because the prompt that asks "which piece" lives there, and a
       layout that can clip it would eat the game's own questions. */
    --board-side: min(100vw - 1.6rem, calc(100dvh - 24rem));
    padding-bottom: calc(3.4rem + env(safe-area-inset-bottom));
  }

  .table-frame { width: var(--board-side); }

  .controller {
    width: var(--board-side);
    margin-inline: auto;
    min-height: 0;
    display: grid;
    gap: 0.55rem;
    align-content: start;
    overflow: hidden;
  }

  /* The felt loses a little depth on a phone: the flick reads the gesture's
     vector, not its length, so a shallower runway costs nothing the thumb
     notices — and the depth it frees keeps the readout on screen. */
  /* The banner and the gaps pay the menu's rent, not the board: YOUR TURN at
     phone size still shouts at 1.3rem, and the die must never share a pixel
     with the readout that explains it. */
  .match { gap: 0.7rem; }
  .turn-label { font-size: 1.3rem; }
  .turn { padding: 0; }

  /* On the shortest phones the board cedes width, and everything beneath must
     follow it down: a grid child's min-content floor would otherwise hold the
     tray at the width of its longest uppercase line and overflow the column. */
  /* The desk's grouping boxes are desk furniture. On a phone they dissolve so
     the turn, the die's words and the pool sit in the controller's own column
     exactly as they did before the desk had a panel. */
  .turn-panel, .throw-stack, .pool-block { display: contents; }
  .die-slot, .pool-caption, .pool-note { display: none; }

  .controller > * { min-width: 0; max-width: 100%; }
  .panel-tabs .tab { padding: 0.4rem 0.5rem; font-size: 0.66rem; }
  .tray-hint, .stop-throwing { white-space: normal; }
}

/* ================================================== the match menu & drawer
   Four items — Home leaves the game; chat, standings and profile open as
   drawers over it. The bar lives where the pointing hand lives: fixed to the
   bottom on a phone, a floating pill at the top on a desk. The drawer comes
   from the same edge as the bar, so the UI always opens out of the menu that
   summoned it. */
.match-nav {
  position: fixed; z-index: 40;
  display: flex; align-items: stretch;
  transition: transform 0.38s cubic-bezier(0.32, 0.72, 0, 1);
  background: linear-gradient(180deg, #241708f2, #170e05f2);
  border: 1px solid var(--line);
  backdrop-filter: blur(10px);
}
/* The class display above beats the UA's [hidden] rule — the same trap the
   ask scrim fell into — so hidden is spelled out. */
.match-nav[hidden] { display: none; }
.nav-item {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 0.45rem 0.9rem 0.4rem;
  border: 0; background: transparent; color: var(--muted);
  font: inherit; font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
}
.nav-item .nav-icon { font-size: 1.05rem; line-height: 1; }
.nav-item:hover, .nav-item[aria-expanded="true"] { color: var(--cream-100); }
.nav-item[aria-expanded="true"] .nav-icon { color: var(--brass-300); }

/* Sheet behaviour in the iOS idiom the shadcn/Vaul drawer codifies: the sheet
   rides its transform with the sheet-easing curve, the scrim fades, and both
   stay mounted so the exit plays as an animation rather than a disappearance —
   visibility flips only after the transform has finished travelling. */
.drawer-scrim {
  position: fixed; inset: 0; z-index: 41;
  background: #000000cc;
  opacity: 0; visibility: hidden;
  transition: opacity 0.3s ease, visibility 0s 0.3s;
}
.drawer-scrim.open {
  opacity: 1; visibility: visible;
  transition: opacity 0.3s ease;
}
.drawer {
  position: fixed; z-index: 42;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, #291b0c, #1c1208);
  border: 1px solid var(--line-lit);
  box-shadow: 0 -18px 50px #00000088;
  visibility: hidden;
  transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1), visibility 0s 0.32s;
}
.drawer.open {
  visibility: visible;
  transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}
.drawer.dragging { transition: none; }
.drawer-handle {
  flex: none;
  width: 100px; height: 8px; border-radius: 999px;
  background: #5c4526;
  margin: 14px auto 0;
  touch-action: none; cursor: grab;
}
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.7rem 1rem 0.5rem;
}
.drawer-head h2 {
  margin: 0; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--brass-300);
}
.drawer-pane {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  padding: 0 1rem 1rem;
}
.drawer-pane[hidden] { display: none; }
.drawer-pane .tab-body { flex: 1; min-height: 0; overflow: hidden; }
.drawer-pane .tab-pane.on { height: 100%; display: flex; flex-direction: column; }
.drawer-pane .chat, .drawer-pane .log,
.drawer-pane .board-table { flex: 1; min-height: 0; overflow-y: auto; }

.drawer-settings { display: grid; gap: 12px; padding-bottom: 10px; }

/* the phone: a floating pill riding above the screen's foot, the content
   dying away into a fade beneath it so the pill reads as hovering over the
   game rather than fencing it — and the sheet still rises from the true edge,
   over pill and fade alike */
@media (max-width: 999px) {
  .match-nav {
    left: 50%; transform: translateX(-50%);
    bottom: calc(10px + env(safe-area-inset-bottom));
    border-radius: 999px;
    padding: 0.25rem 0.4rem;
    box-shadow: 0 10px 30px #00000088;
  }
  /* During play the pill lives below the screen's edge; gameplay owns the
     felt. The chevron above is what calls it back. */
  .match-nav.tucked {
    transform: translateX(-50%)
      translateY(calc(100% + 24px + env(safe-area-inset-bottom)));
    pointer-events: none;
  }
  .nav-item { padding: 0.4rem 0.85rem 0.35rem; border-radius: 999px; }

  /* The felt's bottom fade belongs to the match, where the chat pill and the
     speed control float over the board. The desk pages have nothing at their
     foot to lift off the page, and a mask there was just a dark band. */
  #matchScreen::after {
    content: "";
    position: fixed; left: 0; right: 0; bottom: 0;
    height: 108px;
    background: linear-gradient(180deg, transparent, #051b10f0 82%);
    pointer-events: none;
    z-index: 39;                       /* under the pill, under the scrim */
  }
  .drawer {
    left: 0; right: 0; bottom: 0;
    max-height: 78dvh;
    border-radius: 10px 10px 0 0;
    border-width: 1px 0 0;
    padding-bottom: env(safe-area-inset-bottom);
    transform: translateY(100%);
  }
  .drawer.open { transform: translateY(0); }
  /* The talk drawer takes a fixed height, so the transcript gets a real
     scrollport and the input stays pinned at the foot however much has been
     said. The sheet used to size itself to its words, which left nothing to
     scroll and let a tall composer push the list out of reach. */
  .drawer:has(.drawer-pane[data-drawer="chat"]:not([hidden])) { height: 78dvh; }
}

/* the desk: the drawer element in sidebar dress — a panel docked to the right
   edge, the tab strip at its head, open for the whole match. No scrim, no
   handle, no title bar: a permanent fixture needs no dismissal furniture. */
@media (min-width: 1000px) {
  .drawer {
    top: 136px; left: 18px; bottom: 18px; right: auto;
    width: var(--col);
    max-height: none;
    border-radius: 14px;
    transform: none; opacity: 1;
    transition: none;
  }
  .drawer.open { transform: none; }
  .drawer-handle,
  .drawer-head,
  .drawer .panel-tabs,
  .drawer-scrim { display: none; }
  .drawer-pane { padding-top: 0.9rem; }
}

/* On the phone, Home on the menu is the way out and a second exit would be
   the same door twice — arrow and spacer both go, and the badge sits dead
   centre. On the desk the arrow IS home, attached to the header. */
@media (max-width: 999px) {
  #matchScreen .controller-top [data-back],
  #matchScreen .controller-top .ghosted { display: none; }
}
#matchScreen .controller-top { justify-content: center; }


/* --------------------------------------------------------------- the summons
   A quiet tab at the screen's foot: chevron up calls the menu, and once the
   menu is up the same tab points down to send it home. Phone-and-match only —
   the desk's tools live in its header. */
.nav-reveal {
  position: fixed; z-index: 39;
  left: 50%; transform: translateX(-50%);
  bottom: calc(4px + env(safe-area-inset-bottom));
  width: 52px; height: 26px;
  display: grid; place-items: center;
  border: 0; border-radius: 999px 999px 0 0;
  background: #241708b8; color: var(--muted);
  cursor: pointer;
  transition: opacity 0.3s;
}
.nav-reveal[hidden] { display: none; }
.nav-reveal:hover { color: var(--cream-100); }
.nav-reveal .chev { width: 18px; height: 18px; transition: transform 0.3s; }
.nav-reveal.open .chev { transform: rotate(180deg); }
@media (min-width: 1000px) { .nav-reveal { display: none; } }

/* ============================================================ notifications
   Words from the table, above the menu: newest nearest the bar, three deep,
   dismissable, and a tap opens the chat they belong to. */
.notif-stack {
  position: fixed; z-index: 45;
  /* A pile, not a column: every card in the same grid cell, the newest on
     top and the older ones peeking out behind it. However fast the table
     talks, the pile is one card tall — it can never grow across the die. */
  display: grid;
  width: min(92vw, 340px);
  pointer-events: none;
}
.notif-stack .notif { grid-area: 1 / 1; }
.notif-stack .notif:nth-child(1) { z-index: 3; }
.notif-stack .notif:nth-child(2) { z-index: 2; opacity: 0.75; }
.notif-stack .notif:nth-child(3) { z-index: 1; opacity: 0.5; }
/* Only the top card takes the tap: the ones behind are a depth cue, and a
   press landing on a peeking edge going to a half-hidden card would read as
   the top one misfiring. */
.notif-stack .notif:not(:nth-child(1)) { pointer-events: none; }
.notif {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: 10px;
  text-align: left;
  background: linear-gradient(180deg, #291b0cf2, #1c1208f2);
  border: 1px solid var(--line-lit); border-radius: 12px;
  padding: 0.55rem 0.7rem;
  color: var(--cream-100); font: inherit; font-size: 0.8rem;
  box-shadow: 0 14px 34px #00000077;
  cursor: pointer;
  animation: notif-in 200ms ease-out;
}
.notif b { color: var(--brass-300); display: block; font-size: 0.72rem; }
.notif .name-chip { margin-bottom: 3px; }
.notif .notif-body { flex: 1; display: grid; gap: 1px; }
.notif .notif-x { color: var(--muted); padding: 0 2px; font-size: 0.8rem; }
.notif .notif-x:hover { color: var(--cream-100); }
@keyframes notif-in { from { transform: translateY(8px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .notif { animation: none; } }

@media (max-width: 999px) {
  /* Over the menu pill, not the felt: the die's patch now runs down to the
     nav, so the one band a card can cover without touching gameplay is the
     chrome itself. A word from the table borrows the menu's spot for eight
     seconds; it never sits over the die. */
  /* (The notification cards retired: the chat bar carries the table's voice.) */
  .notif-stack .notif:nth-child(2) { transform: translateY(7px) scale(0.955); }
  .notif-stack .notif:nth-child(3) { transform: translateY(14px) scale(0.91); }
}


/* ============================================================= the question
   One question at a time, in the game's own dress. The scrim takes a "no" —
   tapping away is declining — and the destructive answer sits on the right,
   named for what it does. */
.ask-scrim {
  position: fixed; inset: 0; z-index: 60;
  background: #000000cc;
  display: grid; place-items: center;
  padding: 1.2rem;
}
/* The class's display beats the UA's [hidden] rule, so hidden must win here
   explicitly — without this the empty question box sat over the whole game,
   its scrim eating every click, which is exactly what shipped. */
.ask-scrim[hidden] { display: none; }
.ask {
  width: min(92vw, 360px);
  background: linear-gradient(180deg, #291b0c, #1c1208);
  border: 1px solid var(--line-lit); border-radius: 14px;
  padding: 1.1rem 1.1rem 0.9rem;
  box-shadow: 0 24px 60px #000000aa;
  animation: notif-in 160ms ease-out;
}
.ask p { margin: 0 0 0.9rem; font-size: 0.92rem; line-height: 1.5; }
.ask-row { display: flex; gap: 0.6rem; justify-content: flex-end; }
.ask-row .btn { min-width: 92px; }

/* --- the winner's moment ------------------------------------------------
   The same house dialog, dressed for the one occasion the table should stop
   for. Brass and the display face, because this is the sentence the whole
   match was for — and the [hidden] rule spelled out, because a class that
   sets display beats the UA's and that mistake once left a dead scrim over
   the deployed game. */
.win-scrim {
  position: fixed; inset: 0; z-index: 61;
  background: #000000d0;
  display: grid; place-items: center;
  padding: 1.2rem;
}
.win-scrim[hidden] { display: none; }
.win {
  width: min(92vw, 380px); text-align: center;
  background:
    radial-gradient(120% 80% at 50% 0%, #1c3350 0%, #291b0c 55%, #1c1208 100%);
  border: 1px solid var(--line-lit); border-radius: 16px;
  padding: 1.4rem 1.2rem 1rem;
  box-shadow: 0 24px 60px #000000aa, 0 0 0 1px #e8c98a22;
  animation: notif-in 200ms ease-out;
}
.win-rank {
  color: var(--brass-300); font-size: 0.7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.18em;
}
.win-who {
  font-family: "Georgia", "Iowan Old Style", serif;
  font-size: 1.65rem; line-height: 1.15; margin-top: 0.35rem;
  color: var(--cream-100);
  overflow-wrap: anywhere;
}
/* The order so far, once there is more than one name in it. */
.win-order {
  list-style: none; margin: 0.9rem 0 0; padding: 0;
  display: grid; gap: 0.3rem;
  text-align: left;
}
.win-order:empty { display: none; }
.win-order li {
  display: flex; align-items: baseline; gap: 0.55rem;
  padding: 0.32rem 0.55rem;
  border: 1px solid var(--line); border-radius: 8px;
  font-size: 0.86rem; color: var(--cream-100);
}
.win-order li .place {
  color: var(--brass-300); font-size: 0.66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  min-width: 3.4rem;
}
.win-note {
  margin: 0.9rem 0 0; color: var(--muted);
  font-size: 0.78rem; line-height: 1.5;
}
.win-note:empty { display: none; }
.win-row {
  display: flex; gap: 0.6rem; justify-content: center; margin-top: 1rem;
}
.win-row .btn { min-width: 108px; }



/* ==================================================================== the desk
   Two zones and nothing else: the board, and the turn beside it. The old desk
   was a dashboard — a standing chat column, a tab bar, a board tab for the
   board you were already looking at — and it read as an application with a
   game inside it rather than a table. Everything that is not the board or the
   turn now lives behind the header, one drawer deep.

   The board leads. The turn panel is sized to the words it has to hold and no
   wider, so the remaining width belongs to the table. */

.desk-avatar, .head-actions, .wordmark { display: none; }

@media (min-width: 1000px) {
  .match-nav { display: none; }

  .match {
    --gutter: 28px;
    --panel: clamp(280px, 24vw, 360px);
    --head: 62px;
    /* The board takes the screen. It is bounded only by what is actually in
       its way — the header's line above it and the panel beside it — and by
       nothing else: a fixed ceiling was leaving a large screen mostly empty
       felt with a small table stranded in the middle of it. */
    --board-max: min(
      calc(100dvh - var(--head) - 2 * var(--gutter)),
      calc(100vw - var(--panel) - 3 * var(--gutter)));

    position: relative;
    height: 100dvh; overflow: hidden;
    display: grid;
    /* The board sizes itself and the panel takes its measure; the pair is
       centred together. Splitting the width by proportion instead would push
       them apart as the screen grew, which is distance, not layout. */
    grid-template-columns: auto var(--panel);
    grid-template-rows: var(--head) minmax(0, 1fr);
    justify-content: center; align-items: stretch;
    column-gap: var(--gutter);
    /* Full bleed. The content inside is centred and bounded; the felt is not
       bounded by anything, because it is the table the game is played on. */
    max-width: none; margin: 0;
    padding: 0 var(--gutter) var(--gutter);
    gap: 0 var(--gutter);
  }

  /* .controller is display:contents on the desk, so the header and the panel
     land in the grid themselves. */
  .controller { display: contents; }

  /* The line reads from both ends: the way out and the game's name on the
     left, the tools gathered into a pill on the right. Nothing in the middle,
     because the middle belongs to the board. */
  .controller-top {
    grid-column: 1 / -1; grid-row: 1;
    position: static; transform: none; z-index: 5;
    justify-self: stretch; align-self: center;
    display: flex; align-items: center; gap: 0.45rem;
    background: none; border: 0; box-shadow: none; backdrop-filter: none;
    padding: 0; width: auto; max-width: none;
  }
  .controller-top .icon-btn {
    width: 32px; height: 32px; border-radius: 999px;
    display: grid; place-items: center;
  }
  #matchScreen .controller-top { justify-content: flex-start; }
  .controller-top .icon-btn { background: transparent; border: 0; }
  /* On the desk the Help control says this in fewer words. */
  .controller-top .match-conditions { display: none; }

  #matchScreen .wordmark {
    display: block;
    font-size: 1.15rem; letter-spacing: 0.01em;
    margin: 0 0.2rem 0 0; padding: 0;
  }

  /* The pill the phone's menu wears, holding the desk's tools. */
  .head-actions {
    display: flex; align-items: center; gap: 0.15rem; margin-left: auto;
    background: linear-gradient(180deg, #241708f2, #170e05f2);
    border: 1px solid var(--line); border-radius: 999px;
    box-shadow: 0 10px 30px #00000066; backdrop-filter: blur(10px);
    padding: 0.25rem 0.3rem;
  }
  .head-btn {
    border: 0; background: transparent; border-radius: 999px;
    color: var(--muted); font: inherit; font-size: 0.72rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.09em;
    padding: 0.42rem 0.8rem; cursor: pointer;
  }
  .head-btn:hover { color: var(--cream-100); background: #ffffff10; }
  .head-btn.on { color: var(--brass-300); }
  .head-btn .dot { font-size: 0.7em; margin-left: 0.35em; }

  .desk-avatar {
    display: grid; place-items: center;
    position: static; margin-left: 0.3rem;
    width: 30px; height: 30px; border-radius: 50%; font-size: 0.72rem;
    border: 1px solid var(--line-lit); cursor: pointer;
  }

  .table-area { grid-column: 1; grid-row: 2; }

  /* ------------------------------------------------------- the turn panel
     Whose turn, the die, the words, the numbers — read top to bottom in the
     order they are used, on one continuous stack. No card, no border: it is
     part of the table, not a window sitting on it. */
  /* §20: whose turn it is starts on the board's own top line, so the two
     columns begin together. The throw — die, its words, the numbers in hand —
     is one piece, and it sits in the middle of the panel's height, level with
     the middle of the board it is played onto. */
  .turn-panel {
    grid-column: 2; grid-row: 2;
    display: grid;
    grid-template-rows: auto 1fr auto;
    justify-items: center; align-items: start;
    min-height: 0; overflow: hidden auto;
    padding-top: 0.5rem;
    text-align: center;
  }
  .turn-panel .turn { grid-row: 1; }
  .throw-stack {
    grid-row: 2;
    align-self: center;
    display: flex; flex-direction: column; align-items: center;
    gap: 0.9rem; width: 100%;
  }

  .turn-panel .turn {
    position: static; transform: none;
    background: none; border: 0; box-shadow: none; backdrop-filter: none;
    padding: 0; width: 100%; text-align: center;
  }
  .turn-label { font-size: 1.45rem; }
  .turn-who { overflow-wrap: anywhere; }
  .turn-owner {
    margin-top: 0.5rem; padding-top: 0.5rem;
    border-top: 1px solid var(--line);
    color: var(--muted); font-size: 0.74rem;
    text-transform: uppercase; letter-spacing: 0.09em;
  }
  .turn-owner strong { color: var(--cream-100); display: block; letter-spacing: 0.06em; }

  /* The die's reserved place in the stack. The die itself is fixed to the
     throw surface — it has to be able to travel across the whole felt — so
     this holds the space and the layout puts the die over it.

     §33 puts the throw first in the order of things a player reaches for, and
     on a desk there is room to say so: the die is bigger here than on a phone,
     and it sits directly under the turn it belongs to. */
  .die-slot { flex: none; width: 100%; height: 110px; }
  .die { width: 84px; height: 84px; border-radius: 17px; }

  .turn-panel .tray {
    position: static; transform: none; width: 100%; max-width: none; margin: 0;
  }

  .pool-block { width: 100%; display: grid; gap: 0.5rem; }
  .pool-block[hidden] { display: none; }
  .pool-caption {
    color: var(--muted); font-size: 0.68rem;
    text-transform: uppercase; letter-spacing: 0.12em;
  }
  .turn-panel .pool {
    position: static; transform: none;
    flex-direction: row; flex-wrap: wrap; justify-content: center;
    gap: 0.5rem;
  }
  .pool-note { color: var(--muted); font-size: 0.76rem; line-height: 1.45; }
  .pool-note:empty { display: none; }

  /* ------------------------------------------------------------ the drawer
     Chat, log, standings and the rules, one drawer deep and overlaying the
     board rather than compressing it: a drawer that shrank the table would
     cost the game the very thing this redesign is protecting. */
  .drawer {
    top: 0; right: 0; bottom: 0; left: auto;
    width: min(400px, 92vw); max-height: none;
    border-radius: 14px 0 0 14px;
    border-width: 1px 0 1px 1px;
    transform: translateX(100%); opacity: 1;
    transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1), visibility 0s 0.32s;
  }
  .drawer.open { transform: translateX(0); }
  .drawer-handle { display: none; }   /* a swipe affordance is a touch affordance */

  /* The header's empty middle — below the brand, over nothing else: not the
     turn heading, not the die, not the board's squares. */
  .notif-stack {
    top: 64px; bottom: auto; left: 50%; right: auto;
    transform: translateX(-50%); width: min(340px, 30vw);
  }
  /* Anchored at its top edge: the older cards peek downward behind. */
  .notif-stack .notif:nth-child(2) { transform: translateY(9px) scale(0.955); }
  .notif-stack .notif:nth-child(3) { transform: translateY(18px) scale(0.91); }
}

/* The laptop gives the board a little back: the panel narrows to its floor
   before the table starts paying for it. */
@media (min-width: 1000px) and (max-width: 1279px) {
  .match { --panel: 268px; --gutter: 20px; }
  .die-slot { height: 96px; }
  .die { width: 72px; height: 72px; border-radius: 15px; }
  .turn-label { font-size: 1.25rem; }
  .die-slot { height: 112px; }
}

/* The rules, read off the match rather than written out here. */
.rules-body { display: grid; gap: 0.9rem; padding-bottom: 0.8rem; }
.rules-line { display: grid; gap: 0.15rem; }
.rules-line .k {
  color: var(--muted); font-size: 0.68rem;
  text-transform: uppercase; letter-spacing: 0.1em;
}
.rules-line .v { color: var(--cream-100); font-size: 0.9rem; }
.rules-note { color: var(--muted); font-size: 0.8rem; line-height: 1.5; }

/* ================================================================ the real die
   The physical die: one transparent WebGL stage over the whole match screen.
   The die rests big in the hand at the tray and is thrown ONTO the board —
   smaller with distance, the camera's real perspective. The canvas never
   takes a pointer; the resting die's touch surface is the #die pad, so the
   board underneath stays fully tappable while the die is anywhere at all. */
.die3d-canvas {
  position: fixed; inset: 0;
  width: 100vw; height: 100dvh;
  /* The die is match furniture: its stage exists on every screen (it is a
     body-level overlay), so it hides itself whenever the match does. */
  display: none;
  pointer-events: none;
  z-index: 35;                 /* over the table, under menu/drawers/cards */
}
body:has(#matchScreen:not(.hidden)) .die3d-canvas { display: block; }
.die.physics {
  background: none; box-shadow: none; border-radius: 0;
  display: block; padding: 0;
  cursor: grab;
  touch-action: none;          /* the drag IS the interaction — never scroll */
}
/* The dice pad: a stitched round of darker felt under the resting die, the
   reference's throwing circle. Drawn on the touch pad, which is exactly
   where the 3D die's home anchor unprojects — an ellipse, because the pad
   lies flat on a table seen at an angle. */
.die.physics::before {
  content: "";
  position: absolute; left: 4%; right: 4%; top: 26%; bottom: 14%;
  border-radius: 50%;
  background:
    radial-gradient(ellipse at 50% 38%, #14522f 0%, #0e3c22 55%, #092a17 100%);
  border: 2px solid #c2903c66;
  box-shadow:
    0 0 0 6px #00000030 inset, 0 2px 0 #ffffff14 inset,
    0 10px 24px #00000066;
}
.die.physics:active { cursor: grabbing; }
.die.physics.disabled { opacity: 0.55; }

/* ============================================================ name chips
   The board carries no names, so wherever the interface says one, it sits on
   its seat's colour — the turn banner large, chat and notifications small.
   One element teaches name and colour as a single thing. */
.name-chip {
  display: inline-block;
  padding: 0.3rem 0.95rem;
  border-radius: 999px;
  background: var(--ink-800); color: var(--cream-100);
  font-weight: 800; letter-spacing: 0.02em;
  /* A gold rim, like the reference's enamelled plate. */
  border: 1.5px solid #d9b05e99;
  box-shadow: 0 2px 8px #00000055, 0 1px 0 #ffffff2e inset;
}
/* White on every seat colour, by ruling. The gold seat is the pale one, so
   the shadow under the letters is what keeps white legible there. */
.name-chip.south,
.name-chip.west,
.name-chip.north,
.name-chip.east { color: #fff; text-shadow: 0 1px 2px #00000059; }
.name-chip.south { background: var(--south); }
.name-chip.west  { background: var(--west); }
.name-chip.north { background: var(--north); }
.name-chip.east  { background: var(--east); }
.name-chip.sm {
  padding: 0.12rem 0.55rem; font-size: 0.7rem;
  box-shadow: none; margin-bottom: 2px;
}
.turn-who .name-chip { font-size: 1.02rem; }
.msg .name-chip { align-self: flex-start; }

/* ============================================================== open talk
   The input row in the messenger idiom: a pill of an input, a round send
   button, a counter that only speaks up when the cap is close. */
.chat-input-row { display: flex; gap: 6px; align-items: center; }
.chat-input {
  flex: 1; min-width: 0;
  background: #1a1108; border: 1px solid var(--line); border-radius: 999px;
  color: var(--cream-100); font: inherit; font-size: 0.85rem;
  padding: 0.55rem 0.9rem;
}
.chat-input:focus { outline: none; border-color: var(--brass-300); }
.chat-count { color: var(--muted); font-size: 0.7rem; text-align: right; }
.chat-count:empty { display: none; }
.chat-send {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  border: 0; cursor: pointer;
  background: var(--action-lit); color: #fff;
  font-size: 0.85rem; line-height: 1;
  display: grid; place-items: center;
  padding-left: 3px;      /* the glyph leans left; centre it optically */
}
.chat-send:disabled { opacity: 0.4; cursor: default; }

/* ================================================================ the brand
   The game's name, fixed top centre over every screen — the one piece of
   chrome that never moves. Decorative and untouchable: no pointer, no
   layout. Last in the file on purpose, so its overrides win the cascade. */
#brandHeader {
  position: fixed;
  top: calc(14px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  width: 116px;
  z-index: 38;              /* over the die's stage, under menus and sheets */
  pointer-events: none;
  filter: drop-shadow(0 3px 6px #000000aa);
}
#brandHeader img { display: block; width: 100%; height: auto; }

/* One brand, one place: the wordmarks the screens used to carry stand down.
   The auth screen keeps its hero — that one is the splash, not chrome. */
.topbar .wordmark,
#matchScreen .controller-top .wordmark { display: none; }

/* The auth screen's old hero wordmark stood down when the desk redesign
   hid wordmarks globally; the fixed brand is the name everywhere now. */
#authScreen .auth-brand { padding-top: calc(52px + env(safe-area-inset-top)); }

/* The phone's board starts below the brand rather than under it. */
@media (max-width: 999px) {
  .match { padding-top: calc(104px + env(safe-area-inset-top)); }
}

/* Every screen sits on the same table the match is played on: warm wood
   above, giving way to the felt. One world, not a menu system and a game. */
body {
  background:
    linear-gradient(180deg, #2b1a0a 0%, #221507 30%, #1a1208b0 55%, transparent 75%),
    radial-gradient(140% 120% at 50% 10%, #17452c 0%, #123723 55%, #0a2415 100%);
}
.screen:not(.match):not(.auth) { padding-top: calc(64px + env(safe-area-inset-top)); }

/* Quick answers on a talk notification: react in one tap, or drop straight
   into the composer already quoting the speaker. */
.notif-quick { display: flex; gap: 6px; margin-top: 6px; }
.notif-quick-btn {
  background: #ffffff10; border: 1px solid var(--line-lit);
  border-radius: 999px; padding: 0.14rem 0.55rem;
  font: inherit; font-size: 0.82rem; line-height: 1.3;
  color: var(--cream-100); cursor: pointer;
}
.notif-quick-btn:hover { border-color: var(--brass-300); background: #ffffff1c; }
.notif-reply { font-size: 0.72rem; font-weight: 700; color: var(--brass-300); }

/* A talk card follows a sideways finger; vertical stays with the page. */
.notif { touch-action: pan-y; }

/* Sound on or off: one small round button, fixed where the thumb lives.
   Above the felt and the die's stage, below sheets and menus. */
.mute-btn {
  position: fixed;
  right: calc(12px + env(safe-area-inset-right));
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 39;
  width: 42px; height: 42px;
  border-radius: 50%;
  border: 1px solid var(--line-lit);
  background: linear-gradient(180deg, #241708f2, #170e05f2);
  color: var(--cream-100); font-size: 1.05rem; line-height: 1;
  display: grid; place-items: center;
  cursor: pointer;
  box-shadow: 0 8px 20px #00000077;
}
.mute-btn:hover { border-color: var(--brass-300); }
.mute-btn.off { opacity: 0.6; }

/* ======================================================== the celebration
   Woven-strip confetti over everything, and the rankings walking in. */
.confetti {
  position: fixed; inset: 0;
  z-index: 70;                /* over the winner's card; it IS the moment */
  pointer-events: none;
}

@keyframes win-row-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}
.win-order li { animation: win-row-in 0.45s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
body.still .win-order li { animation: none; }

.win-board {
  list-style: none; margin: 0.7rem 0 0; padding: 0.5rem 0 0;
  border-top: 1px solid var(--line);
  display: grid; gap: 0.25rem; text-align: left;
}
.win-board:empty { display: none; }
.win-board-row {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.26rem 0.55rem;
  font-size: 0.8rem; color: var(--cream-100);
  border-radius: 7px;
  animation: win-row-in 0.45s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
body.still .win-board-row { animation: none; }
.win-board-row .rank { color: var(--muted); min-width: 1.1em; text-align: right; }
.win-board-row .who { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.win-board-row .wins { font-weight: 700; }
.win-board-row .delta { font-size: 0.66rem; }
.win-board-row.up .delta { color: #5fb183; }
.win-board-row.down .delta { color: #d98c79; }
.win-board-row.same .delta, .win-board-row.new .delta { color: var(--muted); }
.win-board-row.up { background: #2f9d5e14; }

/* ============================================== the corner controls, redone
   Two plain white glyphs, nothing else — no pill, no border. A drop shadow
   is what keeps them readable on the felt. Later in the file than the old
   mute dress on purpose: these rules win. */
.fab {
  /* Gameplay furniture: sound and table speed mean nothing outside a match,
     so the buttons exist only while the match screen is up. Above the felt's
     bottom fade (39), which otherwise dims whatever it overlaps. */
  display: none;
  position: fixed;
  right: calc(10px + env(safe-area-inset-right));
  z-index: 40;
  width: 40px; height: 40px;
  background: none; border: 0; padding: 0;
  color: #ffffff; opacity: 0.85;
  place-items: center;
  cursor: pointer;
  box-shadow: none;
  border-radius: 0;
  filter: drop-shadow(0 2px 5px #000000cc);
}
.fab:hover { opacity: 1; border-color: transparent; }
.fab svg { width: 22px; height: 22px; display: block; }
.mute-btn {
  left: calc(10px + env(safe-area-inset-left)); right: auto;
  bottom: calc(12px + env(safe-area-inset-bottom));
}
.mute-btn.off { opacity: 0.45; }
/* Under the gear, top right: the two things that change how the table BEHAVES
   sit together, and the foot of the screen belongs to the chat bar now. */
.ff-btn {
  top: calc(14px + 38px + 10px + env(safe-area-inset-top));
  right: calc(10px + env(safe-area-inset-right));
  bottom: auto;
}
.ff-btn .ff-badge {
  position: absolute; top: 2px; right: 0;
  font-size: 0.58rem; font-weight: 800; color: #ffffff;
}
.ff-btn:not(.on) { opacity: 0.7; }

body:has(#matchScreen:not(.hidden)) .fab { display: grid; }

/* ===================================================== gameplay corners v2
   Four corners, four jobs: out (top left), you (top right), talk (bottom
   left), speed (bottom right). The old bottom menu retires from the match —
   its work is now split across the corners. Phone only; the desk keeps its
   own header. */
.back-fab { top: calc(14px + env(safe-area-inset-top)); left: calc(10px + env(safe-area-inset-left)); right: auto; }
.settings-fab {
  top: calc(14px + env(safe-area-inset-top)); right: calc(10px + env(safe-area-inset-right));
  width: 38px; height: 38px; border-radius: 50%;
  background: linear-gradient(180deg, #2a1b0cf2, #170e05f2);
  border: 1.5px solid var(--brass-300);
  color: var(--cream-100);
  opacity: 1;
}
.settings-fab svg { width: 19px; height: 19px; }
@media (min-width: 1000px) {
  .match-chrome { display: none !important; }   /* the desk header does these */
}
@media (max-width: 999px) {
  /* They live OUTSIDE the match screen's element, so they are named
     directly: the old menu and its summons are retired from gameplay. */
  .match-nav, .nav-reveal { display: none !important; }
}



/* ============================================================= the avatar
   One face everywhere it appears: uploaded picture, Google photo, or the
   initial. Pictures cover their circle. */
.has-photo { background-size: cover; background-position: center; }

.avatar-row { display: flex; align-items: center; gap: 0.8rem; }
.avatar.drawer-face {
  width: 54px; height: 54px; border-radius: 50%;
  display: grid; place-items: center;
  background-color: var(--ink-800);
  border: 1.5px solid var(--brass-300);
  font-weight: 800; font-size: 1.2rem; color: var(--cream-100);
  flex: none;
}
.avatar-side { min-width: 0; }
.avatar-side .kind { margin-top: 0.1rem; }
.avatar-actions { display: flex; gap: 0.5rem; }
.avatar-actions .btn { padding: 0.4rem 0.8rem; font-size: 0.78rem; min-height: 0; }

/* The gear rides above its own sheet and doubles as the way to close what it
   opened — the sheet still rises from the foot, like every other sheet. */
body:has(.drawer.open) .settings-fab { z-index: 43; }

/* THE CHAT BAR: the shape of the START MATCH button, dressed as the input it
   opens. Icon left, the line itself, send right. It is a button and not a real
   field on purpose — a live input at the foot of a match would take the
   keyboard every time a thumb strayed, and the drawer is where writing
   happens. */
.chat-bar {
  display: none;
  position: fixed; z-index: 40;
  left: 18px; right: 18px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  width: auto; height: 50px;
  padding: 0 6px 0 6px;
  align-items: center; gap: 10px;
  border-radius: 999px;
  background: rgba(12, 8, 3, .82);
  border: 1px solid var(--line, #ffffff1f);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .55);
  color: var(--muted);
  cursor: pointer; font: inherit; text-align: left;
}
body:has(#matchScreen:not(.hidden)) .chat-bar { display: flex; }

.chat-bar-icon {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: linear-gradient(180deg, #fdf8ec, #efe3c8);   /* the die's ivory */
  color: #33220f; overflow: hidden;
}
.chat-bar-icon svg { width: 19px; height: 19px; display: block; }
/* When a portrait takes the slot it FILLS it: the face grows to the whole
   circle, loses its ring, and the ivory behind it goes — and the image keeps
   the head-crop `.face img` gives it everywhere else. Overriding that img rule
   here was what left a whole tiny figure floating in a ring of ivory. */
.chat-bar-icon:has(.face) { background: none; }
/* Three classes deep on purpose: `.face.sm` sets a 22px box from desk.css,
   which loads after this file, so an equal-weight selector here would lose and
   the portrait would sit in the middle of the slot instead of filling it. */
.chat-bar .chat-bar-icon .face {
  width: 100%; height: 100%; font-size: 15px;
  /* The ring is the speaker's own seat colour — `.face` sets --seat-ink from
     the seat class — so who is talking is legible before the words are. */
  border: 2px solid var(--seat-ink, var(--hair-bright));
}

.chat-bar-text {
  flex: 1 1 auto; min-width: 0;
  font-size: 0.86rem; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A remark waiting to be read is the table's own voice, not placeholder grey. */
.chat-bar.said .chat-bar-text { color: var(--cream-100); white-space: normal; }

/* WITH A REMARK ON IT THE BAR GROWS. One line and an ellipsis three words in
   is a notification nobody can read, which is the job it was given. */
.chat-bar.said {
  height: auto; min-height: 68px;
  padding-top: 8px; padding-bottom: 8px;
  border-color: var(--brass-line, #c7953755);
}
.chat-bar-who {
  display: block;
  font-size: 0.68rem; font-weight: 800; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--brass-300);
  margin-bottom: 2px;
}
.chat-bar-said {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 0.86rem; line-height: 1.25;
}

.chat-bar-send {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--brass-soft, #c7953722); color: var(--brass-300);
}
.chat-bar-send svg { width: 17px; height: 17px; display: block; }
/* While somebody's words are on the bar the key answers them instead. */
.chat-bar-send.replying { background: var(--brass, #c79537); color: #1b1108; }
.chat-bar-send.replying svg { width: 19px; height: 19px; }

.chat-bar:hover { border-color: var(--brass-line, #c7953755); }

/* A piece on your own side, when seats play as one: a hairline in the cream the
   whole interface uses for "yours", outside the piece rather than on it so the
   seat's own colour is untouched. */
.piece-ally {
  fill: none; stroke: #f2e7ce; stroke-width: 0.075; opacity: .92;
  pointer-events: none;
  /* A dark seat behind the hairline, so the cream reads on the pale squares as
     well as on the felt. Cheaper than a second ring and it never doubles up. */
  paint-order: stroke;
  filter: drop-shadow(0 0 0.02px #1b110899);
}

/* The black star on the leather heart — the logo's medallion, on the felt. */
.centre-star { fill: #120c06; stroke: #000000aa; stroke-width: 0.03; pointer-events: none; }

/* The chat pill lays out in a row — its show-state must say so, or the
   generic fab grid stacks the icon over the word. */
/* (The chat is a bar now, shown by its own rule above.) */

/* A piece the current hand cannot move rests dimmer — a kicked piece done
   for the turn, a yard piece without its entry roll. The reason speaks in
   the readout when tapped. */
.piece.stuck { opacity: 0.55; }
