/* HexColony — a sea-and-parchment shell around a canvas island.
   Mobile first: everything is sized off --app-height (real innerHeight, not 100vh)
   and padded for the notch, because these games get played on phones. */

:root {
  --sea-1: #071a2c;
  --sea-2: #0b2540;
  --sea-3: #123a5c;
  --sand: #e8d6b2;
  --parch: #f3e6cb;
  --ink: #0d1b28;
  --text: #eaf1f8;
  --dim: #9db2c6;
  --line: rgba(232, 214, 178, 0.16);
  --panel: rgba(9, 28, 48, 0.86);
  --panel-2: rgba(14, 40, 66, 0.92);
  --gold: #e3ba57;
  --gold-2: #c9963a;
  /* Sky, the same blue the app icon is ringed in and the same one on the player palette
     in rules.js — so the name on the landing page, the ring round the icon on the home
     screen and a player's own colour are all one blue rather than three. */
  --sky: #35c4ff;
  --sky-2: #1f9ed6;
  --sky-ink: #04222f;   /* 8.2:1 on the top of the gradient, 5.4:1 on the bottom */
  --sky-shade: #0f5f85; /* the lip the raised buttons sit on */
  --danger: #e5484d;
  --good: #22c55e;
  /* The same two accents, taken down far enough to read on a white plate. Gold and red
     were both chosen against dark blue; on white they sit at about 1.8:1 and 3:1, which
     is a colour you can see but not a colour you can read at a glance across a table. */
  --gold-ink: #7a5a10;
  --danger-ink: #b8262b;

  --app-height: 100vh;
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);

  --r-sm: 10px;
  --r: 16px;
  --r-lg: 22px;
}

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

/* Why a tap is slower on an iPhone than on an Android.
   Safari has ignored user-scalable=no since iOS 10, deliberately, for accessibility — so
   double-tap-to-zoom stays live on anything that has not opted out, and Safari therefore
   waits about a third of a second after a tap to find out whether a second one is coming
   before it will dispatch the click. Chrome on Android drops that wait as soon as the
   viewport is width=device-width, which is why the same build feels instant on one phone
   and laggy on the other.
   Declaring manipulation says there is no double-tap gesture here, so the click goes
   through at once. Panning and pinch-zoom are untouched; only double-tap-to-zoom goes.
   The board never had this problem: the canvas sets touch-action: none and reads
   pointerup directly, which is exactly why placing a road always felt immediate and
   pressing a button did not. */
button, input, select, textarea, a,
.veil, .banner, .seat, .chip, [data-act], [data-trade], [data-say],
/* The containers that carry the delegated handlers. Strictly redundant — the effective
   touch-action for a touch is the intersection down the tree, so a button saying
   manipulation already wins over an ancestor that has not said anything — but these are
   the elements the listeners are actually bound to, and a rule you have to reason about
   the intersection algorithm to trust is a rule that gets broken later. */
.tray, .actions, .hand, .trade-want, .trade-bar, .trade-asks, .trade-offers {
  touch-action: manipulation;
}

html, body {
  margin: 0; padding: 0;
  height: 100%;
  background: var(--sea-1);
  color-scheme: dark;
  overscroll-behavior: none;
}

body {
  background: radial-gradient(120% 80% at 50% 0%, var(--sea-3) 0%, var(--sea-2) 45%, var(--sea-1) 100%);
  color: var(--text);
  font: 15px/1.45 ui-rounded, "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

button, input { font: inherit; color: inherit; }
/* No will-change here. It was added to "prime the GPU" for the :active scale, but this
   selector is every button in the app — a hundred in the markup before the dynamic ones —
   and will-change promotes each to its own compositor layer permanently, hidden sheets
   included. That is a standing GPU-memory cost on the device that can least afford it, on
   a screen whose one job is a canvas, in exchange for a transform the compositor already
   handles. The tap highlight stays: it removes the grey flash and costs nothing. */
button {
  background: none; border: 0; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* ------------------------------------------------------------ screens */
.screen {
  position: fixed; inset: 0;
  /* `innerHeight` can stop above the home-indicator area in an installed iPhone app.
     Fixed inset edges are the full viewport, so let them determine the height and use
     the measured value only as a floor. */
  min-height: var(--app-height);
  display: none;
  flex-direction: column;
}
.screen.is-active { display: flex; }

.home-scroll, .lobby-scroll {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: calc(var(--safe-t) + 22px) 18px calc(var(--safe-b) + 26px);
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 560px;
  width: 100%;
  margin: 0 auto;
}

/* ------------------------------------------------------------ brand */
.brand { text-align: center; padding: 6px 0 2px; }
.brand-mark {
  width: 92px; height: 92px;
  margin: 0 auto 6px;
  display: block;
  border-radius: 18px;
  /* The logo is a hexagon on paper, so it needs an edge of its own against the dark
     landing page — without one the parchment just bleeds into the background. */
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(232, 214, 178, 0.28);
}

.wordmark {
  margin: 0;
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--parch);
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.35);
}
.wordmark span { color: var(--sky); }

.tagline { margin: 6px 0 0; color: var(--dim); font-size: 13.5px; }

/* ------------------------------------------------------------ cards + inputs */
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 16px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
.card-title {
  font-weight: 700; font-size: 15px; margin-bottom: 10px;
  display: flex; align-items: center; gap: 8px;
}

.field-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--dim); font-weight: 700; margin: 2px 0 7px;
  display: flex; align-items: baseline; gap: 8px;
}
.field-label em { text-transform: none; letter-spacing: 0; font-style: normal; font-weight: 500; opacity: 0.75; }
.field-label + .field-label, .card > .field-label:not(:first-child) { margin-top: 16px; }

.text-input {
  width: 100%;
  background: rgba(4, 16, 28, 0.7);
  border: 1.5px solid var(--line);
  border-radius: var(--r);
  padding: 13px 14px;
  outline: none;
  transition: border-color .15s, background .15s;
}
.text-input:focus { border-color: var(--gold); background: rgba(4, 16, 28, 0.92); }
.text-input::placeholder { color: #5f778e; }


.or-split {
  display: flex; align-items: center; gap: 12px;
  margin: 14px 0;
  color: var(--dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em;
}
.or-split::before, .or-split::after { content: ""; flex: 1; height: 1px; background: var(--line); }

/* ------------------------------------------------------------ buttons */
.btn {
  border-radius: var(--r);
  padding: 13px 16px;
  font-weight: 700;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  transition: transform .12s, filter .15s, opacity .15s;
  width: 100%;
}
.btn:active { transform: scale(0.975); }
.btn:disabled { opacity: 0.45; pointer-events: none; }

.btn-key {
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%);
  color: var(--sky-ink);
  box-shadow: 0 5px 0 var(--sky-shade), 0 12px 24px rgba(0, 0, 0, 0.35);
}
.btn-key:active { box-shadow: 0 2px 0 var(--sky-shade), 0 6px 14px rgba(0, 0, 0, 0.35); }
.btn-label { font-size: 16.5px; }
.btn-sub { font-size: 12px; font-weight: 600; opacity: 0.72; }

.btn-ghost {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  color: var(--text);
}
/* On the landing page the quiet buttons are blue too, outlined rather than filled.
   Everything on that screen is a way into a game, so nothing there is really secondary —
   but four solid blue slabs in a column is a wall, and an outline says "blue, and not
   the first thing to press". Scoped to the home screen: Cancel and Close inside a sheet
   are genuinely secondary and stay grey. */
#screen-home .btn-ghost {
  background: rgba(53, 196, 255, 0.10);
  border-color: rgba(53, 196, 255, 0.55);
  color: var(--sky);
}
#screen-home .btn-ghost .btn-sub { color: var(--sky); opacity: 0.75; }
#screen-home .btn-ghost:active { background: rgba(53, 196, 255, 0.2); }
.btn-ghost:active { background: rgba(255, 255, 255, 0.12); }

/* A destructive confirm — leaving a game mid-play, and anything else in that shape
   later. Outlined in the same red the menu already marks that row with, rather than a
   solid fill: this is still a sheet action among ghost buttons, not the primary one. */
.btn-danger {
  background: rgba(229, 72, 77, 0.14);
  border: 1px solid var(--danger);
  color: #ff9fa1;
}
.btn-danger:active { background: rgba(229, 72, 77, 0.24); }

.btn-go {
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%);
  color: var(--sky-ink);
  width: auto; padding: 13px 22px;
  box-shadow: 0 4px 0 var(--sky-shade);
}

/* The room-setup surface's own clear blue action language. */
#screen-lobby .btn {
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: var(--sky-ink);
  box-shadow: 0 5px 0 var(--sky-shade), 0 12px 24px rgba(0, 0, 0, 0.25);
}
#screen-lobby .btn:active { box-shadow: 0 2px 0 var(--sky-shade), 0 6px 14px rgba(0, 0, 0, 0.25); }
#screen-lobby .btn .btn-sub { color: var(--sky-ink); opacity: 0.78; }

/* The launch surface goes back to gold — the colour it had before every surface leaned
   on the icon's blue — and flat, the same solid fill and dark ink the selected pill
   already uses when setting up a solo game, rather than the raised gradient-and-lip
   style the rest of the app's buttons use. How to Play and Settings are excluded: they
   are not a way into a game like everything else on this screen, so they keep the app's
   usual blue, raised button. */
#screen-home .btn {
  background: rgba(227, 186, 87, 0.9);
  border: 1px solid rgba(227, 186, 87, 0.9);
  color: #2a1e08;
  box-shadow: none;
}
#screen-home .btn:active { background: #e3ba57; box-shadow: none; }
#screen-home .btn .btn-sub { color: #2a1e08; opacity: 0.78; }
#screen-home #btn-how,
#screen-home #btn-settings {
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%);
  color: var(--sky-ink);
  box-shadow: 0 5px 0 var(--sky-shade), 0 12px 24px rgba(0, 0, 0, 0.25);
}
#screen-home #btn-how:active,
#screen-home #btn-settings:active { box-shadow: 0 2px 0 var(--sky-shade), 0 6px 14px rgba(0, 0, 0, 0.25); }
#screen-home #btn-how .btn-sub,
#screen-home #btn-settings .btn-sub { color: var(--sky-ink); opacity: 0.78; }
#screen-lobby .icon-btn,
#screen-lobby .step-b,
#screen-lobby .seg {
  background: rgba(53, 196, 255, 0.16);
  border: 1px solid rgba(53, 196, 255, 0.58);
  color: var(--sky);
}
#screen-lobby .icon-btn:active,
#screen-lobby .step-b:active { background: rgba(53, 196, 255, 0.3); }
#screen-lobby .seg.on {
  background: linear-gradient(180deg, var(--sky) 0%, var(--sky-2) 100%);
  color: var(--sky-ink);
  box-shadow: 0 2px 0 var(--sky-shade);
}

.home-row { display: flex; gap: 10px; }
.home-row .btn { flex: 1; }

/* The landing page is a launch surface, not a document. Keep every way into a game
   visible together on a phone rather than making the player hunt below the fold. */
#screen-home .home-scroll {
  overflow: hidden;
  gap: 10px;
  padding-top: calc(var(--safe-t) + 10px);
  padding-bottom: calc(var(--safe-b) + 10px);
}
#screen-home .brand-mark { width: 68px; height: 68px; margin-bottom: 3px; }
#screen-home .wordmark { font-size: 32px; }
#screen-home .tagline { margin-top: 2px; font-size: 12px; }
#screen-home .card { padding: 10px; }
#screen-home .field-label { margin: 0 0 4px; }
#screen-home .text-input { padding: 9px 12px; }
#screen-home .btn { padding: 9px 12px; }
#screen-home .btn-label { font-size: 15px; }
#screen-home .btn-sub { font-size: 11px; }
#screen-home .or-split { margin: 7px 0; }
#screen-home .home-row { gap: 8px; }
#screen-home #btn-join-code { margin-top: 8px; }
#screen-home #btn-solo { margin-top: 8px; }
#screen-home .ver { margin-top: 0; padding-top: 0; }

/* ------------------------------------------------------------ topbar */
.topbar {
  display: flex; align-items: center; gap: 10px;
  padding: calc(var(--safe-t) + 8px) 10px 8px;
  background: rgba(5, 18, 32, 0.9);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
  flex-shrink: 0;
}
.topbar-title { flex: 1; text-align: center; font-weight: 700; }

.icon-btn {
  width: 40px; height: 40px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.06);
  position: relative;
}
.icon-btn svg { width: 22px; height: 22px; }
.icon-btn:active { background: rgba(255, 255, 255, 0.14); }
/* ------------------------------------------------------------ lobby */
.code-hero {
  text-align: center;
  background: linear-gradient(180deg, rgba(227, 186, 87, 0.15), rgba(227, 186, 87, 0.04));
  border: 1px solid rgba(227, 186, 87, 0.3);
  border-radius: var(--r-lg);
  padding: 16px;
}
.code-hero-l { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim); }
.code-hero-v {
  display: block; width: 100%;
  font-size: 46px; font-weight: 800; letter-spacing: 0.16em;
  color: var(--gold); text-indent: 0.16em;
  padding: 2px 0;
}
.code-hero-s { display: block; font-size: 12.5px; color: var(--dim); }
.code-hero-s b { color: var(--text); }

.seat-list { display: flex; flex-direction: column; gap: 8px; }
.seat {
  display: flex; flex-wrap: wrap; align-items: center; gap: 11px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line);
  border-left-width: 5px;
  border-radius: var(--r-sm);
  padding: 10px 12px;
}
.seat-kick {
  margin-left: auto;
  width: 28px; height: 28px; flex: 0 0 auto;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.05);
  color: var(--dim);
  font-size: 13px; font-weight: 800;
  display: grid; place-items: center;
}
.seat-kick:active { background: rgba(229, 72, 77, 0.25); color: #ffb4b6; }

.seat-swatch {
  width: 16px; height: 16px; border-radius: 5px;
  background: var(--c, #888);
  border: 1px solid rgba(0, 0, 0, .4);
  flex: 0 0 auto;
}
/* Your own row is a button — it opens the colour picker. */
.seat-me { width: 100%; text-align: left; cursor: pointer; }
.seat-me:active { background: rgba(255, 255, 255, 0.1); }
.seat-name { flex: 1 1 auto; min-width: 0; font-weight: 700; overflow-wrap: anywhere; }
.seat-tag {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
  padding: 3px 8px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.1); color: var(--dim);
}
.seat-tag.host { background: rgba(227, 186, 87, 0.2); color: var(--gold); }
.seat-tag.you { background: rgba(34, 197, 94, 0.18); color: #7ee2a8; }
.seat-tag.choosing { background: rgba(227, 186, 87, 0.14); color: var(--gold); }

.pill {
  font-size: 12px; font-weight: 700;
  padding: 2px 9px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.1); color: var(--dim);
}

.hint { color: var(--dim); font-size: 12.5px; margin: 10px 0 0; text-align: center; }
.backend-hint { margin: 6px 0; }

.opt-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0;
  border-top: 1px solid var(--line);
}
.opt-row:first-of-type { border-top: 0; }
.opt-row.col { flex-direction: column; align-items: stretch; gap: 9px; }
.opt-l { flex: 1; font-weight: 600; display: flex; flex-direction: column; gap: 2px; }
.opt-l em { font-style: normal; font-weight: 500; font-size: 12px; color: var(--dim); }

/* A heading over a stretch of related settings. Six rows that each had to introduce
   themselves is what made the panel long; three headings carry the same meaning in a
   fraction of the height, and give the eye somewhere to rest between groups. */
.opt-group {
  font-size: 10.5px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--gold);
  padding: 16px 0 2px;
  border-top: 1px solid var(--line);
  margin-top: 4px;
}
.opt-group:first-of-type { border-top: 0; margin-top: 0; padding-top: 2px; }
/* The heading already draws the line above the group, so the first row under it must
   not draw a second one three pixels below. */
.opt-group + .opt-row, .opt-group + .opt-pair { border-top: 0; }

/* The explainer moved out of the label and under the control it describes — it is about
   the answer, not the question, and reading it after the choice is the right order. */
.opt-note {
  font-style: normal; font-weight: 500;
  font-size: 12px; color: var(--dim);
  line-height: 1.35;
}

/* Two settings of the same shape, side by side. Both are a number with a stepper and
   they are read together, so they cost one row rather than two. */
.opt-pair {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  padding: 11px 0;
  border-top: 1px solid var(--line);
}
.opt-pair.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.opt-cell { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
/* Narrow cells: the stepper has to give up its gaps before the number gets clipped. */
.opt-pair.three .step-b { width: 30px; height: 30px; font-size: 16px; }
.opt-pair.three .step-v { min-width: 20px; font-size: 16px; }
.opt-cell .stepper { justify-content: space-between; }

/* The size row under Dynamic. Indented and quieter than the boards above it, so it reads
   as a detail of the choice already made rather than a second, competing choice. */
.seg-row.sub {
  margin-left: 14px;
  background: rgba(0, 0, 0, 0.18);
}
.seg-row.sub .seg { font-size: 12.5px; padding: 7px 8px; }

.stepper { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.step-b {
  width: 34px; height: 34px; border-radius: 10px;
  background: rgba(255, 255, 255, 0.08);
  font-size: 18px; font-weight: 700;
  display: grid; place-items: center;
}
.step-b:active { background: rgba(255, 255, 255, 0.18); }
.step-v { min-width: 30px; text-align: center; font-weight: 800; font-size: 17px; }

.seg-row { display: flex; gap: 6px; background: rgba(0, 0, 0, 0.28); padding: 4px; border-radius: 13px; }
.seg-row.wide { margin-bottom: 12px; }
/* Four boards will not sit in one row on a phone — "Newfoundland · 52" needs more width
   than a quarter of a 375px screen. Two by two, so every board keeps a readable name. */
.seg-row.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The dynamic board takes the full width on its own row: it is the odd one out of five,
   and it is the only option whose size is a range rather than a number. */
.seg-row.grid2 .seg.span2 { grid-column: 1 / -1; }
.seg {
  flex: 1; padding: 9px 8px; border-radius: 10px;
  font-size: 13px; font-weight: 700; color: var(--dim);
  transition: background .15s, color .15s;
}
.seg.on { background: rgba(227, 186, 87, 0.9); color: #2a1e08; }

.lobby-actions { margin-top: 2px; }

/* ------------------------------------------------------------ game board */
#screen-game { padding-top: 0; }

.board-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
#board-cv { width: 100%; height: 100%; display: block; touch-action: none; }

/* Whose turn it is, how long is left, and what was rolled — one row across the top of
   the board. They used to be three separately positioned things stacked down the middle,
   which read as three announcements rather than one status line, and pushed the dice a
   third of the way down the island. */
/* One white pill holding all three, rather than three dark ones in a row. They are a
   single status line — whose go it is, how long is left, what came up — and three
   separate lozenges read as three announcements that happened to land side by side.
   White because the parts inside are ink and gold and a card face; on the dark blue they
   each had to carry their own plate to be legible. */
.board-hud {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 9px;
  max-width: calc(100% - 16px);
  padding: 5px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(13, 27, 40, 0.14);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(8px);
  color: var(--ink);
  pointer-events: none;
  /* Above the veil (40) and above the sheets (50). A sheet the game is waiting on is
     exactly when the clock matters most — you are being held to a deadline by the very
     thing that was covering it. Nothing here takes a tap, so sitting on top of the veil
     costs the veil nothing. */
  z-index: 60;
}

/* The pill around all of it carries the plate now, so each part is just its text. */
.board-badge {
  font-size: 13px; font-weight: 700;
  white-space: nowrap;
  /* The flexible one: the dice and the clock are fixed widths, so when the row runs out
     of phone this is what gives. */
  min-width: 0; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis;
}
/* Bronze, not --gold: the gold was picked to carry on dark blue and is barely there on
   white. Same hue, taken far enough down to read. */
.board-badge.mine, .board-badge.pause-badge { color: var(--gold-ink); }
.board-badge.pause-badge.pause-urgent {
  color: var(--danger-ink);
  animation: timer-tick 1s steps(2) infinite;
}

.turn-timer {
  flex: 0 0 auto;
  min-width: 26px;
  font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums;
  text-align: center;
  pointer-events: none;
}
/* Yours to act on — and running out. */
.turn-timer.mine { color: var(--gold-ink); }
.turn-timer.urgent {
  color: var(--danger-ink);
  animation: timer-tick 1s steps(2) infinite;
}
/* Hairlines instead of gaps, so three things on one plate still read as three things.
   Only on what is actually showing — the clock and the dice each come and go. */
.turn-timer:not([hidden]), .dice-float:not([hidden]) {
  border-left: 1px solid rgba(13, 27, 40, 0.16);
  padding-left: 9px;
}
@keyframes timer-tick { 50% { opacity: 0.45; } }

.dice-float {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 5px;
}
/* Smaller than they were, because they now share a row rather than owning one. Dark
   faces, since the plate under them went white and a parchment die on white is a number
   floating in space rather than a die. */
.die {
  width: 28px; height: 28px;
  border-radius: 8px;
  background: var(--ink);
  color: var(--parch);
  font-size: 15px; font-weight: 800;
  display: grid; place-items: center;
  box-shadow: 0 2px 5px rgba(13, 27, 40, 0.3);
  animation: die-in .32s cubic-bezier(.2, 1.4, .4, 1);
}
@keyframes die-in { from { transform: scale(.3) rotate(-25deg); opacity: 0; } }

/* ------------------------------------------------------------ the roll
   Rolling is the most repeated action in the game and it used to be two numbers
   appearing. One large die spins over the board and lands showing the total — the
   number that actually decides the turn — then flies to take its place in the readout.

   The stage covers the board and takes no input: the die is a thing happening, not a
   thing to press, and a full-screen layer that swallowed taps would eat the first tap of
   the turn it is announcing. */
.dice-stage {
  position: absolute; inset: 0;
  z-index: 6;
  pointer-events: none;
  display: grid; place-items: center;
}
.bigdie {
  width: 128px; height: 128px;
  border-radius: 26px;
  background: linear-gradient(160deg, #fdf6e4, var(--parch));
  color: var(--ink);
  display: grid; place-items: center;
  font-size: 60px; font-weight: 800;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 22px 48px rgba(0, 0, 0, .55), inset 0 -5px 0 rgba(0, 0, 0, .18);
  /* The two transforms the animation drives, declared here so the settle can start from
     wherever the spin happened to leave the die. */
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}
.dice-stage.rolling .bigdie { animation: die-spin .62s cubic-bezier(.28,.02,.24,1) both; }

/* Two full turns, always — a real toss looks different every time, but an animation that
   varied would read as a glitch rather than as a roll. Landing back at a multiple of
   360deg is what lets the total appear upright the instant the spin ends, with no separate
   "straighten up" step to sit through. */
@keyframes die-spin {
  0%   { transform: scale(.4) rotate(0deg); opacity: 0; }
  16%  { opacity: 1; }
  38%  { transform: scale(1.2) rotate(300deg); }
  62%  { transform: scale(.9) rotate(560deg); }
  84%  { transform: scale(1.06) rotate(680deg); }
  100% { transform: scale(1) rotate(720deg); opacity: 1; }
}

/* ------------------------------------------------------------ a trade crossing the table
   Both hands laid out at once, each player's name over the cards they gave up, so a
   completed trade reads as the exchange it was rather than just two hand counts changing
   a moment later. Trades are public knowledge already — the offer sheet shows both
   sides — so unlike a steal this plays for everyone, not just the two traders. */
.trade-stage {
  position: fixed; inset: 0;
  z-index: 91;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  pointer-events: none;
}
.trade-duel {
  display: flex; align-items: center; gap: 18px;
  padding: 20px 24px;
  border-radius: 20px;
  background: rgba(4, 11, 19, .94);
  border: 1px solid rgba(255, 255, 255, .16);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5), 0 24px 70px rgba(0, 0, 0, .7);
  backdrop-filter: blur(10px);
  animation: trade-duel-in .3s cubic-bezier(.2, 1, .3, 1) both;
}
.trade-duel.is-leaving { animation: trade-duel-out .28s ease both; }
.trade-duel-side {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: auto; max-width: 160px;
}
.trade-duel-name {
  font-weight: 800; font-size: 14px;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.trade-duel-cards {
  display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 160px;
}
.trade-duel .rcard { width: 68px; height: 92px; border-radius: 11px; }
.trade-duel .rcard-face { border-radius: 8px; }
.trade-duel .rcard-count { top: -7px; right: -7px; min-width: 24px; height: 24px; line-height: 24px; font-size: 13px; }
.trade-duel-arrows {
  color: var(--gold);
  display: grid; place-items: center;
  animation: trade-duel-pulse 1s ease-in-out infinite;
}
@keyframes trade-duel-in { from { opacity: 0; transform: scale(.82) translateY(10px); } }
@keyframes trade-duel-out { to { opacity: 0; transform: scale(.9); } }
@keyframes trade-duel-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } }

/* A few seconds to take back your own last build. Floats just above the tray, the same
   way the trade layer does, so it never sits under your own thumb. */
.undo-bar {
  position: fixed;
  left: 50%; bottom: calc(var(--tray-h, 0px) + 10px);
  transform: translateX(-50%);
  z-index: 61;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 9px 9px 16px;
  border-radius: 999px;
  background: rgba(8, 24, 40, 0.94);
  border: 1px solid var(--line);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .45);
  color: var(--text);
  font-size: 13px; font-weight: 600;
  animation: undo-in .2s cubic-bezier(.2, 1.2, .4, 1) both;
}
.undo-bar:active { background: rgba(8, 24, 40, 1); }
.undo-cta {
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--sky);
  color: var(--sky-ink);
  font-weight: 800;
}
@keyframes undo-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } }

/* The reaction picker: big, tappable, six choices and nothing to read — the whole point
   is that it costs less than typing. */
.react-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
  margin-top: 4px;
}
.react-grid button {
  font-size: 34px; line-height: 1;
  padding: 16px 0;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
}
.react-grid button:active { background: rgba(255, 255, 255, 0.14); transform: scale(0.94); }

/* A thrown reaction, rising off the sender's own pill so it reads as coming from them
   rather than from the middle of the board. Fixed to the viewport, same as a trade's
   flying cards, for the same reason: the pills it starts at live in the header. */
.reaction-stage {
  position: fixed; inset: 0;
  z-index: 92;
  pointer-events: none;
}
.reaction-float {
  position: fixed;
  transform: translate(-50%, -50%);
  font-size: 30px;
  animation: reaction-rise 1.5s ease-out forwards;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
@keyframes reaction-rise {
  0% { transform: translate(-50%, -50%) scale(.4); opacity: 0; }
  18% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
  32% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, calc(-50% - 100px)) scale(1); opacity: 0; }
}

/* ------------------------------------------------------------ what you were paid
   The cards a roll gave you, shown at a size you cannot miss and then put away into your
   hand. Before this the only sign you had been paid was a badge quietly counting up in
   the tray, which is easy to miss entirely on somebody else's turn — and the whole point
   of a production roll is finding out what you got. */
.gain-stage {
  position: absolute; inset: 0;
  z-index: 7;
  pointer-events: none;
  display: grid; place-items: center;
}
/* The gap has to clear the blown-up cards, which overflow their own boxes. */
.gain-row { display: flex; align-items: center; gap: 62px; }
.gain-card {
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  /* Held up at more than double the size they will be in the tray. */
  animation: gain-in .34s cubic-bezier(.2, 1.35, .4, 1) both;
}
/* Blown up with a transform rather than by setting a bigger width. A card is a frame,
   a radius, a badge and a shadow as well as a picture, and resizing the box leaves all of
   those at their original few pixels — at twice the size the border reads as no border at
   all. Scaling takes the whole card with it. */
.gain-blow { display: block; transform: scale(2.1); transform-origin: 50% 50%; }
@keyframes gain-in {
  from { transform: translateY(46px) scale(.35); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}
/* Away: each card flies to its own slot in the tray, so where it lands says which card
   it was. Measured at run time, like the dice — the tray moves with trade mode, the safe
   area and how many cards are in it. */
.gain-stage.away .gain-card {
  animation: gain-away .42s cubic-bezier(.55, 0, .85, .5) both;
  animation-delay: var(--d, 0ms);
}
@keyframes gain-away {
  to { transform: translate(var(--dx), var(--dy)) scale(var(--ds)); opacity: 0; }
}

/* A stolen card goes the other way: held up so both of you see what it was, then away
   upward off the top of the board, shrinking as it goes. Up rather than into a hand,
   because a card was taken — the movement is the point, and it reads the same to the
   player who lost it and the one who took it. */
.gain-stage.stolen .gain-card {
  animation: gain-stolen .52s cubic-bezier(.4, 0, .8, .4) both;
}
@keyframes gain-stolen {
  to { transform: translateY(var(--dy, -70vh)) scale(.18) rotate(-8deg); opacity: 0; }
}

/* ------------------------------------------------------------ the awards
   Longest Road and Largest Army are two points changing hands, the biggest single swing
   in the game outside somebody winning. They used to be a line of text on the shoutout
   card; they are cards in their own right, so they are shown as cards. */
.award-stage {
  position: absolute; inset: 0;
  z-index: 8;
  pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px;
}
.award-frame {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
}
/* The taker's name and the award's own name, on the card rather than above it — the same
   dark pill the turn and steal shoutouts use, laid over the middle of the illustration. */
.award-take {
  position: absolute;
  --c: var(--gold);
  max-width: calc(100% - 32px);
  background: rgba(8, 24, 40, 0.94);
  border: 2px solid var(--c);
  border-radius: 999px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 0 0 6px rgba(0, 0, 0, 0.18);
  padding: 14px 26px;
  font-size: 20px; font-weight: 800; letter-spacing: 0.01em;
  color: var(--parch);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  white-space: nowrap;
  animation: award-take-in .3s cubic-bezier(.2, 1.3, .4, 1) both;
}
.award-take-line { display: block; }
.award-card {
  /* Tall card on a short screen: height is the constraint, not width. */
  height: min(52vh, 340px);
  width: auto;
  filter: drop-shadow(0 20px 44px rgba(0, 0, 0, .65));
  animation: award-in .42s cubic-bezier(.2, 1.25, .38, 1) both;
}
.award-stage.out .award-take, .award-stage.out .award-card {
  animation: award-out .3s ease-in both;
}
@keyframes award-take-in {
  from { opacity: 0; transform: translateY(14px); }
}
@keyframes award-in {
  from { opacity: 0; transform: translateY(30px) scale(.72) rotate(-4deg); }
}
@keyframes award-out {
  to { opacity: 0; transform: translateY(-16px) scale(.94); }
}

/* The settle: the die shrinks and flies to the readout in the status row. The distance
   is measured at run time and handed in as --dx/--dy, because where that row sits moves
   with the safe area, the score strip and the width of the turn badge. */
.dice-stage.settling .bigdie {
  animation: die-settle .40s cubic-bezier(.5, 0, .75, .9) both;
}
@keyframes die-settle {
  to { transform: translate(var(--dx), var(--dy)) scale(var(--ds)); opacity: 0; }
}

/* The total, so the roll can be read without adding the pips up. Gold rather than ink,
   so it is plainly the answer rather than a third die. */
.die-eq {
  align-self: center;
  color: rgba(13, 27, 40, 0.5);
  font-size: 13px; font-weight: 800;
}
.die.die-sum {
  background: var(--gold);
  color: #2a1e08;
  min-width: 28px; width: auto; padding: 0 8px;
}

/* ------------------------------------------------------------ discarding */
.discard-head { display: flex; align-items: flex-start; gap: 10px; }
.discard-head .sheet-title { flex: 1; margin-bottom: 2px; }
.discard-clock {
  flex: 0 0 auto;
  min-width: 40px;
  padding: 5px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 15px; font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.discard-clock.urgent {
  border-color: var(--danger); color: #ff9fa1;
  animation: timer-tick 1s steps(2) infinite;
}

/* What you are about to lose. Red, because it is the one area on screen where tapping
   costs you something, and it holds its height whether full or empty so the cards below
   never shift under your finger between taps. */
.discard-bin {
  display: flex; flex-wrap: wrap; gap: 6px;
  align-content: flex-start;
  min-height: 104px;
  margin: 10px 0 14px;
  padding: 9px;
  border: 2px solid var(--danger);
  border-radius: var(--r);
  background: rgba(229, 72, 77, 0.09);
}
.discard-bin.empty {
  align-content: center; justify-content: center;
  border-style: dashed;
}
.discard-bin-hint { color: var(--dim); font-size: 12px; text-align: center; padding: 0 12px; }

/* Laid out like the tray's hand, because it is the same hand — same spacing, same
   bottom alignment, same room round the edge for the count badges that sit outside the
   card. Recognising it as your hand is the whole point. */
.discard-hand {
  display: flex; gap: 12px; justify-content: center; align-items: flex-end;
  flex-wrap: wrap;
  padding: 8px 4px 2px;
  min-height: 52px;
}
/* Full: the hand is no longer the thing to tap, so it stops asking to be. */
.discard-hand.done { opacity: 0.4; pointer-events: none; }

.discard-card { padding: 0; background: none; border: 0; line-height: 0; }
.discard-card:active { transform: scale(0.9); }

/* ------------------------------------------------------------ trading */
/* The rate under each card it prices. Gold whenever a port has beaten the standard 4:1,
   which is the only reason this number is ever anything else. */
.pick-have b { color: var(--gold); font-weight: 800; }

/* ---------------------------------------------------------------- trading, in the tray
   The whole point of moving trading out of a sheet is that the board, the turn clock and
   your own cards stay on screen while you do it. So everything here has to be short: two
   rows of cards and one row of buttons, sized to leave the island the room it had. */

.trade-want {
  display: flex; gap: 10px; justify-content: center; align-items: flex-end;
  padding: 10px 4px 4px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 2px;
  flex-wrap: wrap;
}
#give-lead { padding-top: 6px; margin-bottom: -2px; }
.trade-give-bin {
  display: flex; flex-wrap: wrap; gap: 6px;
  align-content: flex-start;
  min-height: 104px;
  margin: 0 0 2px;
  padding: 9px;
  border: 2px solid var(--sky);
  border-radius: var(--r);
  background: rgba(61, 143, 224, 0.12);
}
.trade-give-bin.empty {
  align-content: center; justify-content: center;
  border-style: dashed;
}
.trade-give-hint { color: var(--dim); font-size: 12px; text-align: center; padding: 0 12px; }
.trade-lead {
  align-self: center;
  font-size: 10px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--gold);
  width: 100%; text-align: center;
  margin-bottom: -4px;
}

/* A card with its clear button. The cross sits on the opposite corner from the count,
   so a selected card carries "how many" on one shoulder and "not any more" on the
   other, and neither has to move to make room for the other. */
/* A column: the bank's rate for this card, the card, and how many of it are going.
   The rate sits above because that is the order it is used in — you read what a card
   costs, then decide whether to part with it. Underneath is the answer, not the input. */
.pick {
  position: relative;
  display: inline-flex; flex-direction: column; align-items: center;
  flex: 0 0 auto;
}
.pick-rate {
  position: relative;
  z-index: 3;
  font-size: 10px; font-weight: 800;
  letter-spacing: .03em;
  color: var(--gold);
  margin-bottom: 3px;
  line-height: 1;
  padding: 1px 3px 2px;
  border-radius: 4px;
  background: rgba(5, 18, 32, 0.94);
}
/* The card you tap is a button again. It was a bare span carrying a data attribute,
   which works for a finger and for nothing else — no keyboard, no screen reader, no
   announcement of what tapping it would do. The old trade sheet had this right. */
.pick-tap {
  padding: 0; margin: 0;
  background: none; border: 0;
  display: inline-flex;
  color: inherit;
}
.pick-tap:active .rcard { transform: scale(.94); }
.pick-clear {
  position: absolute; top: -7px; left: -7px;
  width: 22px; height: 22px;
  border-radius: 999px;
  background: rgba(10, 20, 32, 0.95);
  border: 1.5px solid var(--line);
  color: var(--text);
  font-size: 13px; font-weight: 800; line-height: 1;
  display: grid; place-items: center;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .45);
  z-index: 2;
}
.pick-clear:active { background: rgba(229, 72, 77, 0.5); }

.trade-bar { display: flex; flex-direction: column; gap: 7px; }
/* The readout is the part that teaches, so it gets the width and a reserved line — a
   note that appeared and vanished would shuffle the whole tray up and down. */
.trade-note {
  min-height: 32px;
  display: flex; align-items: center; justify-content: center;
  text-align: center;
  padding: 5px 10px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line);
  font-size: 12.5px; font-weight: 600; line-height: 1.3;
  color: var(--text);
}
.trade-go { display: flex; gap: 7px; }
.trade-go .btn { flex: 1; padding: 11px 8px; font-size: 13.5px; }
.trade-go .btn:disabled { opacity: 0.4; }

/* An offer waiting on your answer. It used to be a sheet, and it covered your hand and
   the clock — which is worse for the person answering than for the person asking, since
   you are deciding on somebody else's timer. Struck through with the asker's colour so
   it is obvious at a glance who is waiting on you. */
/* Trades sit above the tray, over the water, rather than inside it.
   They used to be tray rows, so an incoming offer grew the tray and the board — which is
   a flex child above it — lost exactly that much height. On a phone a couple of offers
   squeezed the island into a strip, and the whole map jumped every time somebody asked
   for wheat. Out of the flow, the board underneath does not move at all. */
.tray-float {
  position: absolute;
  left: 10px; right: 10px; bottom: 100%;
  /* Capped and centred rather than stretched edge to edge.
     A phone is narrower than the cap, so this changes nothing there — it is the desktop
     that needed it, where an offer ran the full width of the window with one short
     sentence stranded at the far left of it. `margin: auto` against both `left` and
     `right` is what centres an absolutely positioned box once its width is limited. */
  max-width: 520px;
  margin: 0 auto 8px;
  z-index: 4;
  display: flex; flex-direction: column; gap: 6px;
  /* Empty most of the time, and must not swallow taps on the board when it is. */
  pointer-events: none;
}
.tray-float > *:not([hidden]) { pointer-events: auto; }

/* The whole of trading lives in one centred column, the floating offers and the picker
   in the tray alike. Each of these was a full-width row, which is the right answer on a
   phone and looks abandoned on a desktop: a short sentence and two buttons stretched
   across a 1400px window. The cap is wider than any phone, so none of this binds there. */
#trade-want, #give-lead, #trade-give-bin, #trade-bar {
  width: 100%;
  max-width: 520px;
  margin-left: auto;
  margin-right: auto;
}

.trade-asks { max-height: 34vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }
/* Reading over open water rather than over the tray's own panel, so each one needs to
   carry its own weight against a moving background. */
.tray-float .ask,
.tray-float .my-offer { box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); }
.ask {
  border: 1px solid var(--line);
  border-left: 4px solid var(--c, var(--gold));
  border-radius: var(--r);
  background: var(--panel-2);
  padding: 8px 10px;
  margin-bottom: 7px;
}
/* One sentence with the cards set into it, so the offer reads as a thing somebody said
   rather than a form with two labelled halves. A flex row rather than true inline flow:
   40px cards in a line of 13px text make a mess of line-height, and this wraps cleanly
   on a narrow phone without any of that. */
.ask-line {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: center; text-align: center;
  gap: 5px;
  font-size: 13px; font-weight: 600; line-height: 1.35;
  color: var(--text);
}
/* Ten seconds, counted where the offer is rather than up on the board. It is a deadline
   on this one thing, not on the turn, and the two would be read as the same clock if they
   sat together. The number is written in place by the ticking loop — rebuilding the strip
   four times a second would throw away whatever tap was half-made on it. */
.offer-clock {
  flex: 0 0 auto;
  min-width: 30px; text-align: center;
  padding: 2px 7px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  color: var(--dim);
  font-size: 11px; font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.offer-clock.urgent { background: rgba(229, 72, 77, 0.22); color: #ff9fa1; }

.ask-name {
  font-size: 14px; font-weight: 800;
  color: #fff;
  text-align: center;
  margin-bottom: 4px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ask-cards { display: inline-flex; gap: 4px; padding-top: 5px; }
/* Each half of the sentence holds on to its own card.
   Loose in the line, the cards were separate items and the line broke wherever it ran
   out of room — which on a narrow phone meant a card alone on the next row, reading as a
   mistake rather than as a sentence. Bound to the words they belong to, a long name
   breaks the line between the two halves instead: "Dag is offering you a wheat" over
   "in exchange for an ore", which is where a person would break it too. */
.ask-part { display: inline-flex; align-items: center; gap: 5px; flex: 0 1 auto; }
/* The countdown rides with the buttons, not in the sentence.
   In the sentence it was one more item competing for the same line, and it was the item
   that tipped "…in exchange for a [card]" onto a second row — the sentence fits in about
   308px of a 345px strip, and the pill and its gap are 35 of them. Down here it is also
   nearer what it actually governs: how long is left to press one of these. */
.ask-go { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 7px; margin-top: 8px; }
.ask-go .offer-clock { flex: 0 0 auto; }
.ask-go .btn { flex: 1 1 0; min-width: 0; padding: 9px 8px; font-size: 13px; }

/* Your own offers ride above the pickers, and stay in the tray after you leave trade
   mode — an acceptance should not need a sheet reopened at you to be seen. */
.trade-offers {
  /* A posted offer is the only trade UI left after it is submitted. Do not make a
     player tap or scroll just to see who still needs to answer. */
  margin-bottom: 4px;
}

.my-offer {
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel-2);
  padding: 9px 10px;
  margin-bottom: 8px;
}
.my-offer-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
.my-offer-top .offer-cards { display: flex; gap: 3px; flex-wrap: wrap; min-width: 0; }
.swap-arrow { color: var(--gold); font-size: 15px; }
.offer-drop {
  margin-left: auto;
  width: 26px; height: 26px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.05);
  color: var(--dim);
  font-size: 13px;
  flex: 0 0 auto;
}

/* Falling paper, over everything, touchable by nothing. Above the sheets so it keeps
   falling past the scoreboard when that arrives, and above the veil so it is not dimmed
   by it. */
.confetti {
  position: fixed; inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 96;
}
.confetti-bit {
  position: absolute; top: -16px;
  width: 9px; height: 15px;
  background: var(--c, #fff);
  border-radius: 1px;
  animation-name: confetti-fall;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  will-change: transform;
}
/* One transform and one opacity, which is all the compositor needs and all it gets —
   seventy pieces animating anything else would be felt on a phone. */
@keyframes confetti-fall {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translate3d(var(--drift, 0), 105vh, 0) rotate(var(--spin, 360deg)); opacity: 0; }
}

/* ------------------------------------------------------------ results */
.over-meta { margin-top: 4px; font-size: 12.5px; color: var(--dim); }

.final-top { display: flex; align-items: baseline; gap: 10px; }
.final-sources {
  display: flex; flex-wrap: wrap; gap: 4px 10px;
  margin-top: 7px; padding-left: 30px;
}
/* One line per source of points, so the parts visibly add to the total beside them. */
.src { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }
.src-ico { font-size: 12px; }
.src-lab { color: var(--dim); }
.src-vp {
  min-width: 17px; text-align: center;
  font-weight: 800;
  padding: 1px 5px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.09);
}
.src-none { font-size: 12px; color: var(--dim); font-style: italic; }

/* ------------------------------------------------------------ chat */
/* The board's own tools, in its bottom-left corner — opposite the side a thumb places a
   road on, so nothing floating sits under it.

   One handle, three tools. Trade filter, recentre and the log are each worth a few taps a
   game and were on the water for all of it, three dark discs over the island. Folded up
   they cost one, and open they are a single pill rather than three discs again: the
   buttons inside lose their own circles so the pill is the shape and the icons are its
   contents. */
.board-tools {
  position: absolute;
  left: 12px;
  /* Above whatever the floating trade layer is currently holding. Both live in the
     bottom-left corner of the board, so an offer arriving would otherwise land straight
     on top of these and bury the name of whoever sent it. */
  bottom: calc(12px + var(--float-h, 0px));
  z-index: 5;
  transition: bottom .18s;
}
/* Stacked ABOVE the handle by absolute position rather than by being a sibling in a
   column. In a column it would have to leave the layout to stay shut, and a box that
   leaves the layout cannot be animated back into it — and it would drag the handle up
   off the corner every time it opened. Out of flow, the handle never moves and the pill
   can grow out of it. */
.board-tools-list {
  position: absolute;
  bottom: calc(100% + 8px); left: 50%;
  display: flex; flex-direction: column; gap: 2px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(5, 18, 32, 0.9);
  border: 1px solid var(--line);
  backdrop-filter: blur(8px);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .45);
  transform-origin: 50% 100%;
  transform: translateX(-50%);
  transition: opacity .16s ease, transform .16s cubic-bezier(.2, 1.2, .4, 1), visibility .16s;
}
/* Shut: invisible AND untappable, so it cannot swallow a tap meant for the island. */
.board-tools:not(.open) .board-tools-list {
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(8px) scale(.85);
}
/* Inside the pill the buttons ARE the pill's contents, so they drop their own disc.
   Kept the full 44px tall: a smaller target is the wrong economy on a phone. */
.board-tools-list .board-fab {
  background: none;
  border: 0;
  border-radius: 999px;
  backdrop-filter: none;
  box-shadow: none;
}
.board-tools-list .board-fab:active { background: rgba(255, 255, 255, .14); }
.board-tools.open .board-tools-btn svg { transform: rotate(90deg); }
.board-tools-btn svg { transition: transform .16s; }
.board-fab {
  position: relative;
  width: 44px; height: 44px; border-radius: 50%;
  background: rgba(5, 18, 32, 0.9);
  border: 1px solid var(--line);
  color: var(--text);
  display: grid; place-items: center;
  backdrop-filter: blur(8px);
  z-index: 5;
}
.board-fab svg { width: 22px; height: 22px; }
.board-fab:active { background: rgba(5, 18, 32, 1); }
.chat-float {
  position: fixed;
  width: 50px;
  height: 50px;
  right: 12px;
  bottom: calc(var(--safe-b) + 12px);
  /* Keep it above the results sheet and its veil so the table can still chat after
     somebody wins. Tapping it replaces the sheet with chat as usual. */
  z-index: 60;
  cursor: grab;
  touch-action: none;
}
.chat-float svg { width: 26px; height: 26px; color: #fff; }
.chat-float.dragging { cursor: grabbing; }
.chat-dot {
  position: absolute; top: -3px; right: -3px;
  font-style: normal;
  min-width: 19px; height: 19px; padding: 0 5px;
  border-radius: 999px;
  background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 800; font-style: normal;
  line-height: 19px; text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .5);
}
/* The log's marker carries no number — there is nothing useful to count, since a turn
   can put half a dozen lines in at once — so it is a plain dot rather than a badge. */
.log-dot { min-width: 12px; width: 12px; height: 12px; padding: 0; }

.chat-log {
  max-height: 46vh; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; gap: 7px;
  padding: 2px 0 6px;
}
#sheet-chat {
  background: linear-gradient(180deg, #bfe7f7 0%, #7cc2e4 100%);
  color: var(--ink);
}
#sheet-chat .text-input {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.72);
  border-color: rgba(13, 27, 40, 0.28);
  caret-color: var(--ink);
}
#sheet-chat .text-input:focus {
  background: rgba(255, 255, 255, 0.9);
  border-color: var(--ink);
}
#sheet-chat .text-input::placeholder { color: rgba(13, 27, 40, 0.58); }
#sheet-chat .text-input:disabled { opacity: .55; }
.chat-row {
  border-left: 3px solid var(--c, var(--dim));
  padding: 5px 9px;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: rgba(255, 255, 255, 0.05);
}
.chat-row.mine { background: rgba(53, 196, 255, 0.12); }
.chat-meta time { margin-left: auto; }
.chat-meta { display: flex; align-items: baseline; gap: 7px; }
.chat-who {
  font-size: 11px; font-weight: 800;
  color: var(--c, var(--dim));
}
.chat-time { font-size: 10px; color: rgba(13, 27, 40, 0.56); }
.chat-text { font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
.chat-mention {
  display: inline-block; padding: 0 3px; border-radius: 4px;
  background: rgba(35, 116, 180, 0.22); color: #0b5d91; font-weight: 800;
}

.chat-send { display: flex; gap: 8px; margin-top: 10px; }
.chat-input-wrap { position: relative; flex: 1 1 auto; min-width: 0; }
.chat-input-wrap .text-input {
  display: block; width: 100%; min-height: 42px; max-height: 112px;
  resize: none; overflow-y: auto; padding-right: 9px;
}
.chat-mentions {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 7px);
  max-height: 210px; overflow-y: auto; padding: 5px;
  border: 1px solid rgba(13, 27, 40, 0.25); border-radius: 10px;
  background: rgba(238, 250, 255, 0.98); box-shadow: 0 10px 24px rgba(0, 0, 0, .22);
}
.chat-mention-option {
  display: flex; flex-direction: column; align-items: flex-start; width: 100%;
  padding: 8px 9px; border-radius: 7px; color: var(--ink); text-align: left;
}
.chat-mention-option b { font-size: 13px; }
.chat-mention-option small { color: rgba(13, 27, 40, 0.58); font-size: 11px; }
.chat-mention-option:active, .chat-mention-option:hover { background: rgba(53, 196, 255, 0.2); }
.chat-emoji-wrap { position: relative; flex: 0 0 auto; }
.chat-emoji {
  width: 42px; height: 42px; border-radius: 10px;
  background: rgba(255, 255, 255, 0.72); color: var(--ink);
  border: 1px solid rgba(13, 27, 40, 0.28); font-size: 21px;
}
.chat-emoji:active { background: rgba(255, 255, 255, 0.95); }
.emoji-picker {
  position: absolute; right: 0; bottom: calc(100% + 7px); z-index: 5;
  display: grid; grid-template-columns: repeat(6, 38px); gap: 4px; padding: 7px;
  border: 1px solid rgba(13, 27, 40, 0.25); border-radius: 10px;
  background: rgba(238, 250, 255, 0.98); box-shadow: 0 10px 24px rgba(0, 0, 0, .22);
}
.emoji-picker button { width: 38px; height: 36px; border-radius: 7px; font-size: 21px; }
.emoji-picker button:active, .emoji-picker button:hover { background: rgba(53, 196, 255, 0.2); }

/* ------------------------------------------------------------ sea picker */
/* Shut, the control is the answer it gives: a disc of the water and its hex. The wheel
   is two hundred and fifty pixels and only wanted while it is being used. */
/* The block owns the whole row: the label and the swatch share its first line, and the
   wheel drops underneath with the full width to draw into. */
.sea-row { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.sea-head { display: flex; align-items: center; gap: 12px; }
.sea-head .opt-l { flex: 1; }
.sea-wheel { width: 100%; }
/* iro draws at a fixed 210 and does not reflow, so the box it sits in must never be
   narrower than that plus its own padding. */
.sea-iro { min-width: 210px; }
.sea-toggle {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 10px 6px 6px; border-radius: 999px;
  background: rgba(255, 255, 255, .06); border: 1px solid var(--line);
}
.sea-toggle:active { background: rgba(255, 255, 255, .12); }
.sea-wheel-preview {
  width: 30px; height: 30px; flex: 0 0 auto; border-radius: 50%;
  background: linear-gradient(180deg, var(--a), var(--b));
  border: 1px solid rgba(255, 255, 255, .35);
}
.sea-hex {
  font-size: 12px; font-weight: 700; color: var(--dim);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}
.sea-caret { width: 16px; height: 16px; color: var(--dim); transition: transform .18s; }
.sea-row.open .sea-caret { transform: rotate(180deg); }

.sea-wheel {
  margin-top: 10px; padding: 12px; border-radius: var(--r);
  background: rgba(0, 0, 0, .18); border: 1px solid var(--line);
}
.sea-iro { min-height: 236px; display: flex; justify-content: center; }

/* What a tile produces, shown when one is tapped. Lifted clear of the buttons along the
   bottom — it used to sit at the same corner as the left-hand pair and was covered by
   them — and centred, because it is about the tile under your finger rather than about
   anything in a corner. */
.tile-tip {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 66px;
  white-space: nowrap; max-width: calc(100% - 24px); overflow: hidden; text-overflow: ellipsis;
  background: rgba(5, 18, 32, 0.9);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 7px 12px;
  font-size: 12.5px;
  pointer-events: none;
}

/* ------------------------------------------------------------ score strip */
/* Names need enough room to be read. Two equal columns keep a four-player game compact
   while leaving every player enough width for a real name, rather than four tiny chips
   whose labels are all ellipses. */
.score-strip {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
}
#btn-trade-filter svg { width: 24px; height: 24px; color: #fff; }
.trade-filter-list { display: flex; flex-direction: column; gap: 7px; }
.trade-filter-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center;
  gap: 8px; width: 100%; padding: 10px 11px; text-align: left;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .05);
}
.trade-filter-row.on { border-color: rgba(229, 72, 77, .55); background: rgba(229, 72, 77, .1); }
.trade-filter-name { min-width: 0; font-weight: 700; overflow-wrap: anywhere; }
.trade-filter-state { color: var(--dim); font-size: 11px; white-space: nowrap; }
.trade-filter-row.on .trade-filter-state { color: #ffabad; }
.trade-filter-row .tog { flex: 0 0 auto; }

.chip {
  position: relative;
  display: flex; align-items: center; gap: 6px;
  width: 100%;
  padding: 4px 8px;
  border-radius: 999px;
  /* The chip IS the player's colour — quicker to find yourself than reading names. */
  background: var(--c, #888);
  color: var(--ink, #fff);
  border: 1px solid rgba(0, 0, 0, 0.35);
  min-width: 0;
  font-size: 12px;
  opacity: 0.72;
  transition: opacity .18s, box-shadow .18s, transform .18s;
}
/* Whose turn it is: the same white ring and the same pulse as the border around the
   whole window on your own turn — that ring only ever shows on the active player's own
   screen, so this is the one place everyone else at the table gets the same cue, in the
   same language. Narrower than the screen ring, though: 5px reads clearly on a pill
   this small without swallowing it the way 8px did.
   The ring itself is a static box-shadow, on the chip and on a ::after sitting over it
   with the brighter version baked in — only the ::after's opacity animates. Animating
   box-shadow directly repaints the chip on every frame of every turn, all game; opacity
   is compositor-only, so the same pulse costs nothing once it's past the first paint. */
.chip.up {
  opacity: 1;
  transform: translateY(-1px);
  box-shadow: 0 0 0 5px rgba(255, 255, 255, .34), 0 0 12px 5px rgba(255, 255, 255, .17), 0 3px 10px rgba(0, 0, 0, 0.4);
}
.chip.up::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 0 5px #fff, 0 0 12px 5px rgba(255, 255, 255, .5), 0 3px 10px rgba(0, 0, 0, 0.4);
  opacity: 0;
  animation: chip-turn-pulse 1.15s ease-in-out infinite;
  pointer-events: none;
}
@keyframes chip-turn-pulse { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
.chip-body {
  display: flex; flex-wrap: wrap; align-items: center; align-content: center; gap: 2px 5px;
  flex: 1 1 auto;
  min-width: 0;
}
/* The name owns a full line. It may wrap, but it never becomes an unhelpful ellipsis. */
.chip-name {
  flex: 1 0 100%;
  min-width: 0;
  overflow-wrap: anywhere;
  line-height: 1.1;
  font-weight: 700;
}
/* The score, the same chit used for a tile's number token on the board — a cream disc,
   dark serif-weight digits — so the number reads as the same kind of number wherever it
   shows up. Stretched to the chip's own height and kept a perfect circle by its
   aspect-ratio, so it just fills the pill rather than floating inside it at some
   arbitrary size. */
.chip-vp {
  align-self: stretch;
  aspect-ratio: 1 / 1;
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: #f3e6cb;
  color: #3b2f1c;
  border: 1.5px solid rgba(60, 44, 20, 0.55);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  font-family: ui-rounded, "Segoe UI", system-ui, sans-serif;
  font-weight: 700;
  font-size: 1.35em;
  line-height: 1;
}
/* The hand-card count, with a little card face beside it — the game's own mark
   watermarked in — rather than the bare glyph it used to sit next to. */
.chip-cards {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 13px; font-weight: 700; opacity: 0.9;
}
.chip-cards-card {
  position: relative;
  width: 11px; height: 15px;
  border-radius: 2.5px;
  background: linear-gradient(155deg, #f6ecd6, #e2d2a6);
  border: 1px solid rgba(60, 44, 20, 0.55);
  overflow: hidden;
  flex: 0 0 auto;
}
.chip-cards-card::before {
  content: '';
  position: absolute; inset: 0;
  background: url('icons/icon-192.png') center / 75% no-repeat;
  opacity: 0.4;
}
/* Who holds the two awards, on the chip that already carries their name and score.
   It was a bare emoji at eleven pixels sitting on the player's own colour, which is
   about as close to invisible as a two-point swing gets. A pale disc with a gold ring
   reads as something awarded, and separates it from the numbers either side. */
.chip-crown {
  font-size: 10px; line-height: 1;
  display: inline-grid; place-items: center;
  width: 17px; height: 17px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 0 0 1.5px var(--gold);
  flex: 0 0 auto;
  /* A fixed dark ink of its own — the disc is always white, whatever the chip's own ink
     is doing, so the glyph on it needs a colour of its own too. Left to inherit, a
     player whose chip ink is white drew a white glyph on this white disc and vanished. */
  color: #0d1b28;
}
.chip-crown + .chip-crown { margin-left: 2px; }
.chip-crown svg { display: block; }
/* The awards live in their own span so the chip can be updated in place, but they should
   sit in the chip's flex row exactly as they did when they were written straight into it —
   and an empty wrapper must not leave a gap behind on a chip that has won nothing. */
.chip-awards { display: contents; }

/* More players still use the same readable two-column rhythm. */
.score-strip.tight { gap: 4px; }
.score-strip.tight .chip {
  padding: 4px 8px; gap: 2px 5px;
  font-size: 12px;
  min-width: 0; max-width: none;
}
.score-strip.tight .chip-cards { font-size: 10.5px; }
.score-strip.tight .chip-crown { font-size: 9px; width: 15px; height: 15px; }

/* ------------------------------------------------------------ resource cards */
/* The card faces are the board's own tile illustrations. Those files are cropped to the
   hexagon's BOUNDING BOX, so their four corners hold the printed cream border — fine on
   the board, where the hex mask cuts them off, but it would show on a rectangular card.
   Hence the zoom: a pointy-top hexagon is only full width across the middle half of its
   height, so showing at most half the image's height guarantees nothing but artwork.
   205% of the card height lands just inside that, with a little to spare. */
.rcard-wrap {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  flex: 0 0 auto;
}

.rcard {
  position: relative;
  width: 52px; height: 70px;
  border-radius: 9px;
  padding: 3px;
  background: var(--rc, #6b7a8c);
  box-shadow: 0 2px 0 rgba(0, 0, 0, .35), 0 4px 10px rgba(0, 0, 0, .35);
  border: 1px solid rgba(0, 0, 0, .35);
  display: block;
  transition: transform .12s, box-shadow .15s, filter .15s;
}
.rcard-face {
  display: block;
  width: 100%; height: 100%;
  border-radius: 6px;
  background-color: var(--rc, #6b7a8c);
  background-image: var(--rc-art);
  background-size: auto 205%;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .35);
}

/* Sizes. Everything scales off the same box so a cost, a hand and a trade all match. */
.rcard--xs { width: 30px; height: 40px; border-radius: 6px; padding: 2px; }
.rcard--xs .rcard-face { border-radius: 4px; }
.rcard--sm { width: 42px; height: 56px; border-radius: 8px; }
.rcard--lg { width: 62px; height: 84px; }
.rcard--xl { width: 108px; height: 146px; border-radius: 14px; }
.rcard--xl .rcard-face { border-radius: 11px; }

/* The count sits in the corner the way it does on a real card. */
.rcard-count {
  position: absolute; top: -6px; right: -6px;
  min-width: 21px; height: 21px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--gold);
  color: #2a1e08;
  font-size: 12.5px; font-weight: 800;
  line-height: 21px; text-align: center;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .45);
  font-variant-numeric: tabular-nums;
}
.rcard--xs .rcard-count {
  top: -5px; right: -5px; min-width: 17px; height: 17px; line-height: 17px; font-size: 10.5px;
}

/* The cards behind, so a stack looks like a stack. */
.rcard-edge {
  position: absolute;
  top: 3px; bottom: 3px;
  left: calc(var(--i) * -4px);
  width: 12px;
  border-radius: 8px 0 0 8px;
  background: var(--rc, #6b7a8c);
  border: 1px solid rgba(0, 0, 0, .35);
  filter: brightness(calc(1 - var(--i) * 0.12));
  z-index: 0;
}
.rcard { position: relative; z-index: 1; }

.rcard-label {
  margin-top: 5px;
  font-size: 9.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em;
  color: var(--dim);
}
.rcard-none { color: var(--dim); font-size: 13px; font-style: italic; }

.rcard.is-dim { filter: grayscale(.7) brightness(.55); box-shadow: none; }
.rcard.is-selected {
  box-shadow: 0 0 0 3px var(--gold), 0 4px 12px rgba(0, 0, 0, .45);
  transform: translateY(-4px);
}

/* Tappable cards. */
button.rcard-wrap, .rcard-wrap[role="button"] { cursor: pointer; }
button.rcard-wrap:active .rcard { transform: scale(.94); }

/* The one card with no tile behind it. */
.rcard--dev { --rc: #2f7fd0; }
.rcard-face--dev {
  background-image: none;
  background: linear-gradient(160deg, #3d8fe0, #1b528c);
  color: rgba(255, 255, 255, .92);
  font-size: 26px; font-weight: 800;
  display: grid; place-items: center;
}
.rcard--xs .rcard-face--dev { font-size: 16px; }

/* Fallback colours, and the artwork each face uses. */
.rcard--wood  { --rc: #2f6b3a; --rc-art: url('art/wood.jpg'); }
.rcard--brick { --rc: #b8613a; --rc-art: url('art/brick.jpg'); }
.rcard--sheep { --rc: #78bf5c; --rc-art: url('art/sheep.jpg'); }
.rcard--wheat { --rc: #c9963a; --rc-art: url('art/wheat.jpg'); }
.rcard--ore   { --rc: #7f8a99; --rc-art: url('art/ore.jpg'); }

/* ------------------------------------------------------------ tray */
/* `relative` so the trades can hang off the top of it without being part of it. */
.tray {
  position: relative;
  flex-shrink: 0;
  background: rgba(5, 18, 32, 0.94);
  border-top: 1px solid var(--line);
  padding: 9px 10px calc(var(--safe-b) + 9px);
  display: flex; flex-direction: column; gap: 8px;
  backdrop-filter: blur(10px);
}
/* Whose turn it is, on the one part of the screen your thumbs are already resting on —
   moved here from a ring around the whole window, which nobody looking at their own
   hand of cards was actually seeing. Same white, same pulse.
   Same fix as the chip's turn ring: a static box-shadow plus a ::after with the bright
   version baked in, only its opacity animating, so the pulse repaints nothing for as
   long as it runs — which for this one is the whole time it's your turn. */
.tray.my-turn {
  z-index: 6;
  box-shadow: inset 0 0 0 5px rgba(255, 255, 255, .34), 0 0 20px 2px rgba(255, 255, 255, .25);
}
.tray.my-turn::after {
  content: '';
  position: absolute; inset: 0;
  box-shadow: inset 0 0 0 5px #fff, 0 0 26px 4px rgba(255, 255, 255, .55);
  opacity: 0;
  animation: tray-turn-pulse 1.15s ease-in-out infinite;
  pointer-events: none;
}
@keyframes tray-turn-pulse { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }

.hand {
  display: flex; gap: 12px; justify-content: center; align-items: flex-end;
  flex-wrap: wrap;
  padding: 8px 4px 2px;          /* room for the corner badges to sit outside the card */
}
/* A card that just arrived jumps, so a payout is visible without reading numbers. */
.rcard-wrap.bump { animation: bump .45s cubic-bezier(.2, 1.5, .4, 1); }
@keyframes bump { 0% { transform: translateY(0); } 40% { transform: translateY(-9px) scale(1.1); } }

.actions { display: flex; gap: 7px; }
.act {
  /* Equal shares, always. `flex: 1` alone is not enough: a flex item's automatic minimum
     size stops it shrinking below its own content, so once the bar is tighter than the
     four labels need, they stop sharing and each keeps whatever its own word happens to
     measure — "Players" wide, "DEV" narrow. Measured, they part company below about
     240px. `min-width: 0` removes that floor so the split stays even at any width. */
  flex: 1;
  min-width: 0;
  border-radius: 13px;
  padding: 11px 5px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  font-size: 11.5px; font-weight: 700;
  transition: transform .12s, opacity .15s;
}
.act:active { transform: scale(0.95); }
.act:disabled { opacity: 0.35; pointer-events: none; }
/* The icons are SVG now, not glyphs, so these size the box rather than the type. `block`
   matters: an inline SVG sits on the text baseline and leaves a few pixels of descender
   space under it, which is exactly the misalignment the emoji always had. */
.act-ico { line-height: 0; display: grid; place-items: center; }
.act-ico svg { display: block; }
.act.primary {
  background: linear-gradient(180deg, var(--sky), var(--sky-2));
  color: var(--sky-ink);
  border-color: transparent;
  box-shadow: 0 4px 0 #926b23;
}
.act.primary:active { box-shadow: 0 1px 0 #926b23; }
.act.wide { flex: 2; }
.act .badge {
  position: absolute; top: -5px; right: -5px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 999px; background: var(--danger); color: #fff;
  font-size: 10px; font-weight: 800;
  display: grid; place-items: center;
}
.act { position: relative; }

/* ------------------------------------------------------------ map picker */
.map-bar { display: flex; gap: 7px; align-items: stretch; }
.map-step {
  flex: 0 0 auto;
  min-width: 58px;
  border-radius: 13px;
  padding: 11px 8px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-size: 11px; font-weight: 700;
}
.map-step:disabled { opacity: 0.35; pointer-events: none; }
.map-step svg { width: 20px; height: 20px; }
.map-accept {
  flex: 1;
  border-radius: 13px;
  background: linear-gradient(180deg, var(--sky), var(--sky-2));
  color: var(--sky-ink);
  border: 0;
  box-shadow: 0 4px 0 var(--sky-shade);
  font-weight: 800; font-size: 15px;
}
.map-accept:active { box-shadow: 0 1px 0 #926b23; transform: scale(0.98); }
.map-count {
  text-align: center;
  font-size: 12px;
  color: var(--dim);
  padding-bottom: 2px;
}

/* ------------------------------------------------------------ sheets */
.veil {
  position: fixed; inset: 0;
  background: rgba(2, 8, 16, 0.66);
  backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none;
  transition: opacity .22s;
  z-index: 40;
}
.veil.show { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 50;
  max-height: 86vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel-2);
  border-top: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 18px 16px calc(var(--safe-b) + 18px);
  transform: translateY(102%);
  box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.5);

  /* Parked off the bottom of the screen is not the same as gone. Every sheet casts its
     shadow UPWARD (0 -14px 40px), so all fifteen of them were bleeding a dark band back
     onto the bottom of the page, stacking into a very visible blob on the home screen.
     visibility:hidden stops a closed sheet painting at all — and takes it out of the tab
     order while we are here. The zero-length transition is delayed by the length of the
     slide, so a sheet stays visible while it animates out and only then disappears. */
  visibility: hidden;
  transition: transform .28s cubic-bezier(.2, .9, .3, 1), visibility 0s linear .28s;
  max-width: 560px;
  margin: 0 auto;
}
/* The sheets the game actually waits on sit in the gap between the board's status row
   and the tray, rather than over the top of both.
   Below them your own cards stay on screen, which is the whole of the decision when you
   are choosing what to discard or which two to take from the bank. Above them the turn
   clock stays on screen, which matters because all three of these are timed.
   The top reserve is the topbar, the status row and a little air — safe area + 56 + 10 +
   34 + 8 — and the bottom is however tall the tray is right now, published from JS
   because trade mode changes it. */
#sheet-discard, #sheet-steal, #sheet-pickres {
  bottom: var(--tray-h, 0px);
  max-height: calc(var(--app-height) - var(--safe-t) - 108px - var(--tray-h, 0px));
  /* No longer flush with the bottom of the screen, so it is a card rather than a drawer,
     and it does not need to pad past a home indicator the tray is already clearing. */
  border-radius: var(--r-lg);
  border-bottom: 1px solid var(--line);
  padding-bottom: 16px;
  box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.45), 0 10px 30px rgba(0, 0, 0, 0.45);
}

.sheet.show {
  transform: translateY(0);
  visibility: visible;
  transition: transform .28s cubic-bezier(.2, .9, .3, 1), visibility 0s;
}

.sheet-title { margin: 0 0 4px; font-size: 19px; font-weight: 800; }
.sheet-sub { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
.sheet-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }

/* Numbered instructions — used by the iOS "Add to Home Screen" sheet. The counters are
   drawn rather than left to the browser, so they read as part of the app instead of as
   a bare ordered list. */
.how-list {
  counter-reset: how;
  list-style: none;
  margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 10px;
}
.how-list li {
  counter-increment: how;
  position: relative;
  padding: 10px 12px 10px 42px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--panel-2);
  font-size: 14px; line-height: 1.45;
}
.how-list li::before {
  content: counter(how);
  position: absolute; left: 11px; top: 10px;
  width: 21px; height: 21px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--gold); color: var(--ink);
  font-size: 12px; font-weight: 800;
}
.how-list b { color: var(--gold); font-weight: 700; }
.how-note {
  margin-top: 12px;
  color: var(--dim);
  font-size: 12.5px; line-height: 1.5;
}

/* ------------------------------------------------------------ build sheet */
.build-cost { display: flex; gap: 7px; margin-top: 8px; padding-top: 4px; }

/* ------------------------------------------------------------ dev cards */
.dev-list { display: flex; flex-direction: column; gap: 9px; }
.dev-card {
  display: flex; align-items: center; gap: 12px;
  padding: 12px;
  border-radius: var(--r);
  background: linear-gradient(135deg, rgba(227, 186, 87, 0.14), rgba(255, 255, 255, 0.04));
  border: 1px solid rgba(227, 186, 87, 0.28);
  text-align: left; width: 100%;
}
.dev-card:disabled { opacity: 0.45; pointer-events: none; }
.dev-n {
  min-width: 30px; height: 30px; border-radius: 9px;
  background: rgba(227, 186, 87, 0.85); color: #2a1e08;
  font-weight: 800; display: grid; place-items: center;
}
.dev-txt { flex: 1; }
.dev-name { font-weight: 700; }
.dev-blurb { font-size: 12px; color: var(--dim); margin-top: 2px; }

.dev-buy {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  padding: 13px 12px;
  margin-bottom: 12px;
  border-radius: var(--r);
  border: 1px solid rgba(227, 186, 87, 0.45);
  background: linear-gradient(180deg, rgba(227, 186, 87, 0.16), rgba(227, 186, 87, 0.06));
  text-align: left;
}
.dev-buy:disabled { opacity: 0.45; border-color: var(--line); background: rgba(255, 255, 255, 0.04); }
.dev-buy:not(:disabled):active { transform: scale(0.985); }
.dev-buy-ico { font-size: 22px; flex: 0 0 auto; }
.dev-buy-txt { display: flex; flex-direction: column; gap: 5px; flex: 1; }
.dev-buy-name { font-size: 14px; font-weight: 800; }
.dev-buy-left {
  font-size: 10px; color: var(--dim);
  text-transform: uppercase; letter-spacing: .05em;
  flex: 0 0 auto;
}

/* An action you can afford right now, distinct from one that is merely tappable. */
.act.ready {
  border-color: rgba(227, 186, 87, 0.55);
  background: rgba(227, 186, 87, 0.14);
}
.act.ready .act-ico { filter: drop-shadow(0 0 6px rgba(227, 186, 87, 0.6)); }
.dev-lock { font-size: 11px; color: var(--gold); margin-top: 3px; }

/* ------------------------------------------------------------ trading */
.res-picker {
  display: flex; gap: 8px; justify-content: center; margin-bottom: 14px;
  padding-top: 8px;               /* the corner badges overhang the cards */
}
.pick-col { display: flex; flex-direction: column; align-items: center; gap: 6px; flex: 1; }
.pick-plain { padding: 0; background: none; border: 0; }
.pick-have { font-size: 9.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .04em; }
.pick-pm { display: flex; gap: 3px; width: 100%; }
.pick-pm button {
  flex: 1; height: 30px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid var(--line);
  font-weight: 800; font-size: 16px;
  display: grid; place-items: center;
}
.pick-pm button:active { background: rgba(255, 255, 255, 0.18); }

.offer-cards { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 4px 0; }
.offer-replies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 5px;
}
.reply-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 7px;
  padding: 7px 8px; border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line);
  width: 100%;
}
.reply-row.yes { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.1); }
.reply-row.no { opacity: 0.5; }
.reply-name { flex: 1 1 auto; min-width: 0; font-weight: 600; text-align: left; overflow-wrap: anywhere; }
.reply-tag { font-size: 11.5px; font-weight: 700; color: var(--dim); }
.reply-row.yes .reply-tag { color: #7ee2a8; }

/* ------------------------------------------------------------ steal + players */
.steal-list { display: flex; flex-direction: column; gap: 8px; }
.steal-btn {
  display: flex; flex-wrap: wrap; align-items: center; gap: 11px;
  padding: 12px; border-radius: var(--r);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  border-left: 5px solid var(--c, #888);
  width: 100%;
}
.steal-name { flex: 1 1 auto; min-width: 0; font-weight: 700; text-align: left; overflow-wrap: anywhere; }
.steal-stats { display: flex; gap: 6px; flex: 0 0 auto; }
.steal-stat {
  font-size: 11.5px; font-weight: 700;
  padding: 3px 9px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--dim);
  white-space: nowrap;
}
/* An empty hand has nothing in it to take — worth saying louder than "0". */
.steal-stat.none { color: #ff9fa1; background: rgba(229, 72, 77, 0.14); }
/* Whoever is furthest ahead of the people on this tile. The number alone makes you do
   the comparing; this does it for you, which is the whole job of the sheet. */
.steal-stat.lead { color: var(--gold); background: rgba(227, 186, 87, 0.18); }

.player-cards { display: flex; flex-direction: column; gap: 9px; }
.pcard {
  padding: 12px; border-radius: var(--r);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line);
  border-left: 5px solid var(--c, #888);
}
.pcard-top { display: flex; align-items: center; gap: 9px; }
.pcard-name { flex: 1; font-weight: 700; }
.pcard-vp { font-weight: 800; font-size: 17px; }
.pcard-stats {
  display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px;
}
/* Icons set inline with numbers — the turn badge, the players sheet, the score
   breakdown, the cost list. Nudged down slightly because a 24-unit box centres above the
   x-height of the text beside it. */
#turn-badge svg, .pstat svg, .score-row svg, .cost-ico svg, .history-player svg {
  display: inline-block; vertical-align: -0.18em;
}
.cost-ico { line-height: 0; }

.pstat {
  font-size: 11.5px; padding: 3px 8px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.08); color: var(--dim);
}
.pstat.award { background: rgba(227, 186, 87, 0.2); color: var(--gold); font-weight: 700; }

/* ------------------------------------------------------------ log */
.log-list { display: flex; flex-direction: column-reverse; gap: 6px; max-height: 56vh; overflow-y: auto; }
.log-scrub {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 10px;
}
.log-scrub-btn {
  width: 34px; height: 34px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  display: grid; place-items: center;
  flex: 0 0 auto;
}
.log-scrub-btn svg { width: 18px; height: 18px; }
.log-scrub-btn:disabled { opacity: 0.3; pointer-events: none; }
.log-scrub-label {
  flex: 1;
  padding: 7px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  font-size: 13px; font-weight: 700; text-align: center;
}
.log-scrub-label:active { background: rgba(255, 255, 255, 0.12); }
.log-row {
  font-size: 13px; padding: 8px 11px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.04);
  border-left: 3px solid var(--c, transparent);
}
.log-row b { font-weight: 700; }
.log-row .g { color: #7ee2a8; }
.log-row .r { color: #ff9fa1; }

/* ------------------------------------------------------------ menus + misc */
.menu-list { display: flex; flex-direction: column; gap: 8px; }
.menu-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 13px; border-radius: var(--r);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  width: 100%;
}
.menu-item b { font-weight: 700; }
.menu-item i { font-style: normal; font-size: 12px; color: var(--dim); }
.menu-item.danger b { color: #ff9fa1; }
.pause-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pause-status {
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: rgba(255, 255, 255, 0.06);
  text-align: center;
}
.pause-status strong { display: block; color: var(--gold); font-size: 19px; }
.pause-status small { display: block; margin-top: 4px; color: var(--dim); }
.pause-accepts { margin-top: 10px; color: var(--dim); font-size: 13px; }

.history-summary, .history-list { display: flex; flex-direction: column; gap: 7px; }
.history-list { margin-top: 18px; }
.history-heading {
  margin: 2px 0 3px;
  color: var(--gold);
  font-size: 11px; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase;
}
.history-total, .history-game {
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: rgba(255, 255, 255, 0.05);
  padding: 9px 10px;
}
.history-total { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; align-items: baseline; }
.history-total b { color: var(--gold); }
.history-total i { grid-column: 1 / -1; color: var(--dim); font-size: 11px; font-style: normal; }
.history-game-top { display: flex; justify-content: space-between; gap: 8px; }
.history-game-top b { color: var(--sky); }
.history-game-top span { color: var(--dim); font-size: 11px; white-space: nowrap; }
.history-players { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }
.history-player { padding: 3px 7px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); font-size: 12px; }
.history-player.winner { background: rgba(227, 186, 87, 0.18); color: var(--gold); }

/* The career card: a handful of stat tiles above the ordinary win-totals list, present
   only once there are enough games on this device for any of it to mean something. */
.my-stats:empty { display: none; }
.my-stats { margin-bottom: 18px; }
.my-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.my-stat {
  display: flex; flex-direction: column; gap: 2px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: rgba(255, 255, 255, 0.05);
  padding: 10px 12px;
}
.my-stat b { font-size: 20px; color: var(--sky); }
.my-stat span { font-size: 11px; color: var(--dim); }

.toggle-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0; border-top: 1px solid var(--line);
}
.toggle-row:first-of-type { border-top: 0; }
.toggle-row span { flex: 1; font-weight: 600; display: flex; flex-direction: column; gap: 2px; }
.toggle-row span em { font-style: normal; font-weight: 500; font-size: 12px; color: var(--dim); }
.tog {
  width: 50px; height: 29px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  position: relative; flex-shrink: 0;
  transition: background .2s;
}
.tog i {
  position: absolute; top: 3px; left: 3px;
  width: 23px; height: 23px; border-radius: 50%;
  background: #fff;
  transition: transform .2s;
}
.tog.on { background: var(--sky); }
.tog.on i { transform: translateX(21px); }

.set-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--dim); font-weight: 700; margin: 18px 0 6px;
}
.about-p { font-size: 13px; color: var(--dim); margin: 0 0 10px; }
.ver { text-align: center; font-size: 11.5px; color: #5f778e; margin-top: auto; padding-top: 14px; }

.how-body p { font-size: 13.5px; color: #c9d8e6; margin: 0 0 12px; }
.how-body b { color: var(--text); }
.how-costs {
  display: flex; flex-direction: column; gap: 12px;
  margin: 0 0 14px; padding: 12px;
  border-radius: var(--r); background: rgba(0, 0, 0, 0.25);
  border: 1px solid var(--line);
}
.cost-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.cost-name { flex: 1; font-weight: 700; }
.cost-bits { display: flex; gap: 7px; padding-top: 4px; }

.over-hero { text-align: center; padding: 10px 0 16px; }
.over-name { font-size: 24px; font-weight: 800; margin-top: 6px; }
.over-sub { color: var(--dim); font-size: 13px; margin-top: 3px; }

.final-table { display: flex; flex-direction: column; gap: 7px; }
.final-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: var(--r-sm);
  background: rgba(255, 255, 255, 0.05);
  border-left: 4px solid var(--c, #888);
}
.final-row.win { background: rgba(227, 186, 87, 0.14); }
.final-pos { width: 20px; font-weight: 800; color: var(--dim); }
.final-name { flex: 1; font-weight: 700; }
.final-vp { font-weight: 800; font-size: 17px; }

/* ------------------------------------------------------------ chrome */
/* Whose turn it is, said out loud on every screen for three seconds.
   It sits over the board but cannot be touched — pointer-events stay off the whole
   layer, so a shoutout can never swallow the tap that places a road. Held clear of the
   middle of the island as well, so the piece somebody is watching for is not hidden by
   the announcement that they may place it. */
.shoutout {
  position: fixed; inset: 0;
  display: flex; justify-content: center; align-items: flex-start;
  padding-top: calc(var(--safe-t) + 22vh);
  z-index: 94;
  pointer-events: none;
}
.shoutout-card {
  --c: var(--gold);
  max-width: calc(100vw - 40px);
  background: rgba(8, 24, 40, 0.94);
  border: 2px solid var(--c);
  border-radius: 999px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 0 0 6px rgba(0, 0, 0, 0.18);
  padding: 14px 26px;
  font-size: 22px; font-weight: 800; letter-spacing: 0.2px;
  color: var(--parch);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  white-space: normal; overflow: visible;
  opacity: 0;
}
.shoutout-line {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 1.1em; white-space: nowrap;
}
.shoutout-resource { display: inline-flex; align-items: center; height: 40px; }
.shoutout-resource .rcard-wrap { transform: scale(0.82); transform-origin: center; }
.shoutout-bigcard { display: block; margin: 6px 0 2px; }
.shoutout-card.show { animation: shoutout 3s ease forwards; }

@keyframes shoutout {
  0%   { opacity: 0; transform: translateY(12px) scale(0.92); }
  8%   { opacity: 1; transform: translateY(0) scale(1); }
  84%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-8px) scale(0.97); }
}

/* A shoutout that cannot move still has to say its piece, so it fades rather than
   flies. Three seconds of a motionless card is the whole point of it. */
@media (prefers-reduced-motion: reduce) {
  @keyframes shoutout {
    0%   { opacity: 0; transform: none; }
    8%   { opacity: 1; transform: none; }
    84%  { opacity: 1; transform: none; }
    100% { opacity: 0; transform: none; }
  }

  /* The throw and the payout still happen — they are how you find out what was rolled
     and what you were paid — but they stop travelling. The dice appear where they land
     and the cards fade rather than flying, so the information survives and the motion
     does not. The timings are untouched, because the JavaScript is counting on them to
     know when to take each stage down. */
  @keyframes die-spin { from { opacity: 0; } to { opacity: 1; transform: none; } }
  @keyframes die-settle { to { opacity: 0; transform: none; } }
  @keyframes gain-in { from { opacity: 0; } to { opacity: 1; transform: none; } }
  @keyframes gain-away { to { opacity: 0; transform: none; } }
  @keyframes gain-stolen { to { opacity: 0; transform: none; } }
  @keyframes trade-duel-in { from { opacity: 0; transform: none; } }
  @keyframes trade-duel-out { to { opacity: 0; transform: none; } }
  @keyframes trade-duel-pulse { 0%, 100% { transform: none; } }
  @keyframes reaction-rise { 0% { opacity: 0; transform: none; } 18%, 70% { opacity: 1; transform: none; } 100% { opacity: 0; transform: none; } }
  @keyframes undo-in { from { opacity: 0; transform: translateX(-50%); } }
  @keyframes award-take-in { from { opacity: 0; transform: none; } }
  @keyframes award-in { from { opacity: 0; transform: none; } }
  @keyframes award-out { to { opacity: 0; transform: none; } }
  /* Still there, still your colour, just not flashing — the ::after's bright ring held
     fully visible rather than fading in and out. */
  @keyframes chip-turn-pulse { 0%, 100% { opacity: 1; } }
  @keyframes tray-turn-pulse { 0%, 100% { opacity: 1; } }
}

/* Over the map, under the status row.
   It used to sit just above the tray, at the bottom of the screen — which is where your
   own cards and buttons are, and where your thumbs already are covering it. "Dag offered
   you a trade" is news about the board, so it is said over the board, in the space the
   island can spare and the eye is already crossing. */
.toast {
  position: fixed; left: 50%; top: calc(var(--safe-t) + 108px);
  transform: translateX(-50%) translateY(-14px);
  background: rgba(8, 24, 40, 0.96);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 10px 18px;
  font-size: 13.5px; font-weight: 600;
  z-index: 90;
  opacity: 0; pointer-events: none;
  transition: opacity .2s, transform .2s;
  max-width: calc(100vw - 32px);
  text-align: center;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* Clear of the turn shoutout, which comes down from 22vh — a toast and a shoutout can
   land together, and a trade offer raises both at once. */
.shoutout { padding-top: calc(var(--safe-t) + 24vh); }

.conn-badge {
  position: fixed; top: calc(var(--safe-t) + 8px); left: 50%;
  transform: translateX(-50%);
  background: rgba(229, 72, 77, 0.92);
  border-radius: 999px; padding: 5px 14px;
  font-size: 12px; font-weight: 700;
  z-index: 95;
}
.banner {
  position: fixed; left: 12px; right: 12px; bottom: calc(var(--safe-b) + 12px);
  display: none; align-items: center; gap: 10px;
  background: rgba(8, 24, 40, 0.97);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 11px 13px;
  z-index: 92;
  max-width: 536px; margin: 0 auto;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.banner.show { display: flex; }
/* Both banners are pinned to the same spot, so without this the install banner — which
   comes later in the DOM — paints on top of the update banner and swallows its taps. */
#update-banner { z-index: 94; cursor: pointer; }
#update-banner span { text-decoration: underline; text-underline-offset: 3px; }
.banner span { flex: 1; font-size: 13.5px; font-weight: 600; }
.banner button {
  background: var(--sky); color: var(--sky-ink);
  border-radius: 9px; padding: 7px 13px; font-weight: 700; font-size: 13px;
}
.banner .banner-x { background: none; color: var(--dim); padding: 6px 4px; }

/* A standing instruction in the tray. The turn badge sits at the top of the board and
   is easy to miss on a phone — the tray is where a player's eyes and thumbs already
   are, so anything the game is waiting on has to be said here too. */
.act-prompt {
  position: relative;
  flex: 1;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  border-radius: 13px;
  padding: 12px 10px;
  font-size: 13.5px; font-weight: 700;
  background: linear-gradient(180deg, rgba(227, 186, 87, 0.22), rgba(227, 186, 87, 0.1));
  border: 1px solid rgba(227, 186, 87, 0.5);
  color: var(--gold);
}
/* Same fix as the turn ring: the brighter border lives on a ::after, cross-fading in on
   opacity, so the animation doesn't repaint the prompt on every frame while it's shown. */
.act-prompt::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(227, 186, 87, 0.95);
  opacity: 0;
  animation: prompt-pulse 1.7s ease-in-out infinite;
  pointer-events: none;
}
@keyframes prompt-pulse { 50% { opacity: 1; } }
.act-prompt .act-ico { flex: 0 0 auto; }

/* ------------------------------------------------------------ kebab menu */
.kebab-wrap {
  position: fixed;
  top: calc(var(--safe-t) + 10px);
  right: 12px;
  z-index: 70;
}
.kebab-btn {
  width: 42px; height: 42px;
  display: grid; place-items: center;
  border-radius: 12px;
  background: rgba(5, 18, 32, 0.72);
  border: 1px solid var(--line);
  color: var(--text);
  backdrop-filter: blur(8px);
}
.kebab-btn svg { width: 22px; height: 22px; fill: currentColor; }
.kebab-btn:active { background: rgba(5, 18, 32, 0.92); }

.kebab-menu {
  position: absolute;
  top: 48px; right: 0;
  min-width: 172px;
  padding: 6px;
  border-radius: var(--r);
  background: var(--panel-2);
  border: 1px solid var(--line);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
  display: flex; flex-direction: column; gap: 2px;
  animation: kebab-in .14s ease-out;
}
@keyframes kebab-in { from { opacity: 0; transform: translateY(-6px); } }
.kebab-menu button {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 12px;
  border-radius: 10px;
  font-size: 14.5px; font-weight: 600;
  text-align: left;
  color: var(--text);
}
.kebab-menu button span { font-size: 16px; width: 20px; text-align: center; }
.kebab-menu button:active { background: rgba(255, 255, 255, 0.1); }

/* ------------------------------------------------------------ colour picker */
.colour-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(54px, 1fr));
  gap: 8px;
  margin-bottom: 4px;
}
.colour-cell {
  aspect-ratio: 1;
  border-radius: 13px;
  background: var(--c);
  border: 2px solid rgba(0, 0, 0, .35);
  display: grid; place-items: center;
  color: var(--ink);
  font-size: 18px; font-weight: 800;
  transition: transform .12s, box-shadow .15s;
}
.colour-cell:active { transform: scale(0.92); }
.colour-cell.on { box-shadow: 0 0 0 3px var(--gold); }
/* Taken is not a style choice — two players in the same colour makes the board
   unreadable, so these are genuinely unavailable rather than merely discouraged. */
.colour-cell:disabled { opacity: 0.42; }
.colour-cell .taken-by { font-size: 11px; font-weight: 800; letter-spacing: .02em; }
/* Struck through as well as dimmed. Dimming alone reads as "not yet", and on a grid of
   eleven bright squares it did not read as anything at all — the line is what says the
   colour is gone. */
.colour-cell.taken { position: relative; }
.colour-cell.taken::after {
  content: '';
  position: absolute; inset: 4px;
  background: linear-gradient(to top right,
    transparent calc(50% - 2px), rgba(10, 16, 24, .85) calc(50% - 2px),
    rgba(10, 16, 24, .85) calc(50% + 2px), transparent calc(50% + 2px));
}
.colour-cell.mine { box-shadow: 0 0 0 3px var(--gold); }

/* ------------------------------------------------------------ about */
.about-hero { text-align: center; padding: 6px 0 10px; }
.about-icon {
  width: 96px; height: 96px;
  border-radius: 22px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
}
.about-name { margin: 12px 0 0; font-size: 24px; font-weight: 800; }
.about-ver { margin: 4px 0 0; color: var(--dim); font-size: 13.5px; }
.about-credit { margin: 12px 0 0; font-size: 14px; font-weight: 600; }

[hidden] { display: none !important; }

/* Wider screens get a little breathing room, but the game stays a phone game. */
@media (min-width: 720px) and (min-height: 620px) {
  .board-wrap { border-radius: 0; }
  .tray { padding-left: 18px; padding-right: 18px; }
  .hand { gap: 8px; }
  .rcard { width: 58px; height: 78px; }

  /* Everything the game puts in front of you sits in one centred column.
     Full width is the right answer on a phone and an odd one on a desktop: measured at
     1440px the action bar was 1404px and the score strip 1370px, so four buttons and four
     name chips were being stretched across the whole window with nothing else in it. The
     board keeps the full width — it is a picture and it fits itself — and everything you
     read or press is gathered back into the middle, under the same cap the sheets have
     always used. None of this exists below 720px, so the phone is untouched. */
  /* `width: 100%` is load-bearing. These are children of a column flex container, and an
     auto inline margin on one of those stops it stretching and shrinks it to its content
     instead — which collapsed the action bar to the width of three small buttons rather
     than centring a full-width one. Given a width to fill first, the auto margins go back
     to doing only what they are here for. */
  .actions,
  .hand { width: 100%; max-width: 560px; margin-left: auto; margin-right: auto; }

  /* The kebab comes out of the flow so the chips centre on the window rather than on
     what is left of it once the button has taken its 40px — otherwise the strip sits
     half a button to the left of centre, which is exactly the sort of near-miss that
     looks like a mistake rather than a layout. */
  .game-top { position: relative; }
  .game-top #game-menu { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); }
  .score-strip { max-width: 640px; margin-left: auto; margin-right: auto; }
}
