/* ══════════════════════════════════════════════════════════════════════
   Pact Breaker — an admiralty chart with a brass instrument panel.

   Ink for water, oxidised vellum for land, brass for anything the player
   can act on. Sea features are set in Didone italic and land features in
   letterspaced caps, which is how hydrographic charts have always done it.
   ══════════════════════════════════════════════════════════════════════ */

:root {
  /* 1403 / 1288 — the map's own proportions, as a number for calc(). */
  --map-ratio: 1.0893;
  --abyss:      #06090e;
  --ink:        #101a24;
  --ink-2:      #16232f;
  --ink-3:      #1e2f3d;
  --sea:        #16252f;
  --sea-deep:   #0e1a22;
  --vellum:     #e4d7b9;
  --vellum-2:   #d3c29c;
  --vellum-3:   #bfab84;
  --paper:      #f0e8d4;

  --brass:      #c9a227;
  --brass-2:    #e8cd6a;
  --brass-dim:  #8a6f1c;
  --verdigris:  #52a09a;

  /* Three steps, and every one of them readable.
     --text-faint was #6f6959: 3.55–3.65:1 against the surfaces it actually sits on,
     under the 4.5:1 that applies to normal-weight text below 18px. That token carries
     the entire cost-and-identity surface of the machine players — the note explaining
     that calls are billed to your own API account, the hard-ceiling explanation, the
     spend readout, the diplomat's name in the ledger, the press status line — so the
     text a player must read before entering a credential was the least legible text
     in the product.
     Measured against #070b10 / #13171c / --ink / --ink-2 / --ink-3, the worst case of
     each: faint 4.72, dim 6.94, text 10.64. --text-dim was lifted with it so the
     hierarchy keeps a visible step rather than collapsing into two shades of the
     same grey; it only ever gets lighter, so nothing that used it lost contrast. */
  --text:       #e9e2d1;
  --text-dim:   #c0b8a2;
  --text-faint: #9e9785;
  --rule:       #2a3a47;
  --rule-2:     #3d5261;

  --austria: #a5304a;
  --england: #33538a;
  --france:  #4f9cc4;
  --germany: #4a4744;
  --italy:   #4a854f;
  --russia:  #7355a3;
  --turkey:  #c68b1e;
  /* Status. These meant one thing each and were written five different ways,
     picked by whichever surface they happened to land on. */
  --bad: #d8493f;          /* strokes and marks on the board */
  /* Text on ink. The text ramp above was audited to 4.5:1 and the status palette was
     not, which left every refusal in the product — the arming flow's, the press
     composer's, and the rearm dialog's, the one a player must read to restart a stalled
     table — as the least legible text on its own surface: #c2564f measured 3.85 to
     4.44:1 at 11–12.5px normal weight. Worst case now 4.85:1, against --ink-3; 5.63 on
     --ink-2 and 6.96 on the darkest composited card. --bad keeps its own value: it is
     strokes on the board, not small text. */
  --bad-soft: #dc807a;     /* the same meaning, as text on ink */
  --bad-paper: #a03a32;    /* and as text on the flimsy's paper */
  --warn: #94651a;
  /* Warning, as small text on ink — the sibling --bad already has in --bad-soft, and the
     one --warn never got. #94651a measured 3.46:1 against --ink and 2.70 against --ink-3,
     which made "phone disconnected — New link reopens the seat" the only text in the
     shared-board view below AA, and the dimmest line on a screen read from across a room.
     Worst case now 5.19:1. --warn keeps its value for marks on the flimsy, which is not
     small text on ink. */
  --warn-soft: #c8973a;
  --good-paper: #2e6b34;

  --neutral: #8d8371;

  --display: 'Bodoni Moda', 'Didot', 'Playfair Display', Georgia, serif;
  --ui: 'Archivo', ui-sans-serif, system-ui, 'Helvetica Neue', sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  --rail: 274px;
  --gap: 14px;
  --ease: cubic-bezier(.22, .61, .36, 1);
}

* { box-sizing: border-box; }
html, body { height: 100%; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* Author `display` beats the UA rule for [hidden], and several panels below set
   one. Restate it here so `hidden` keeps meaning hidden. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background:
    radial-gradient(120% 90% at 50% -10%, #101c26 0%, var(--abyss) 62%),
    var(--abyss);
  color: var(--text);
  font-family: var(--ui);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

button { font: inherit; color: inherit; cursor: pointer; }
h1, h2, h3 { margin: 0; font-weight: 400; }
input, select, textarea { font: inherit; color: inherit; }
/* The one hyperlink in the product — the map's CC BY-SA credit in the rules sheet — was
   the browser default blue on a near-black surface: measured 2.1:1, the only text in the
   app under AA and the only element outside the brass-and-parchment system. There was no
   `a` rule anywhere in this file at all, which is exactly how it survived: a default
   never drifts, so nothing ever pointed at it. */
a { color: var(--brass-2); text-underline-offset: 2px; }
a:hover { color: var(--brass); }
/*
 * The one text colour in the product that was never chosen.
 *
 * Chrome's default placeholder is #757575, which on the field's own background — a .35
 * black over the card over the gate — measures 4.23:1 at 12px, under the floor the rest
 * of this file's ramp was re-tuned to clear. It lands on the key field, on the table
 * code, and on `#orderInput`, whose placeholder is the only statement of the order syntax
 * anywhere on screen. `--text-faint` puts the same text at 6.9:1 on that background.
 *
 * `opacity: 1` because the UA sheet dims the placeholder as well as colouring it, and an
 * audited value applied at 54% is an unaudited value again.
 */
input::placeholder, textarea::placeholder { color: var(--text-faint); opacity: 1; }
:focus-visible { outline: 2px solid var(--brass-2); outline-offset: 2px; }

/* ══════════════════════  Title screen  ══════════════════════ */

.gate {
  position: fixed; inset: 0; z-index: 50;
  /* A column: the plate scrolls, the commit bar below it does not. See `.gate-commit`. */
  display: flex; flex-direction: column; align-items: center;
  padding: 24px;
  overflow: hidden;
  background:
    radial-gradient(80% 60% at 50% 0%, rgba(201,162,39,.07), transparent 70%),
    linear-gradient(180deg, #0a1219, #05080c);
}
/* The gate is a scroller too, and it was the one with nothing to say so — the platform's
   overlay scrollbars leave no cue, and this is the first screen anyone sees.
   It says so by carrying `.scroller` now, like every other one. A verbatim second copy of
   those four rules stood here, claiming in its own comment to be the "same treatment as
   the order list and the room's seats" while having already drifted from them on three
   values — 10px against 8, .3 against .28, radius 5 against 4 — and while getting none of
   the boundary fade the class also carries. Two of this file's consolidations have now
   been undone by a hand-kept copy left behind beside them.
   The one declaration that stays is the width: this scrollport is the width of the plate
   and 8px reads as a hairline across it. */
.gate-scroll::-webkit-scrollbar { width: 10px; }

/*
 * The plate holds the frame; an inner element does the scrolling.
 *
 * When the plate itself scrolled, its decorative `::before` — absolutely positioned
 * against the plate's padding box — kept the scrollport's *height* but travelled with the
 * scrolled content, so once the form was scrolled at all its bottom rule drew a gold
 * hairline straight across the middle of the arming fields, with two stub verticals, and
 * the inner border it exists to provide was gone. A decoration must be painted against
 * the box the viewer sees, which means it cannot live inside the thing that moves.
 */
.gate-plate {
  width: min(980px, 100%);
  flex: 0 1 auto; min-height: 0; overflow: hidden; margin: auto;
  display: flex; flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: 2px;
  background: linear-gradient(180deg, rgba(22,35,47,.86), rgba(10,17,23,.92));
  box-shadow: 0 40px 120px -30px #000, inset 0 1px 0 rgba(232,205,106,.08);
  position: relative;
}
.gate-plate::before {
  content: ''; position: absolute; inset: 7px;
  border: 1px solid rgba(201,162,39,.16);
  pointer-events: none;
  /* Above the scrolling content, below any control in it. */
  z-index: 1;
}
/* The scroller, so the commit bar beneath the plate is outside the scrollport and cannot
   be scrolled under. The padding lives here, inside the frame. */
.gate-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: clamp(24px, 4vw, 52px);
}

.gate-seal { color: var(--brass); width: 74px; margin: 0 auto 18px; opacity: .85; }
.gate-seal svg { display: block; width: 100%; }

.gate-eyebrow {
  margin: 0 0 10px; text-align: center;
  font-size: 10.5px; letter-spacing: .28em; text-transform: uppercase;
  color: var(--brass); font-weight: 600;
  /* Even lines rather than a one-word orphan. At 360px this broke to three lines with
     "DICE" alone on the last — at 10.5px and .28em tracking that reads as an accident. */
  text-wrap: balance;
}

.gate-title {
  font-family: var(--display);
  font-size: clamp(52px, 10vw, 108px);
  line-height: .86;
  letter-spacing: -.03em;
  text-align: center;
  color: var(--paper);
  font-weight: 500;
  margin: 0 0 4px;
}
.gate-title span {
  display: block;
  font-style: italic;
  font-weight: 400;
  color: var(--brass-2);
}

.gate-lede {
  max-width: 58ch; margin: 18px auto 30px;
  text-align: center; color: var(--text-dim);
  font-size: 15px; line-height: 1.62;
}

/* The title stays the title; the work starts beneath it. The redesign changes the
   decision structure without turning the first screen into a different brand. */
.gate-masthead { text-align: center; }

.gate-routes {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid var(--rule); margin-bottom: 18px;
}
.gate-routes:has(.gate-route[hidden]) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gate-route {
  min-width: 0; min-height: 92px; padding: 15px 16px;
  display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 11px; align-items: start;
  border: 0; border-right: 1px solid var(--rule); background: rgba(4,8,12,.24);
  text-align: left; color: var(--text-dim); position: relative;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.gate-route:last-child, .gate-route:nth-last-child(2):has(+ [hidden]) { border-right: 0; }
.gate-route::after {
  content: ''; position: absolute; inset: auto 0 0; height: 2px;
  background: transparent; transition: background .18s var(--ease);
}
.gate-route:hover { color: var(--text); background: rgba(201,162,39,.045); }
.gate-route.is-on { color: var(--paper); background: rgba(201,162,39,.085); }
.gate-route.is-on::after { background: var(--brass); }
.gate-route-icon { width: 26px; height: 26px; color: var(--brass); }
.gate-route-icon svg { width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
.gate-route strong { display: block; font-family: var(--display); font-size: 20px; font-weight: 500; line-height: 1.1; }
.gate-route small { display: block; margin-top: 5px; color: var(--text-faint); font-size: 11.5px; line-height: 1.4; }

.gate-panels { border: 1px solid var(--rule); background: rgba(6,11,15,.5); }
.gate-panel { padding: clamp(20px, 3.3vw, 34px); }
.gate-panel-head {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, .75fr);
  gap: 24px; align-items: end; padding-bottom: 18px; margin-bottom: 20px;
  border-bottom: 1px solid var(--rule);
}
.gate-panel-head h2 { font-family: var(--display); font-size: clamp(32px, 4.4vw, 48px); color: var(--paper); line-height: 1; }
.gate-panel-head > p { margin: 0; color: var(--text-faint); font-size: 12.5px; line-height: 1.55; }
.gate-kicker, .gate-field-label, .gate-format legend {
  margin: 0 0 7px; color: var(--brass); font-family: var(--mono); font-size: 10px;
  letter-spacing: .18em; text-transform: uppercase;
}

.gate-format { margin: 0 0 22px; padding: 0; border: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.gate-format legend { grid-column: 1 / -1; width: 100%; }
.gate-format label {
  min-width: 0; min-height: 72px; padding: 12px; display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start;
  border: 1px solid var(--rule); background: rgba(0,0,0,.18); cursor: pointer;
}
.gate-format label:has(input:checked) { border-color: var(--brass); background: rgba(201,162,39,.075); box-shadow: inset 3px 0 0 var(--brass); }
.gate-format input { margin: 3px 0 0; accent-color: var(--brass); }
.gate-format strong { display: block; color: var(--text); font-size: 13px; font-weight: 500; }
.gate-format small { display: block; margin-top: 3px; color: var(--text-faint); font-size: 11px; line-height: 1.35; }
.gate-format em { color: var(--brass); font-family: var(--mono); font-size: 9px; font-style: normal; letter-spacing: .15em; text-transform: uppercase; }

.gate-create-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, .75fr); gap: 24px; }
.gate-create-main, .gate-create-essential { min-width: 0; }
.gate-panel .power-pick { grid-template-columns: repeat(auto-fit, minmax(138px, 1fr)); }
.gate-room-explainer {
  margin: 0; padding: 18px; min-height: 104px; border: 1px solid var(--rule);
  color: var(--text-dim); font-family: var(--display); font-size: 20px; line-height: 1.45;
  background: linear-gradient(105deg, rgba(201,162,39,.08), transparent);
}

.gate-advanced { margin-top: 8px; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.gate-advanced summary { padding: 13px 0; color: var(--brass-2); cursor: pointer; font-size: 12.5px; letter-spacing: .08em; }
.gate-advanced-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; padding: 8px 0 4px; }
.gate-advanced-grid .gate-field select { max-width: none; }

.gate-action { max-width: 520px; margin: 22px 0 0 auto; }
.gate-action .seal-button-face { padding-block: 14px; }
.gate-action-note { margin: 10px 0 0; text-align: center; color: var(--text-faint); font-size: 11.5px; }
.gate-action .gate-note { margin: 0 0 7px; text-align: center; min-height: 0; }
.gate-action .gate-note:empty { display: none; }

.gate-join-layout { display: grid; grid-template-columns: minmax(220px, .7fr) minmax(0, 1.3fr); gap: 28px; }
.gate-code-field input { font-family: var(--mono); font-size: 24px; letter-spacing: .24em; text-transform: uppercase; }
.gate-action--join { margin-top: 24px; }

.gate-returns { display: grid; gap: 10px; }
.gate-return-card {
  padding: 16px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: center;
  border: 1px solid var(--rule); background: linear-gradient(105deg, rgba(201,162,39,.07), transparent);
}
.gate-return-card h3 { font-family: var(--display); font-size: 25px; color: var(--paper); }
.gate-return-card p { margin: 4px 0 0; color: var(--text-faint); font-size: 12px; }
.gate-return-card .ghost-button { width: auto; min-width: 190px; margin: 0; }
.gate-return-actions { display: flex; align-items: center; gap: 10px; }
.gate-forget { border: 0; background: none; color: var(--text-faint); font-size: 11px; text-decoration: underline; text-underline-offset: 2px; }
.gate-start-other { display: block; margin: 18px auto 0; }

@media (max-width: 760px) {
  .gate-routes, .gate-routes:has(.gate-route[hidden]) { grid-template-columns: 1fr; }
  .gate-route, .gate-route:nth-last-child(2):has(+ [hidden]) { border-right: 0; border-bottom: 1px solid var(--rule); }
  .gate-route:last-child { border-bottom: 0; }
  .gate-panel-head, .gate-create-grid, .gate-join-layout { grid-template-columns: 1fr; gap: 14px; }
  .gate-format { grid-template-columns: 1fr; }
  .gate-advanced-grid { grid-template-columns: 1fr; }
  .gate-return-card { grid-template-columns: 1fr; }
  .gate-return-actions { align-items: stretch; flex-direction: column; }
  .gate-return-card .ghost-button { width: 100%; }
  .gate-action { max-width: none; }
}

/* `align-items: start` so each card is as tall as its contents.
   Stretched — the grid default — the second card matched the first's 1221px while its
   own content ended 388px in, leaving 833px of empty bordered box: the largest single
   element on the product's first screen was a void with a border around it, and its
   scroll depth is what put the gate's only error line out of reach. */
/*
 * One card above the other, each using the full plate.
 *
 * Two side-by-side columns looked economical and was not: the cards are wildly unequal —
 * 784.8px against 406.7px at 1280x800, 944.8 against 453.5 at 1440x900 — so nearly 500px
 * of the narrow column sat blank while the wide one pushed the API key, the one hard
 * prerequisite for all four doors, 77.8px past the fold at 1280 and 565px past it at 360.
 * The player saw a lit "Begin the game" and none of what it needs.
 *
 * Stacked, each card gets the whole 980px and lays its own controls out across it, which
 * is what actually buys the height back.
 */
.gate-forms {
  display: flex; flex-direction: column; gap: 18px;
  /* The cards below switch on their own width, not the window's — see the container query.
     `inline-size` rather than `size`, because these cards are sized by their content. */
  container-type: inline-size;
}

.gate-card {
  padding: 20px;
  border: 1px solid var(--rule);
  background: rgba(6,11,15,.5);
}
/*
 * The primary card lays its settings out two across.
 *
 * Five stacked `.gate-field`s measured 428px of a 990px card, which was most of why the
 * card's own Begin button sat 605px below the fold while all three doors in the narrower
 * column beside it were in view — the recommended path was the only one you could not
 * see. Two columns turns five rows into three. Everything that is not a field spans the
 * width, so the reading order is unchanged.
 *
 * Only where there is room for it: below 900px the card is too narrow for two selects
 * side by side, and the grid collapses to the single column it always was.
 */
/*
 * Keyed to the card's own width, not the window's.
 *
 * At a 899px window the card already measures 767px — ample for three columns — but the
 * old `min-width: 900px` viewport query kept it in one, so across the whole 700–899 band
 * the arming form ran 411px deeper than it needed to and rendered a 723px-wide select
 * whose value is "$5". Two pixels of window then changed the card's height by 249px while
 * its width moved by 1.8px. A container query asks the question that actually matters.
 */
@container (min-width: 700px) {
  /* Three across, now that the card has the whole plate. Five fields become two rows. */
  .gate-card:not(.gate-card--minor) {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 18px; align-content: start;
  }
  .gate-card:not(.gate-card--minor) > :not(.gate-field) { grid-column: 1 / -1; }
  /* The second card's three doors sit in a row rather than a stack, for the same reason:
     it is a full-width card now and they are peers. */
  .gate-card--minor {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 18px; align-items: start;
  }
  .gate-card--minor > h2,
  .gate-card--minor > .gate-note,
  .gate-card--minor > #resumeBoard { grid-column: 1 / -1; }
  /* The deadline spans the row so its note can run full width, but the control itself is
     held to one column's worth. Spanning outright made an 849px select beside 271px peers
     — the widest control on the screen, and the least consequential of the six. */
  .gate-card--minor > .gate-field:not(.gate-door) { grid-column: 1 / -1; }
  .gate-card--minor > .gate-field:not(.gate-door) select { max-width: 271px; }
  /* The tier, how long it thinks and what it may spend are one decision, so they share
     one column; the temper of the table and the key are the other. Left to auto-flow, the
     grid put "How long they think" under "Temper of the table", split the tier from the
     ceiling that spends it, and left a 208x115px hole beside the spending control — the
     largest empty rectangle in the card.
     The picker is named explicitly too: pinning the five fields to rows 3-5 while leaving
     it on `auto` pushed it past them into row 6, so the card headed "Take a power" showed
     five spending settings first and the power choice last, 267px below the fold — and
     the keyboard order jumped back up to the selects after it. */
  #powerPick        { grid-row: 3; }
  #mindsModelField  { grid-column: 1; grid-row: 4; }
  #botTemperField   { grid-column: 2; grid-row: 4; }
  #mindsEffortField { grid-column: 3; grid-row: 4; }
  /* The key spans two of the three, because it is the field that actually blocks the
     button and the only one somebody has to paste into. */
  #mindsKeyField    { grid-column: 1 / span 2; grid-row: 5; align-self: start; }
  #spendCapField    { grid-column: 3; grid-row: 5; }
}

/*
 * Begin stays reachable — in a bar of its own, outside the scroller.
 *
 * Shortening this card got it from 1152px to 846px, and that is as far as honesty goes:
 * a seven-power picker, five settings and a key field do not fit above an 800px fold.
 * The defect was never the scrolling — it was that the *primary* action was the only
 * door out of view while the three alternatives in the shorter column beside it were all
 * visible.
 *
 * The first attempt made the button `position: sticky` inside the card, which was worse
 * than the problem: pinned to the scrollport it painted over whatever it scrolled past,
 * and `elementFromPoint` at each control's centre returned the seal instead — at the
 * default scroll position that was France's chip at 360px, the *pre-selected* power, and
 * mid-scroll it was the API-key field. A pinned control inside its own scrollport always
 * eats clicks; the only fix is to take it out of the scrollport. So the plate scrolls and
 * the bar does not, which reserves the bar's height by construction.
 */
.gate-commit {
  flex: none; width: min(980px, 100%);
  display: flex; flex-direction: column; align-items: stretch;
  padding: 12px clamp(24px, 4vw, 52px) 0;
}
/* Above the button, so a refusal grows upward into the plate rather than displacing the
   control that was just pressed — see the note in index.html. No height is reserved,
   because none needs to be: the bar is anchored to the bottom of the gate. */
.gate-commit .gate-note { text-align: center; margin: 0 0 6px; min-height: 0; }
.gate-commit .gate-note:empty { display: none; }
.gate-card h2 {
  font-family: var(--display); font-size: 20px; letter-spacing: -.01em;
  color: var(--paper); margin-bottom: 6px;
}
.gate-help { margin: 0 0 16px; font-size: 12.5px; color: var(--text-faint); line-height: 1.55; }

/* As many per row as the card is wide enough for, one where it is not. Seven chips in a
   single stack made the first card 1221px tall on an 800px-high viewport, which is what
   pushed the Begin button — and everything under it — below the fold.
   One `minmax` floor, here, at the base. There was a second in the `(min-width: 900px)`
   block above raising it to 210px, and it never once applied: this rule is declared later
   at equal specificity, so it won at every width. Fifth time this file has been caught by
   a media query adding no specificity — hence one declaration rather than two. 188px
   gives four across on the full-width card and two on a phone. */
.power-pick {
  display: grid; gap: 6px; margin-bottom: 16px;
  grid-template-columns: repeat(auto-fit, minmax(188px, 1fr));
}
/* Flag, then name over description. A three-column row put the description in
   a right-aligned column of its own, which made it read as data rather than as
   a line about the power — and wrapped raggedly the moment a name was as long
   as "Austria-Hungary". */
.power-chip {
  /* `align-content: start` so the two rows keep their content heights. Left to stretch,
     a chip shorter than its grid row split the spare height between both rows, which put
     two adjacent descriptions 8.1px out of line whenever one of them wrapped. */
  display: grid; grid-template-columns: 22px 1fr; align-items: start; align-content: start; gap: 4px 10px;
  padding: 9px 11px; border: 1px solid var(--rule); background: transparent;
  text-align: left; transition: border-color .18s var(--ease), background .18s var(--ease);
}
.power-chip-flag { grid-row: span 2; margin-top: 3px; }
.power-chip:hover { border-color: var(--rule-2); background: rgba(255,255,255,.025); }
.power-chip[aria-checked="true"] {
  border-color: var(--brass); background: rgba(201,162,39,.09);
}
.power-chip-flag { width: 22px; height: 14px; border: 1px solid rgba(0,0,0,.5); }

/* — Ensigns —
   The seven flags, wherever a power has to be *recognised* rather than merely
   coloured: choosing a seat, your own crest, the standings, and the seat list
   on the shared board. Everywhere else the colour still speaks for itself,
   because on the map colour is the language — a legend of flags would stop
   explaining what the tinted provinces mean.
   The flag is stretched to whatever box it is given, which is a small lie about
   proportion; a key reads as a key only when its marks are one size. Each box
   keeps a hairline of the power's own colour so that where the flag is only an
   impression — 13px in the ledger, and narrower still on a phone — the tie back
   to the map is never carried by the flag alone. */
.ensign { display: block; width: 100%; height: 100%; }
.power-chip-flag, .ledger-swatch, .room-seat-flag {
  flex: none; line-height: 0; background: #0b1117;
  box-shadow: inset 0 0 0 1.5px var(--power, transparent);
}
.crest-mark > .ensign { position: absolute; inset: 0; width: auto; height: auto; }
.power-chip-name { font-weight: 500; line-height: 1.25; }
.power-chip-note { font-size: 11.5px; color: var(--text-faint); line-height: 1.4; }

.gate-field { display: block; margin-bottom: 16px; }
.gate-field > span {
  display: block; margin-bottom: 6px;
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: var(--text-faint);
}
/* One height for every control on this screen. They shared a padding and a border and
   still came out at four different heights in a single stack — 37.5 for the selects, 38
   for the key (a smaller mono face), 38.75 for a ghost button, 41 for the join field
   (stretched by its grid row) — none of it intended. Stated once instead of emerging. */
.gate-field select,
.gate-field input,
.gate-forms .ghost-button {
  /* The line-height matters as much as the floor: left at the inherited value, the join
     field's 14px text made a 41px box that the 40px minimum could not pull back.
     No `:not()` here — that raised this rule's specificity above the 44px touch floor in
     the narrow block, so at 360px the selects took 44 and the buttons stayed at 40, which
     is the four-different-heights defect again in a new place. */
  min-height: 40px; line-height: 1.2;
}
.gate-field select, .gate-field input {
  width: 100%; padding: 9px 10px;
  background: rgba(0,0,0,.35); border: 1px solid var(--rule); border-radius: 0;
  /* A control no wider than its content needs, in the single-column branch too. A 723px
     select holding "$5" is not a control, it is a bar. */
  max-width: 420px;
}
/* Not stretched to its row: the button beside it sets the height for both. */
.gate-join > * { align-self: start; }
/* One shape for each of the three ways in: a label, a control, a line of fine print. They
   were a lead button, a bare button and a labelled field, so in one grid row they shared
   no top edge, no control height and no label treatment — three alternatives presented as
   equals that aligned on nothing. */
/*
 * Three rows — label, control, fine print — that the three doors *share* when they sit in
 * a row, so a label that wraps cannot drop its own column's control below its peers'.
 *
 * It did. At 800x780 the three columns are 211.3px and "A table you have a code for" has
 * an intrinsic width of 213.3, so that one label took two lines and pushed its field
 * 16.5px below the two buttons beside it — across roughly a 30px band of widths, and by
 * 1.3px, which is a cliff rather than a slope. Giving every label two lines' worth of
 * `min-height` would align them by leaving a hole above the other two; `subgrid` aligns
 * them by letting the tallest label set one row height for all three, which is what
 * calling them peers was supposed to mean.
 *
 * Outside the three-column container the parent is not a grid, `subgrid` computes to
 * `none` and `grid-row` is ignored, so this is a one-column grid: the same stack the flex
 * column drew before.
 */
.gate-door {
  display: grid; grid-template-rows: subgrid; grid-row: span 3;
  margin-bottom: 8px;
}
/* `.gate-door > .gate-door-label`, not the bare class: one of the three doors is also a
   `.gate-field`, whose `> span` rule is more specific and would have given that one label
   a different size and tracking from its two peers — the same misalignment in a new
   place. */
.gate-door > .gate-door-label {
  /* Sat at the bottom of the shared row, so when one label wraps the other two keep the
     same distance to the control they name and the spare line opens above them instead of
     between label and field. A label belongs to the thing under it; that gap is the only
     thing saying so. */
  display: block; align-self: end; margin-bottom: 5px;
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-faint);
}
.gate-door .gate-fine { margin-top: 5px; }
.gate-door .ghost-button { margin-bottom: 0; }

.gate-join { display: grid; grid-template-columns: 1fr auto; gap: 6px; }
/* Room for the longest label this button ever shows, so going from "Join" to "Joining…"
   does not widen it and steal 27px from the code field beside it mid-action. */
#joinGo { min-width: 92px; }

/*
 * The small print under a field — what the key pays for, and what the spend ceiling
 * does when it is reached. Sentence case and a readable measure rather than the uppercase tracking
 * the field labels use: these are sentences to be read, not labels to be scanned,
 * and the tracking that suits a three-word label makes a paragraph hard work.
 */
.gate-fine {
  display: block; margin: 6px 0 0;
  font-size: 11.5px; line-height: 1.5; color: var(--text-faint);
  /* `min-width: 0` because a grid or flex item defaults to `auto`, which refuses
   * to shrink below its longest unbreakable run — the lesson the join URL taught
   * this file when it made the whole sheet 362px wide in a 360px viewport. */
  min-width: 0; overflow-wrap: break-word;
}
/* A refusal in the rearm dialog looked exactly like the neutral "spent so far" line
   directly above it — same class, same colour, nothing to tell the two apart. */
.gate-fine.is-bad { color: var(--bad-soft); }
/* A refusal, printed where the field it refuses is. The door's own note keeps the
   live region; this is the copy a sighted player reads without scrolling 771px. */
.gate-field-error {
  display: block; margin: 6px 0 0; min-width: 0;
  font-size: 11.5px; line-height: 1.5; color: var(--bad-soft);
}
/* The gate's field component, reused in a sheet, at the width the gate uses it. `width:
   100%` made a password field and a five-option select 722px wide in a 760px panel —
   an unstyled form, in the one dialog that recovers a stopped game, and the same
   full-bleed defect `.seal-button--sheet` was written to fix a few lines above. */
.sheet-body .gate-field select,
.sheet-body .gate-field input { max-width: 320px; }

/* Room kept for the message so the commit button below cannot move when one arrives.
   It sits above `.sheet-actions`, so writing to it shifted the button 16.6px downward
   under the pointer that had just pressed it — inviting a second press on the one
   control that restarts a stalled table. The same reason `.press-foot` grows upward. */
#rearmNote { min-height: 2.6em; margin-top: 10px; }

/* A key is a long unbroken string; showing it in the game's mono face makes a
 * mistyped one findable, and stops it being kerned like prose. */
#mindsKey, #rearmKey { font-family: var(--mono); font-size: 12px; letter-spacing: .04em; }
/* Marked as well as described. A refusal that names a field should be visible on the
   field, not only in the sentence about it. */
/* `input` alone meant a select could never carry the mark, so the rearm dialog's refusal
   named a control it had no way to point at. */
.gate-field :is(input, select)[aria-invalid="true"] { border-color: var(--bad-soft); }

/* The way into the prose the gate used to print in full. A link, not a button-looking
   thing: it opens a reading sheet, it does not act. */
.gate-more {
  display: inline; padding: 0; border: 0; background: none;
  font: inherit; color: var(--brass); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.gate-more:hover { color: var(--brass-2); }
/* One quiet line under the commit button, pointing at the alternatives that start below
   the fold. Centred on the button it follows, and small enough that the bar's own height
   barely moves — the bar is anchored to the bottom of the gate, so anything that grows
   here pushes up into the plate. */
.gate-commit-alt {
  margin: 8px 0 0; text-align: center;
  font-size: 12px; color: var(--text-faint);
}

/* The wax seal: the one place the interface asks to be pressed. */
.seal-button {
  position: relative; display: block; width: 100%;
  padding: 0; border: 0; background: transparent;
}
.seal-button-face {
  display: block; padding: 13px 18px;
  font-family: var(--display); font-size: 16px; letter-spacing: .01em;
  color: #1a1305; text-align: center;
  background: linear-gradient(178deg, var(--brass-2), var(--brass) 46%, var(--brass-dim));
  border: 1px solid #6b5413;
  box-shadow: 0 4px 0 #4b3a0c, 0 10px 24px -10px rgba(201,162,39,.6);
  transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
.seal-button:hover .seal-button-face { transform: translateY(-1px); box-shadow: 0 5px 0 #4b3a0c, 0 14px 30px -10px rgba(201,162,39,.7); }
.seal-button:active .seal-button-face { transform: translateY(3px); box-shadow: 0 1px 0 #4b3a0c, 0 4px 12px -6px rgba(201,162,39,.5); }
.seal-button[disabled] { cursor: not-allowed; }
/* This face carries "Held" in a stalled table and "Opening…" while a door works, so it
   is read, not merely seen. --text-faint on this lighter plate is 4.29:1 — the token is
   tuned against the dark surfaces, and this is the one place it sits on a light one. */
.seal-button[disabled] .seal-button-face {
  background: #2a3540; color: #b3ab96; border-color: var(--rule);
  box-shadow: 0 3px 0 #1a222a;
}

/*
 * A button at rest has to look pressable without being hovered.
 *
 * `background: transparent` over a card left it composited at rgb(10,16,22) against the
 * key field's rgb(6,11,14) — 1.03:1, no boundary at all — and both carry the same 1px
 * `--rule` border, the same 40px height and the same left-aligned text. On the gate's
 * "Other ways in" row that produced three near-identical bordered boxes of which two were
 * buttons and one was a field, with pressability carried only by `:hover`, which a touch
 * device never shows. A faint brass wash and a warmer border are the resting cue; hover
 * still lifts both further.
 */
.ghost-button {
  display: block; width: 100%; padding: 9px 12px; margin-bottom: 8px;
  background: rgba(201,162,39,.055); border: 1px solid var(--rule-2); color: var(--text-dim);
  text-align: center; font-size: 12.5px;
  transition: border-color .18s var(--ease), color .18s var(--ease), background .18s var(--ease);
}
.ghost-button:active { background: color-mix(in srgb, var(--brass) 10%, transparent); }
.ghost-button[disabled], .tool[disabled],
.ghost-button[disabled]:hover, .tool[disabled]:hover {
  opacity: .45; cursor: not-allowed;
  border-color: var(--rule); color: var(--text-faint); background: transparent;
}
.tab:hover { color: var(--text-dim); }
.press-pill:hover { border-color: var(--brass-dim); color: var(--text-dim); }
.tool:active, .power-chip:active, .press-pill:active { transform: translateY(1px); }
.ghost-button:hover { border-color: var(--brass-dim); color: var(--text); background: rgba(201,162,39,.06); }
/* `text-align` is the base's now, not this modifier's — the component used to align two
   ways inside one row. */
.ghost-button--inline { width: auto; margin: 0; }
.ghost-button--tiny {
  /* These are button labels — Rules, Save, Leave, Clear, Hold the rest — not
     the tracked micro-labels the rest of the chrome uses, and they were the
     smallest interactive text on the desktop layout. */
  width: auto; margin: 0; padding: 7px 11px; font-size: 12px;
  letter-spacing: .09em; text-transform: uppercase;
}
.gate-note { min-height: 1.2em; margin: 4px 0 0; font-size: 12px; color: var(--brass); }
/* Arriving by link: the message is the page, not a footnote under a form the
   player is not meant to use. */
.gate-arrival { margin-top: 30px; text-align: center; }
.gate-arrival-note { margin: 0; font-size: 15px; color: var(--text-dim); }
.gate-arrival-note.is-bad { color: var(--bad-soft); }
.gate-arrival .ghost-button { width: auto; margin: 18px auto 0; padding: 9px 18px; }
.gate-note.is-bad { color: var(--bad-soft); }
/* A consequence of a choice, not a failure of one — the door opened. Amber, the same
   colour the ledger uses for a seat nobody is writing for, which is exactly what this
   sentence is about. */
.gate-note.is-warn { color: var(--warn-soft); }

.gate-foot {
  margin: 30px 0 0; text-align: center; font-size: 11px; color: var(--text-faint);
}

/* ══════════════════════  Table layout  ══════════════════════ */

.table {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr) var(--rail);
  gap: var(--gap);
  height: 100vh; height: 100dvh;
  padding: var(--gap);
}

.rail {
  display: flex; flex-direction: column; gap: 14px;
  min-height: 0; overflow: hidden;
}

.rail-heading {
  font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 8px;
  display: flex; justify-content: space-between;
}

/* — Crest — */
.crest { display: grid; grid-template-columns: 40px 1fr; gap: 11px; align-items: center; }
.crest-mark {
  width: 40px; height: 27px; position: relative;
  border: 1px solid rgba(0,0,0,.6);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.14), 0 3px 10px -4px #000;
}
.crest-power {
  margin: 0; font-family: var(--display); font-size: 19px; line-height: 1.1;
  color: var(--paper); letter-spacing: -.01em;
}
.crest-role { margin: 0; font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint); }

/* — Phase clock — */
.clock {
  padding: 13px 14px;
  border: 1px solid var(--rule);
  background: linear-gradient(180deg, rgba(30,47,61,.5), rgba(8,14,19,.5));
}
.clock-phase { margin: 0; font-family: var(--display); font-size: 25px; line-height: 1.05; color: var(--paper); }
.clock-kind {
  margin: 2px 0 10px; font-size: 10px; letter-spacing: .24em;
  text-transform: uppercase; color: var(--brass);
}
.clock-meter { height: 3px; background: rgba(255,255,255,.07); overflow: hidden; }
.clock-meter-fill {
  height: 100%; width: 0;
  background: linear-gradient(90deg, var(--brass-dim), var(--brass-2));
  transition: width .5s var(--ease);
}
.clock-note { margin: 8px 0 0; font-size: 11.5px; color: var(--text-faint); }
/* Only where the ledger's own copy is out of reach — see renderSpend.
   Not keyed to 700px alone: the rail drops to the bottom of the stacked page from 1100
   down, so across the whole 701–1099 tablet band the only spend readout sat ~95% of the
   way down a 1690px document, 592px below the fold. That is exactly the condition the
   ledger's own comment calls "a cap you find out about by the table stopping". Below 1100
   the clock is above the map, so a copy there is worth having; at 1100 and up the rail is
   on screen and a second copy would be furniture. */
.clock-spend { display: none; }
.clock-spend.is-spent { color: var(--brass); }
@media (max-width: 1100px) {
  .clock-spend {
    display: block; margin: 6px 0 0;
    font-family: var(--mono); font-size: 12px; color: var(--text-dim);
  }
  .clock-spend[hidden] { display: none; }
  /* Exactly one readout. Showing this strip without hiding the ledger's printed the same
     figure twice in one column, 173px apart at two different sizes — because below 1100
     the whole left rail moves to the bottom, so both copies live in the same place. The
     clock comes first in that rail, so keeping this one puts the figure higher. */
  .ledger-spend { display: none; }
}

/* — Ledger — */
.ledger { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/*
 * The line numbers, on ink rather than on a dark-surface token.
 *
 * `--text-faint` is tuned against the dark rails; on the parchment it measured 1.84:1
 * against the gradient's darker stop, and `opacity: .75` took it lower still. These
 * numerals are what the typed box's refusals point at — "line 1: F Tri – Mun — a fleet
 * cannot enter Munich" — so a player following that instruction had to count rows.
 * `#5f5a49` is 4.37 and 4.63 against the two stops with the opacity gone.
 */
.flimsy-key {
  text-align: right; font-family: var(--mono); font-size: 10px;
  color: #5f5a49;
}
/* Brass-on-brass. The selected row's band is a 16% gold wash, and the brass numeral on it
   measured **1.05:1** — the number simply was not there, on the one line the player is
   working on. The band itself is the selection cue and does not need the numeral to
   repeat it, so the numeral keeps the colour it can be read in and gains weight instead. */
.flimsy-line.is-sel .flimsy-key { color: #4a4536; font-weight: 600; }

.keytable { width: 100%; border-collapse: collapse; font-size: 13px; }
.keytable td { padding: 4px 0; vertical-align: baseline; }
.keytable td:first-child { width: 130px; white-space: nowrap; }
.jump-hits { margin: 8px 0 0; min-height: 1.2em; font-size: 12px; color: var(--text-faint); }
.sheet-note { margin-top: 14px; font-size: 12px; color: var(--text-faint); line-height: 1.55; }

/* Every confirmation's action row. It was an inline style, so the destructive
   choice and the safe one rendered identically in all three dialogs that ask
   before doing something irreversible. */
/* A row of its own outside `.sheet-body`, so a long sheet cannot scroll its own exit out
   of reach. `flex: none` because the body above it is the part that scrolls. */
/* Inset like the head and the body it sits under — 18px, the panel's one gutter. Moving
   this row out of `.sheet-body` took it out of that padding too, so the commit button
   that restarts a stopped table sat 1px from the panel border while the title above it
   was inset 19px, and the divider ran the full width of an otherwise inset panel. */
.sheet-actions {
  flex: none; display: flex; gap: 8px; flex-wrap: wrap;
  margin-top: 0; padding: 14px 18px 18px; border-top: 1px solid var(--rule);
}
.sheet-actions[hidden] { display: none; }
/* `margin-bottom: 0` because `.ghost-button`'s base carries 8px for the stacked gate, and
   as a stretched flex item that margin came out of the button's own height — leaving
   "Close" 8px shorter than the seal beside it with their bottom edges out of line, and 8px
   of stray space under one side of the row. */
.sheet-actions { align-items: stretch; }
.sheet-actions .ghost-button {
  width: auto; flex: 0 1 auto; padding: 9px 16px; margin-bottom: 0;
}
/* A sheet's commit action wears the wax seal, but sized to itself like everything else
   in this row. `.seal-button` is `display:block; width:100%`, so without this the
   modifier was applied and defined nowhere and the rearm dialog's action rendered as a
   722px full-bleed slab inside a 760px panel, ignoring the row it lives in. */
.sheet-actions .seal-button--sheet { display: inline-block; width: auto; flex: 0 1 auto; }
.sheet-actions .seal-button--sheet .seal-button-face {
  padding: 11px 20px; font-size: 15px;
  /* Room for the longest label it takes, so relabelling to "Raising…" mid-action does not
     collapse it 54px under the pointer that just pressed it. `#joinGo` carries the same
     guard for the same reason. */
  min-width: 168px;
}
.ghost-button--danger { border-color: var(--bad-soft); color: var(--bad-soft); }
.ghost-button--danger:hover {
  border-color: var(--bad); color: var(--bad);
  background: color-mix(in srgb, var(--bad) 10%, transparent);
}

.ledger-rows {
  list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1;
  scrollbar-gutter: stable;
}
.ledger-row {
  display: grid; grid-template-columns: 19px 1fr auto 12px; gap: 9px; align-items: center;
  width: 100%; padding: 5px 0; border: 0; border-bottom: 1px solid rgba(42,58,71,.55);
  background: transparent; color: inherit; font: inherit; text-align: left;
  cursor: pointer; transition: background .15s var(--ease);
}
.ledger-row:hover { background: rgba(255,255,255,.03); }
/* A grid item's default `min-width: auto` means a track will not shrink below
   its own content, so `1fr` is a maximum here and not a limit. Once a seat
   badge joined the name — "Turkey PLAYER" — the name track stopped fitting the
   104px tile the phone lays these out in, the whole row grew past its tile, and
   the ⌖ on the right-hand end of the third column rendered at x=388.5 on a
   375px screen. That gave the entire page a sideways scroll off one glyph.
   With this the track shrinks, the badge wraps under the name, and the tile
   holds. */
.ledger-row > * { min-width: 0; }
/* The row moves the map. That was invisible, so it read as a label you could
   not press — a trapdoor. The mark appears on hover and on keyboard focus. */
/* Kept inside its own row. It sat 7px past the right edge of a 375px screen,
   which gave the whole page a sideways scroll and clipped the glyph. */
.ledger-find {
  opacity: 0; color: var(--brass-2); font-size: 12px; flex: none;
  transition: opacity .15s var(--ease);
}
.ledger-row:hover .ledger-find, .ledger-row:focus-visible .ledger-find { opacity: 1; }
.ledger-row.is-you { background: rgba(201,162,39,.06); }
.ledger-row.is-out { opacity: .35; }
.ledger-swatch { width: 19px; height: 13px; }
/*
 * One line, always — the power, then its seat badge, then the sealed flag.
 *
 * Laid out inline, the badge could take the whole track (it is `max-width: 100%` so its
 * own ellipsis works) and push " sealed" onto a second line, which made the row 59px
 * instead of 38px. Whether it wrapped was decided by the length of a diplomat's surname,
 * so a mid-phase ledger alternated 38 and 59 down the rail — measured 59/38/59/38/59/38/38
 * across the seven powers — and the rail's only scannable table stopped being scannable.
 * Third time this component has produced this shape; the first two were "Turkey PLAYER"
 * overflowing the phone tile and the badge floating 6px off its baseline.
 *
 * As a flex row the badge shrinks and ellipsises, which is what its inner span was built
 * for, and the flag keeps its place. `display: block` in the narrow branch below still
 * wins on source order, so the phone keeps the flag on its own line deliberately.
 */
.ledger-name {
  font-size: 12.5px; font-weight: 500; letter-spacing: .01em;
  display: flex; align-items: baseline; gap: 6px; min-width: 0;
  /* A hard boundary between the name track and the count column. `min-width: 0` lets
     the track shrink but does not stop its contents being painted outside it, and a
     badge that will not shrink was drawn straight over the centre count. Whatever
     cannot fit here is now clipped here, rather than landing on the numbers. */
  overflow: hidden;
}
.ledger-name > .ledger-seat { min-width: 0; }
.ledger-name > .ledger-ready { flex: none; }
.ledger-bar {
  display: block; height: 2px; margin-top: 4px; background: rgba(255,255,255,.08); }
.ledger-bar-fill {
  display: block; height: 100%; transition: width .6s var(--ease); }
.ledger-tally { text-align: right; font-family: var(--mono); font-size: 15px; letter-spacing: -.02em; }
.ledger-delta { font-size: 10px; font-family: var(--mono); color: var(--text-faint); }
.ledger-delta.is-plus { color: var(--verdigris); }
.ledger-delta.is-minus { color: var(--bad-soft); }
/* The build surplus, shaped so it cannot be mistaken for the unit count beside it: a
   bordered chip rather than another bare figure in the same slot. Only drawn in a phase
   that has one. */
.ledger-quota {
  display: inline-block; margin-left: 5px; padding: 0 4px;
  font-family: var(--mono); font-size: 10px; line-height: 1.5;
  border: 1px solid currentColor; border-radius: 2px;
}
.ledger-quota.is-plus { color: var(--verdigris); }
.ledger-quota.is-minus { color: var(--bad-soft); }
.ledger-ready { font-size: 11px; line-height: 1; color: var(--brass); }
.ledger-seat {
  /* inline-block, or a wrap inside the badge splits its border and leaves a
     stray bracket beside the power's name on narrow screens. */
  display: inline-block; white-space: nowrap;
  /* Caps carry at sizes lower case does not, which is why the labels through
     here run small — but 8.5px was past the point where that holds, and this
     badge is how a player tells a human seat from a machine one. */
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  margin-left: 6px; padding: 1px 4px; border: 1px solid currentColor; border-radius: 2px;
  vertical-align: 1px;
  /*
   * It may not shrink below its own word.
   *
   * `.ledger-name` is a flex row and `.ledger-row > * { min-width: 0 }` lets everything in
   * it compress; with `overflow: visible` this badge kept painting at full width while its
   * box was squeezed, so the surplus printed straight through the `sealed` chip beside it.
   * Measured at 1280x800 on a sealed human row: box 34.2px around 50px of text, 9.8px of
   * overprint into a chip starting 0.9px later — `PLAYE̶R̶SEALED` on all six sealed rows.
   * `flex: none` keeps the badge at its natural width and lets the *power's name* take the
   * compression instead, which is the part that can afford it.
   *
   * Third recurrence of `min-width: 0` plus `overflow: visible` in this component. The
   * persona variant escaped it only because its inner span already clips.
   */
  flex: none; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.ledger-seat.is-you { color: var(--brass-2); }
.ledger-seat.is-player { color: var(--verdigris); }
.ledger-seat.is-machine { color: var(--text-faint); }
/* A player whose device has dropped but whose seat is still theirs. Amber, because it is
   a state to notice and not to act on — and distinctly not the diplomat's grey, which is
   what it used to be shown as the instant a socket closed. */
.ledger-seat.is-away { color: var(--warn-soft); }
/* A seat on a table that cannot reach its characters. Not `is-machine`: nothing is
   writing for it, and it will fall into civil disorder. */
.ledger-seat.is-unplayed { color: var(--warn-soft); }
/*
 * A diplomat's name in the seat, rather than the word "machine".
 *
 * Set as a name and not as a label: no uppercase and no tracking, because those
 * belong to a three-letter status word and make a nine-character surname about
 * 45% wider — measured at 89px in a 50px tile, painting into the centre count in
 * the next column, which is precisely the fault "PLA4ER" was.
 *
 * `text-overflow: ellipsis` is the guarantee rather than the plan. Nine
 * characters is the cap in ai/personas.js and most names clear the tile
 * comfortably; the longest do not, and a clipped name with an ellipsis is a name
 * you can still half-read, where an overflowing one damages the column beside it.
 * The specificity here (two classes) also beats the 12px readability floor set
 * further down, which is deliberate — the floor still applies, this just is not
 * overridden by it the way an earlier attempt at 9.5px silently was.
 *
 * One catch with that guarantee: `overflow: hidden` moves an inline-block's baseline to
 * its bottom margin edge (CSS 2.1 §10.8.1), which defeated the `vertical-align: 1px`
 * above it — the six machine rows measured 43px against the human row's 38px, with the
 * diplomat's name floating 6px off the baseline of the power it belongs to. Keeping the
 * clip on an inner span leaves the badge itself a normal inline-block, so the ellipsis
 * survives and the rows are the same height again.
 */
.ledger-seat.is-persona {
  text-transform: none; letter-spacing: .01em; font-style: italic;
  display: inline-flex; max-width: 100%; vertical-align: 1px;
}
.ledger-seat.is-persona > span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.ledger-foot { margin: 10px 0 0; font-size: 10.5px; color: var(--text-faint); }

.rail-nav { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* Leave ends the game. It sat at exactly the weight of Save, its own antidote,
   one pixel away. */
/* De-emphasis only. Danger is `--danger`'s job; this was firing a red hover on
   the shared board's Close, whose own dialog says it destroys nothing. */
/*
 * The remedy for a stopped table, wherever it is offered — the player console and the
 * shared board's note. One rule for both call sites; its spacing used to live in three
 * inline strings at two different values.
 *
 * It wears the wax seal because it is a commit. As a `.ghost-button--tiny` it rendered
 * identically to Clear and Hold-the-rest beside it — the least prominent of the four
 * controls in a console whose dominant element was the *disabled* Seal — which is the
 * same mistake this file records fixing for sheet actions.
 *
 * A line of its own in the console: inline, it landed beside the wrapped tail of the
 * status sentence, next to an orphaned "$0.01." fragment.
 */
.seal-button--remedy { display: block; width: auto; margin: 10px 0 0; }
.seal-button--remedy .seal-button-face { padding: 9px 16px; font-size: 14px; }
/* Scaled with the room, like everything else on that screen, and on the same line as the
   sentence — there the note is wide and the sentence is short. */
.room-note .seal-button--remedy {
  display: inline-block; margin: 0 0 0 12px; vertical-align: middle;
}
.room-note .seal-button--remedy .seal-button-face {
  font-size: clamp(13px, 1.15vw, 18px); padding: 8px 16px;
}
/* Leave ends the game, so it keeps no fill: the base's resting wash is a pressability cue
   and this is the one ghost button that should not invite the press. */
.ghost-button--quiet {
  margin-left: auto; color: var(--text-faint); border-color: transparent; background: transparent;
}
.ghost-button--quiet:hover { border-color: var(--rule-2); color: var(--text-dim); background: transparent; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.table-code {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px;
  padding: 8px 10px; border: 1px solid var(--rule); background: rgba(201,162,39,.05);
}
.table-code-label {
  grid-column: 1 / -1;
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: var(--text-faint);
}
.table-code-value {
  font-family: var(--mono); font-size: 19px; letter-spacing: .22em; color: var(--brass-2);
}
/* The same component, one size up, where the code is the point of the screen rather
   than a reference in the rail. A modifier instead of the inline 26px block this
   replaced, so the two cannot drift apart again. */
.table-code--sheet { margin: 14px 0; }
.table-code--sheet .table-code-value { font-size: 26px; }
/* The address a player reads off this screen and types into a phone. Centred with
   `overflow-wrap: anywhere` it broke inside "html" and left a 7-character tail centred on
   its own line — the hardest possible form to transcribe. Left-aligned, and broken only
   where a URL has a natural seam. */
.sheet-share { text-align: left; min-width: 0; }
.sheet-share code { display: block; overflow-wrap: break-word; word-break: normal; }
/* The table code on its own line, so the part that has to be typed by hand is never the
   tail of a wrapped URL. */
.sheet-share .sheet-share-code { color: var(--brass-2); }

.clock-deadline {
  margin: 6px 0 0; font-family: var(--mono); font-size: 12px; color: var(--brass-2);
}
.clock-deadline.is-urgent { color: var(--bad); }
/* A live deadline belonging to a turn other than the one on screen. It is the only thing
   in the clock still describing the present while everything above it names the turn being
   read, so it is given a rule and a background rather than sitting inside a heading that
   contradicts it — and it carries the phase's own name, which is what makes it readable
   rather than ambiguous. */
.clock-deadline.is-elsewhere {
  margin-top: 8px; padding: 5px 8px;
  border: 1px solid rgba(200,151,58,.42); border-radius: 3px;
  background: rgba(200,151,58,.10); color: var(--warn-soft);
}
.clock-deadline.is-elsewhere.is-urgent {
  border-color: rgba(200,80,70,.55); background: rgba(200,80,70,.12); color: var(--bad);
}
.clock-deadline-live { font-family: var(--sans, inherit); letter-spacing: .01em; }
/* This tail is the instruction, so unlike its sibling it survives the phone. */
.clock-deadline.is-elsewhere .clock-deadline-tail { display: inline; }

/* A quieter twin of the seal, for actions that commit nothing. */
.seal-button--quiet .seal-button-face {
  background: transparent; color: var(--text-dim);
  border: 1px solid var(--rule-2); box-shadow: none;
  font-family: var(--ui); font-size: 12.5px; letter-spacing: .1em; text-transform: uppercase;
}
.seal-button--quiet:hover .seal-button-face {
  transform: none; border-color: var(--brass-dim); color: var(--text); box-shadow: none;
}
.seal-button--quiet:active .seal-button-face { transform: translateY(1px); box-shadow: none; }

/* ══════════════════════  Board  ══════════════════════ */

.board-stage {
  display: flex; flex-direction: column; gap: var(--gap); min-width: 0; min-height: 0;
  /* Centres the frame on the cross axis so `aspect-ratio` can size its width
     from its height. Stretch (the default) makes both axes definite, which
     makes the ratio inert — the frame then framed empty sea. Scoped below,
     because at narrow widths the stretched width is the only thing the flex
     base size can come from. */
  align-items: center;
  /* A size container, so the frame can take the smaller of its two fits — the same
     treatment `.room-board` was given, and which this, its sibling, never received. */
  container-type: size; }
/* The centring above is for the frame. The console is the stage's other child
   and must keep the full width, or the control bar changes size every time the
   phase changes the sentence inside it. */
.board-stage > .console { align-self: stretch; }

/*
 * The frame takes whichever of its two fits is smaller, like the room's.
 *
 * `flex: 1; height: 100%` made the height definite and `max-width: 100%` then clamped the
 * width, leaving `aspect-ratio` nothing to do — the identical construction, and identical
 * failure, that `.room-board .board-frame` was rewritten to escape. Measured before this:
 * 581x634.5 at 1101x800 and 760x834.5 at 1280x1000, ratio 0.91 against a declared 1.0893,
 * with ~17% of the frame's height empty sea inside its own border. The room was fixed and
 * its sibling was not, because a single viewport was checked and the result generalised.
 *
 * `100cqh` is the stage's height, so the console and the legend below the frame are
 * subtracted along with the two flex gaps. Every `var()` here carries a fallback: an
 * unset one makes the whole `calc()` invalid and collapses the frame to nothing, which is
 * exactly how the shared board came to render no map at all.
 */
.board-frame {
  aspect-ratio: 1403 / 1288;
  width: min(100cqw, calc((100cqh - var(--console-h, 66px) - var(--stage-legend-h, 24px)
    - (var(--gap) * 2)) * var(--map-ratio)));
  height: auto; margin: auto;
  position: relative; flex: none; min-height: 0;
  border: 1px solid var(--rule-2);
  background: var(--sea-deep);
  overflow: hidden;
  box-shadow: inset 0 0 90px -20px #000, 0 20px 60px -30px #000;
}
.board-frame::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(201,162,39,.14);
}
.board-mount { position: absolute; inset: 0; transition: opacity 240ms linear; }
.board-mount svg { display: block; width: 100%; height: 100%; touch-action: none; }

/* The theatre sits over the flat board in the same frame and opens on a view
   that matches it exactly, so the handover reads as the map lifting rather
   than as one picture being swapped for another. It never takes pointer
   events: the flat board underneath stays the thing you interact with. */
.theatre {
  position: absolute; inset: 0; z-index: 2;
  display: block; width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
  transition: opacity 200ms linear;
}
/* Only takes the pointer while it is actually playing, and then only to get
   out of the way: the flat board underneath is what you interact with. */
.theatre.is-live { opacity: 1; pointer-events: auto; cursor: pointer; }
.board-frame.is-theatre .board-mount { opacity: 0; }
.board-frame.is-theatre .board-tools { opacity: .35; pointer-events: none; transition: opacity 200ms linear; }

.board-tools {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  display: flex; flex-direction: column; gap: 4px;
}
.tool {
  width: 30px; height: 28px; display: grid; place-items: center;
  background: rgba(8,14,19,.86); border: 1px solid var(--rule);
  color: var(--text-dim); font-size: 14px; line-height: 1;
  backdrop-filter: blur(4px);
}
.tool:hover { border-color: var(--brass-dim); color: var(--brass-2); }
.tool--wide { font-family: var(--display); font-size: 13px; }
.tool.is-off { opacity: .5; }

/* Beneath the map, in the flow. It used to be absolutely positioned inside the
   frame, which put it under the zoom column on a phone and over the map's top
   edge everywhere; and now that the frame is centred, an absolute offset from
   the stage would float it over empty space beside the map. */
.board-legend {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 3px 14px;
  padding: 2px 4px 0;
  font-size: 10px; letter-spacing: .06em;
}
.legend-item { display: flex; align-items: center; gap: 5px; color: var(--text-dim); }
.legend-swatch { width: 9px; height: 9px; }

.tooltip {
  position: absolute; z-index: 6; pointer-events: none;
  padding: 6px 9px; max-width: 240px;
  background: rgba(4,8,11,.94); border: 1px solid var(--rule-2);
  font-size: 11.5px; line-height: 1.45;
  box-shadow: 0 8px 26px -10px #000;
}
.tooltip-name { font-family: var(--display); font-size: 14px; color: var(--paper); }
/* The power's colour as a swatch, its name in a legible token — the same split the press
   card and the record rows use. Set as the text colour it put four of seven powers between
   2.13:1 and 3.33:1; this surface and the standings table were the two places that still
   did it after the press card was fixed. */
.tooltip-meta, .standing-power {
  color: var(--text-dim); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  position: relative; padding-left: 11px;
}
.standing-power { text-transform: none; letter-spacing: .01em; font-size: inherit; }
.tooltip-meta::before, .standing-power::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--power, var(--neutral)); box-shadow: 0 0 0 1px rgba(0,0,0,.5);
}
.tooltip-order { font-family: var(--mono); font-size: 11px; color: var(--brass-2); margin-top: 3px; }

/* — Console — */
.console {
  display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  background: linear-gradient(180deg, rgba(30,47,61,.44), rgba(8,14,19,.6));
}
.console-status { font-size: 12.5px; color: var(--text-dim); min-height: 1.4em; }
.console-status b { color: var(--paper); font-weight: 500; }
.console-status em { color: var(--brass-2); font-style: normal; }
.console-actions { display: flex; align-items: center; gap: 7px; }
.console-actions .ghost-button, .seal-button--console { white-space: nowrap; flex: none; }
/* Room for the longest label this seal ever carries — "Read — next result (1 more)" — so
   the actions column stops resizing with its own text. Sized from the button, the column
   swung 289px to 109px between states, which squeezed the sentence explaining what just
   happened into three lines exactly when it carried the most, shrank the map by 12.5px,
   and slid `#clearOrders` 180px sideways under the pointer. `#joinGo` and
   `.seal-button--sheet` already carry the same guard for the same reason.
   316px, not 232: the first figure was measured in the fallback stack, where that label is
   229px, but with Bodoni Moda loaded it is 312.5px — so the guard never bound and the
   column still swung 80.5px every phase. A width guard has to be measured in the face the
   text is actually set in. */
.seal-button--console { width: auto; min-width: 316px; }
.seal-button--console .seal-button-face { padding: 10px 22px; font-size: 14.5px; }

/* ══════════════════════  Right rail  ══════════════════════ */

.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--rule); }
.tab {
  position: relative; padding: 8px 11px; background: transparent; border: 0;
  font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint);
}
.tab.is-on { color: var(--brass-2); }
.tab.is-on::after {
  content: ''; position: absolute; left: 11px; right: 11px; bottom: -1px;
  height: 1px; background: var(--brass);
}
/* Centred against the label rather than pinned to the top of the control. Pinned at
   `top: 5px`, it stayed put while the tab grew to the 44px mobile minimum, so the dot
   sat 14.5px above the centre of its own button and read as a stray mark rather than a
   badge on Press. Inline-block in the flow keeps it beside the word at every height. */
.tab-dot {
  display: inline-block; vertical-align: middle;
  width: 5px; height: 5px; margin-left: 5px;
  border-radius: 50%; background: var(--verdigris);
}
/* A class selector outranks the UA sheet's rule for [hidden], so without this the
   dot would be permanently lit the moment it stopped being absolutely positioned. */
.tab-dot[hidden] { display: none; }
/* Once there is a number to show, the dot becomes the number. A 5x5px dot says only that
   something arrived; how many is what a player decides on, and it was on no screen at any
   width. Sized from the text it now carries rather than kept circular by force. */
.tab-dot--count {
  width: auto; height: auto; min-width: 15px; padding: 1px 4px;
  border-radius: 7px; line-height: 1.2;
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  color: #10161c; text-align: center;
}

.pane { display: none; flex: 1; min-height: 0; flex-direction: column; gap: 12px; overflow: hidden; }
.pane.is-on { display: flex; }

/* — Order flimsy: a telegram pad — */
/* The pad's rules used to be printed on the pad: one repeating gradient down
   the whole panel at a 26px pitch. But an order is not 26px tall — it is 44
   with a note under it, more when the text wraps — so the rows and the ruling
   ran at different rates and drifted through each other. Every few rows a line
   came out struck through its own order rather than sitting under it.
   So the paper is blank now and each row carries its own rule, which is what
   a pad of forms actually is: the line belongs to the entry, not to the sheet.
   The unfilled tail below the last order keeps the printed ruling, so the pad
   still reads as a pad. */
.flimsy {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  background: linear-gradient(180deg, #e8dfc6, #d9cdae);
  border: 1px solid #8d8064;
  box-shadow: 0 10px 30px -16px #000;
  color: #23201a;
  /* Room above it when the phone layout scrolls the result into view, so the
     heading lands under the top edge rather than flush against it — and so a
     small late reflow costs a few pixels of margin instead of the title. */
  scroll-margin-top: 12px;
}
.flimsy-head {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 8px 11px 6px; border-bottom: 1px solid rgba(0,0,0,.22);
  /* `#55503f`, not the `#6a6153` this was. That ink was the one in the product that did
     not clear AA: 4.58:1 against the parchment's light stop and **3.86 against its dark
     one**, so where it actually sits it failed — averaging the gradient produced the 4.21
     that made it look borderline rather than wrong. This measures 6.06 and 5.10 against
     the same two stops, so it passes against the worse of them, which is the only honest
     way to read a gradient. Every other surface in this file sits between 6.1 and 15.7:1;
     this was the outlier, not the standard. */
  font-size: 9.5px; letter-spacing: .2em; text-transform: uppercase; color: #55503f;
}
.flimsy-code { font-family: var(--mono); letter-spacing: .06em; }
/* The empty message belongs where the first line would be. As a plain sibling
   after the scroll area it rendered at the foot of a column of blank rules,
   reading as a footnote to nothing. */
.flimsy-empty { order: 1; }
.flimsy-lines {
  order: 2; list-style: none; margin: 0; padding: 4px 0; overflow-y: auto; flex: 1;
  display: flex; flex-direction: column;
}
/* The blank remainder of the sheet, ruled at the same pitch and picking up
   where the last order left off. `flex: 1 0 26px` so it takes whatever is
   left over, and still shows one line when the pad is nearly full. */
.flimsy-lines::after {
  content: ''; flex: 1 0 26px;
  background: repeating-linear-gradient(180deg, transparent 0 25px, rgba(0,0,0,.16) 25px 26px);
}
/* Rows keep their own height inside the column — without this a full pad
   squeezes them under their own text rather than scrolling. */
.flimsy-line, .flimsy-rule { flex: none; }
/* A result list runs to seven powers and overflows this pane by a couple of
   hundred pixels. With the platform's overlay scrollbars there was nothing at
   all to say so, and the two powers below the fold are as much the turn as the
   five above it. */
/* A classic scrollbar that occupies layout, not the platform's overlay one:
   `scrollbar-width` is deliberately NOT set here, because specifying it makes
   Chrome use the standard styling and ignore these rules — which left the
   result list with an invisible scrollbar and nothing to say half of it was
   below the fold. */
/* Every scroller in this file, one rule. It was written twice by hand — for the order
   list and the room's seats — and the third one, `.sheet-body`, was missed: at 1280x800
   the Shortcuts sheet clips its last row and note at 643 against 605, silently, because
   the platform's overlay scrollbars show nothing until you already know to scroll. */
/* Every scroller in the product, keyed off one class rather than a hand-kept list of
   selectors. The list was written after this file had been caught three times and then
   missed three more — `.press-log`, `.ledger-rows` and `.record-list` were all
   `overflow-y: auto` with no gutter and no fade, which is how the press log came to hide
   214px of newer correspondence with nothing on screen to say so. A shared class cannot
   be forgotten by the next scroller the way an enumeration can. */
.scroller::-webkit-scrollbar { width: 8px; }
.scroller::-webkit-scrollbar-track { background: rgba(0,0,0,.28); }
.scroller::-webkit-scrollbar-thumb { background: var(--rule-2); border-radius: 4px; }
.scroller::-webkit-scrollbar-thumb:hover { background: var(--brass-dim); }
.scroller.is-more {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
}
/* Anything the app scrolls into view inside a scroller must clear the fade, or it lands
   in it: `scrollIntoView({block:'nearest'})` parks an element flush with the scrollport
   edge, which is exactly the 26px the mask dissolves. Measured on the rearm dialog's
   refusal at 360x780 — the third line rendered at 0.12 alpha, the tail of the one message
   explaining why a stopped table will not restart. */
.scroller :is(.gate-fine, .gate-note, .press-note) { scroll-margin-bottom: 34px; }
/* ...and a fade at the boundary, so the clipped row is not mistaken for the
   last one. Removed once the list is scrolled to its end. */
/* Held back until the sequence has shown it. */
body.is-resolving .flimsy-lines { visibility: hidden; }
/* The empty line is the one thing left visible, because it is the only thing in
   this panel that can say why the rest of it is gone. Hidden along with the list,
   the pad spent the whole sequence — 11.1s measured — as a heading reading
   "What happened / F1901M" over blank parchment, the one held surface with no
   words on it while the clock beside it said "working it out…". `renderFlimsy`
   owns what it says; here it only has to be allowed to say it. */
body.is-resolving .flimsy-head { opacity: .45; }
.flimsy-line {
  display: grid; grid-template-columns: 11px 14px 1fr 16px; gap: 6px; align-items: baseline;
  padding: 4px 11px 5px; min-height: 26px;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
  cursor: pointer; border-left: 2px solid transparent;
  border-bottom: 1px solid rgba(0,0,0,.16);
}
.flimsy-line:hover { background: rgba(0,0,0,.055); }
/* A result row cannot be edited. It stays readable rather than pretending to
   be a control, and only offers the pointer where a click will move the map. */
/* The global ring sits outside the element, which the scrollers these live in
   clip away. Inset it, and darken it on the flimsy where brass on paper is
   about 1.6:1. */
.flimsy-line:focus-visible { outline: 2px solid #6b5413; outline-offset: -2px; }
.ledger-row:focus-visible, .record-turn:focus-visible {
  outline: 2px solid var(--brass-2); outline-offset: -2px;
}
/* Result rows carry no shortcut key, so they are a three-column row. Left in
   the four-column form their text landed in the 14px pip track and broke into
   a vertical column of single characters — after every single resolution. */
.flimsy-line--result { grid-template-columns: 14px minmax(0, 1fr) 16px; cursor: default; }
.flimsy-line--result .flimsy-note { grid-column: 1 / -1; }
.flimsy-line--result[title] { cursor: zoom-in; }
.flimsy-line.is-sel { border-left-color: var(--brass); background: rgba(201,162,39,.16); }
/* A line with no order on it yet — "A Bud —". It was `#8f8878`, 2.23:1 on the parchment's
   darker stop, and these are how a player sees which units are still unordered, which is
   the question the pad exists to answer. `#5d5747` is 4.55 and 4.82 against the two stops
   and still reads as lighter than a written order (11.98:1), which is the distinction the
   colour is carrying. */
.flimsy-line.is-blank .flimsy-text { color: #5d5747; }
.flimsy-pip { width: 9px; height: 9px; border: 1px solid rgba(0,0,0,.45); align-self: center; }
.flimsy-text { overflow-wrap: anywhere; }
.flimsy-text .void { color: var(--bad-paper); }
.flimsy-mark { font-family: var(--ui); font-size: 12px; text-align: right; align-self: center; }
.flimsy-mark.ok { color: var(--good-paper); }
.flimsy-mark.no { color: var(--bad-paper); }
.flimsy-mark.cut { color: var(--warn); }
.flimsy-note { grid-column: 2 / -1; font-family: var(--ui); font-size: 10.5px; color: #55503f; }
.flimsy-empty { padding: 12px 11px; font-size: 11.5px; color: #55503f; font-style: italic; }
.flimsy-rule {
  padding: 9px 11px 2px; margin-top: 2px;
  font-size: 9px; letter-spacing: .2em; text-transform: uppercase; color: #55503f;
  border-top: 1px solid rgba(0,0,0,.16);
}
.flimsy-lines > .flimsy-rule:first-child { border-top: 0; margin-top: 0; padding-top: 4px; }

/* — Typed orders — */
.typebox { border: 1px solid var(--rule); padding: 9px 10px; background: rgba(8,14,19,.45); }
/* Adjustments: the box reads movement orders and there is no build syntax for
   it. It says so in its own label and stops inviting a try. */
.typebox.is-off { opacity: .5; }
.typebox.is-off textarea { cursor: not-allowed; }
/*
 * Collapsed rather than merely greyed, in the phase where it cannot be used at all.
 *
 * Builds and removals are made on the map — the label says so — and beneath that sentence
 * sat a disabled three-row textarea and a disabled Apply, measured at 209px, which is
 * 24.8% of an 844px phone. The largest single block on the screen during adjustments was
 * a control announcing that it does not work. The label stays: it is the instruction, and
 * it is the reason the rest is gone.
 */
.typebox.is-off textarea,
.typebox.is-off .typebox-foot { display: none; }
.typebox.is-off { opacity: 1; padding-bottom: 9px; }
.typebox.is-off .typebox-label { margin-bottom: 0; color: var(--text-dim); }
.typebox-label { display: block; font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 6px; }
.typebox textarea, .press-form textarea {
  /* Dragged past the pane's height it used to clip the hint box — and its
     Support and Convoy buttons — with no scrollbar to reach them. */
  width: 100%; max-height: 40vh; resize: vertical; padding: 7px 8px;
  background: rgba(0,0,0,.4); border: 1px solid var(--rule);
  font-family: var(--mono); font-size: 11.5px; line-height: 1.55;
}
/* Apply sits on the bottom edge of this row, not in the middle of it.
   The pane hands its leftover height to the order pad, so this box grows
   *upward* — its bottom is pinned and its top rises. With the row centred, a
   refusal message that wrapped to a 150px block put Apply 58px above where it
   had just been, and a click aimed at it landed on the empty half of the row
   and did nothing at all. Typing is the thing that reveals the message and
   Apply is the thing that acts on it, so Apply cannot be what the message
   moves. Pinned to the pinned edge, it stays put however long the note runs. */
.typebox-foot, .press-foot {
  display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; margin-top: 7px;
  /* This line carries the parser's refusals — "line 1: F Bre – Mun — a fleet
     cannot enter Munich" — which is copy a player has to read to fix a rejected
     order. It was the smallest text on the screen. */
  font-size: 12.5px; color: var(--text-faint);
}
/* And a ceiling on the message itself, so a bad paste cannot eat the order pad
   above it. Past four lines it scrolls. */
/* Four lines, not three-and-a-bit. The parser's refusals name the line, the order and the
   reason — "line 1: F Tri – Vie — a fleet cannot enter Vienna — it is landlocked" — and at
   5.6em a four-line message measured 94px of content in a 70px box, ending mid-clause on
   "…so its army cannot be" with the reason cut off. It scrolls, and it carries `.scroller`
   so it fades, but a sentence whose last clause is the answer should not need scrolling to
   finish. The ceiling stays, because a bad paste must not eat the pad above it. */
#typeboxNote { max-height: 7.4em; overflow-y: auto; min-width: 0; }
/*
 * The refusal gets the row to itself, above the button rather than beside it.
 *
 * Sharing a flex row with a 100px Apply left it 136px wide and 70px tall against 75px of
 * content — clipped — so "line 1: F Tri – Vie — a fleet cannot enter Vienna — it is
 * landlocked" wrapped to four ragged lines with the last one cut off. That sentence is
 * the most specific help in the product: it names the line, the order and the reason, and
 * it was the one thing on the screen too narrow to read. The wrap only happens when there
 * is something to say, so an empty note still costs nothing.
 */
.typebox-foot { flex-wrap: wrap; }
.typebox-foot > #typeboxNote { flex: 1 0 100%; }
.typebox-foot > #applyTyped { margin-left: auto; }
/* The press form's status line had no rule at all, while its twin below the
   order box has proper good/bad states. */
/*
 * The notification opt-in, under the press composer.
 *
 * Sits below the send row rather than in it: it is a one-time decision, not part
 * of writing a message, and putting it beside Send would make it look like a
 * choice about the message in the box.
 *
 * Declared here, with the other .press-* base rules, and not 200-odd lines below the
 * responsive blocks: a media query adds no specificity, so a base rule written after one
 * simply wins, and the mobile legibility bump for this control was silently dead.
 */
.press-alert {
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  margin: 8px 0 0; min-width: 0;
}
.press-alert small { font-size: 10.5px; line-height: 1.45; color: var(--text-faint); }

.press-note { font-size: 11px; color: var(--text-faint); }
.press-note.is-bad { color: var(--bad-soft); }
/* The good state was missing, so "Sent." was styled exactly like the two refusals
   above it — matching #typeboxNote, which has had both all along. */
.press-note.is-good { color: var(--verdigris); }
/* The letter went; it will not be answered. Not a failure — the send worked — so it takes
   the amber the gate uses for a consequence rather than the red it uses for a refusal. */
.press-note.is-warn { color: var(--warn-soft); }
#typeboxNote.is-bad { color: var(--bad-soft); }
#typeboxNote.is-good { color: var(--verdigris); }

.hint-actions { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.hintbox {
  border-left: 2px solid var(--brass-dim); padding: 7px 10px;
  font-size: 11.5px; line-height: 1.5; color: var(--text-dim);
  background: rgba(201,162,39,.05);
  min-height: 2.4em;
}
.sheet-body kbd, .hintbox kbd {
  font-family: var(--mono); font-size: 10px; padding: 1px 4px;
  border: 1px solid var(--rule-2); background: rgba(0,0,0,.4); color: var(--brass-2);
}

/* — Press — */
.press-log { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 9px; padding-right: 2px; }
.wire {
  border: 1px solid var(--rule); border-left-width: 2px;
  padding: 8px 10px; background: rgba(8,14,19,.5);
}
.wire-head {
  display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px;
  font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase;
  /* Wrap rather than squeeze. This pane is about 250px wide and a diplomat's full name
     with a turn code beside it does not always fit; forced onto one line the name broke
     mid-word instead, which is worse for the one thing on the card you scan by. */
  flex-wrap: wrap;
}
/* The diplomat who wrote it — the bot-identity surface, so not the smallest thing on
   the card. It was 9.5px against a 12.5px body, and coloured with the raw power token,
   which put four of seven senders between 2.10 and 3.29:1 on the widest supported
   viewport. The power is already on this card as the coloured left border of .wire, so
   the name does not have to carry it. */
/* Set as a name, not as a label — the same rule .ledger-seat.is-persona and
   .room-seat-who already follow, so one diplomat reads as one person on all three
   surfaces. `.wire-head` sets uppercase for the micro-labels in it, and this inherited
   it: "COUNT TADEUSZ ZBROŻEK" in the very register the stylesheet documents as wrong
   for names. The uppercase micro-label register belongs to .wire-power below, which
   also restores the case contrast between the two stacked lines. */
.wire-from {
  font-weight: 600; font-size: 12.5px; letter-spacing: .01em; color: var(--paper);
  text-transform: none;
}
/* Sized explicitly like every one of its siblings. It was the last element of this card
   left inheriting `.wire-head`'s 9.5px — the size this file elsewhere calls "not a size, a
   texture" — and it is the only thing on a letter saying which turn it belongs to. */
.wire-when {
  color: var(--text-faint); font-family: var(--mono); letter-spacing: 0; font-size: 11px;
  /* The head is set in uppercase for its micro-labels, which turned "S1901M · 2m" into
     "S1901M · 2M" — a minutes-ago figure rendered as a capital M immediately after a
     phase code that ends in one. The turn code is already uppercase in the data. */
  text-transform: none;
  /* It carries a time as well as a phase code now, and the head is a flex row: without
     this the two halves break across two lines and take the sender's name with them. */
  flex: none; white-space: nowrap;
}
/* Machine-written — a qualifier on the signature, on the same micro-label line as the
   country it speaks for. Boxed rather than merely coloured, because it sits next to a
   word in the identical size and register and would otherwise read as part of it. */
.wire-mark {
  flex: none; padding: 1px 5px;
  font-size: 10px; letter-spacing: .12em;
  color: var(--text-faint); border: 1px solid var(--rule-2);
}
.wire-body {
  overflow-wrap: anywhere; font-size: 12.5px; line-height: 1.55; color: var(--text); white-space: pre-wrap; }
.wire--mine { background: rgba(201,162,39,.05); }
/* Provenance, not an error. It was --bad-soft — the token :root documents as the error
   colour, shared with .gate-note.is-bad and .press-note.is-bad — printed on every
   letter written by a model in an unclaimed seat. On the default configuration that is
   every incoming letter, so the marker signalled nothing, painted the whole log in the
   error colour, and left a genuine refusal in the same pane indistinguishable from it. */
.wire-machine {
  margin-top: 4px; font-size: 11px; color: var(--text-faint);
  letter-spacing: .04em; font-style: italic;
}
/* The country the sender speaks for, under their name. The pane addresses powers on
   its pills and letters arrive from people; without this there was nothing on the card
   tying "Count Tadeusz Zbrożek" to the Germany you wrote to. */
.wire-power {
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-faint); margin: -2px 0 5px;
  /* A row, because it carries the machine mark beside the country now. Set as plain
     inline spans the two ran together as "ENGLANDMACHINE". */
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
/* Now above the body, so it needs to read as a heading for it rather than as a footnote
   after it: brass, because this is the actionable line on the card. */
.wire-to {
  font-size: 11px; letter-spacing: .04em; color: var(--brass-2);
  margin: 0 0 4px; font-style: normal;
}
/* Mid-reply. A quiet pulse rather than a label, because the pill's whole width is the
   power's name and there is nowhere to put a word — and because this is a state that
   comes and goes on its own rather than something to act on. `prefers-reduced-motion`
   holds it still and leaves the colour, which is the part that carries the meaning. */
.press-pill.is-writing { border-color: var(--verdigris); animation: pill-writing 1.6s ease-in-out infinite; }
@keyframes pill-writing { 0%, 100% { opacity: 1 } 50% { opacity: .55 } }

.press-empty { font-size: 12px; color: var(--text-faint); font-style: italic; padding: 8px 0; }

.press-form { border: 1px solid var(--rule); padding: 9px 10px; background: rgba(8,14,19,.45); }
.press-form select {
  width: 100%; padding: 7px 8px; border-radius: 0;
  background: rgba(0,0,0,.4); border: 1px solid var(--rule); color: var(--text);
  font-size: 12px;
}
.press-form select:hover { border-color: var(--brass-dim); }
/* The composer's own micro-labels, in the register .gate-field > span, .typebox-label
   and .rail-heading already share. */
/* The composer's controls carry the same invalid mark the gate's do — including the
   recipient row, which is a group of pills rather than a single control. */
.press-form :is(select, textarea)[aria-invalid="true"] { border-color: var(--bad-soft); }
.press-to[data-invalid="true"] .press-pill { border-color: var(--bad-soft); }
.press-label {
  display: block; margin-bottom: 4px;
  font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint);
}
.press-kind-row { display: grid; gap: 6px; margin-bottom: 7px; }
.press-to { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 7px; }
.press-pill {
  padding: 4px 8px; border: 1px solid var(--rule); background: transparent;
  font-size: 10.5px; letter-spacing: .08em; color: var(--text-faint);
}
/*
 * Chosen is brass, the way every other chosen thing here is brass — .record-turn.is-on
 * and .power-chip[aria-checked="true"] already say it this way. The power's own colour
 * survives as a swatch, which carries the identity without having to be legible as
 * text: set as the label's colour it made Germany 1.95:1 and read as disabled.
 */
.press-pill.is-on {
  border-color: var(--brass); color: var(--paper);
  background: rgba(201,162,39,.09);
}
.press-pill--power::before {
  content: ''; display: inline-block; vertical-align: baseline;
  width: 6px; height: 6px; margin-right: 5px; border-radius: 50%;
  background: var(--power, var(--neutral));
  /* Visible against both the pill and the rail behind it, whatever the power. */
  box-shadow: 0 0 0 1px rgba(0,0,0,.55);
}
.press-pill.is-on:hover { border-color: var(--brass-2); color: var(--text); }

/* — Record — */
.record-list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 7px; }
.record-turn {
  border: 1px solid var(--rule); padding: 8px 10px; background: rgba(8,14,19,.5);
  cursor: pointer; transition: border-color .15s var(--ease);
}
.record-turn:hover { border-color: var(--brass-dim); }
.record-turn.is-on { border-color: var(--brass); background: rgba(201,162,39,.07); }
.record-head { display: flex; justify-content: space-between; align-items: baseline; }
.record-label { font-family: var(--display); font-size: 14.5px; color: var(--paper); }
.record-code { font-family: var(--mono); font-size: 10px; color: var(--text-faint); }
.record-lines { margin: 6px 0 0; padding: 0; list-style: none; font-family: var(--mono); font-size: 10.5px; line-height: 1.6; }
.record-lines li { display: flex; gap: 6px; color: var(--text-dim); }
.record-lines .rl-mark { width: 10px; flex: none; text-align: center; }
/* Whose order this is: the letter in a legible token, the power's colour as a swatch
   beside it — the same split the press card uses, for the same reason. */
.record-lines .rl-power {
  flex: none; color: var(--text); position: relative; padding-left: 9px;
}
.record-lines .rl-power::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--power, var(--neutral)); box-shadow: 0 0 0 1px rgba(0,0,0,.5);
}
.record-lines .ok { color: var(--verdigris); }
.record-lines .no { color: var(--bad-soft); }
.record-lines .cut { color: var(--brass); }
.record-live { font-size: 11px; color: var(--text-faint); font-style: italic; }

/* ══════════════════════  Overlays  ══════════════════════ */

.sheet {
  /* Above the toast stack (70), not below it. A modal is the one thing on screen the
     player has been asked to deal with, and press toasts were painting straight across
     it: measured at 1280x800, two "A message from Pallister." cards at y505-557 over the
     "Your table is open" panel at y197-602, on top of the footer — the one screen a new
     opener has to read and copy a code from. Behind the scrim they are dimmed out, which
     is what a modal is for. */
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center; padding: 24px;
  /* A grid's automatic track sizes to its content and is not clamped by the
     container, so one unbreakable string — the join URL on the table-code
     sheet — grew the track past the viewport and the panel's `100%` followed
     it. At 360px the panel came out 362 wide starting 24 in, which put its
     close button 7px off the right edge of the screen with nothing to scroll.
     `minmax(0, 1fr)` is what stops a track growing to its content. */
  grid-template-columns: minmax(0, 1fr);
  background: rgba(3,6,9,.78); backdrop-filter: blur(3px);
}
.sheet-panel {
  width: min(760px, 100%); min-width: 0;
  max-height: min(84vh, 100dvh - 48px); display: flex; flex-direction: column;
  border: 1px solid var(--rule-2);
  background: linear-gradient(180deg, #16232f, #0a1218);
  box-shadow: 0 40px 110px -30px #000;
}
.sheet-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 18px; border-bottom: 1px solid var(--rule);
}
.sheet-head h2 { font-family: var(--display); font-size: 24px; color: var(--paper); min-width: 0; }
/* The join address is the one thing on this sheet somebody has to read off a
   screen and type into a phone, and it is a single unbreakable token. */
.sheet-body code { overflow-wrap: anywhere; }
.sheet-close { background: transparent; border: 0; font-size: 24px; line-height: 1; color: var(--text-faint); }
.sheet-close:hover { color: var(--brass-2); }
.sheet-body { padding: 18px; overflow-y: auto; font-size: 13.5px; line-height: 1.66; color: var(--text-dim); }
.sheet-body h3 {
  font-family: var(--display); font-size: 17px; color: var(--brass-2);
  margin: 20px 0 6px;
}
.sheet-body h3:first-child { margin-top: 0; }
.sheet-body p { margin: 0 0 10px; }
.sheet-body ul { margin: 0 0 12px; padding-left: 20px; }
.sheet-body li { margin-bottom: 5px; }
.sheet-body code { font-family: var(--mono); font-size: 12px; color: var(--brass-2); }
.sheet-body table { width: 100%; border-collapse: collapse; margin: 6px 0 14px; font-size: 12.5px; }
.sheet-body th, .sheet-body td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--rule); }
.sheet-body th { font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint); font-weight: 600; }

/*
 * The curtain that used to cover the board for 700ms while a solo game adjudicated
 * in the page went with the solo game. A phase is resolved by the server now, and
 * what covers the board while that happens is the theatre.
 */

.toast-stack {
  position: fixed; z-index: 70; left: 50%; transform: translateX(-50%);
  /* Above the map's colour key, whose height and position depend on the layout
     and so are measured. A toast used to land across it and hide four powers'
     swatches at the moment the board had just changed colour. */
  bottom: max(96px, var(--legend-clear, 96px));
  display: flex; flex-direction: column; gap: 7px; align-items: center; pointer-events: none;
}
.toast {
  padding: 9px 15px; max-width: 62ch;
  background: rgba(6,11,15,.95); border: 1px solid var(--rule-2);
  border-left: 2px solid var(--brass);
  font-size: 12.5px; box-shadow: 0 12px 34px -14px #000;
  animation: rise .3s var(--ease);
}
.toast.is-bad { border-left-color: var(--bad-soft); }
.toast.is-good { border-left-color: var(--verdigris); }
@keyframes rise { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }

/* ══════════════════════  Board internals  ══════════════════════ */

.prov { cursor: default; }
.prov-fill { transition: fill .3s var(--ease); }
.hit { fill: transparent; stroke: none; cursor: pointer; }

.prov-label {
  /* No font-size here: each label sets its own in em so the whole layer can be
     rescaled on zoom from one value on the group. */
  font-family: var(--ui); font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase;
  fill: #4a4335; pointer-events: none; paint-order: stroke;
  stroke: rgba(228,215,185,.62); stroke-width: 2.4px; stroke-linejoin: round;
}
.prov-label--land { fill: #4a4335; }
/* The chart's wide tracking is what the map is set in, but it is a luxury a
   nine-letter name in a small province cannot afford: EDINBURGH ran into CLYDE
   once the names were allowed to grow with the zoom. Under half a pixel per gap
   at the sizes this applies at, and it is the difference between two names and
   one smear. */
.prov-label--tight { letter-spacing: -.05em; }
/* Every name, not only the land ones. The control is labelled "Province names"
   and a sea is a province you order fleets into — switching names off and being
   left with five of them ("Barents Sea", "Skagerrak", "North Atlantic"…) is the
   button doing four fifths of what it says. The old category rule happened to
   take the seas with it; it is gone, so this has to say so itself. */
.labels-off .prov-label { opacity: 0; }
/*
 * A name the board has no room for at this size, left out until it has.
 *
 * This replaces three rules that hid names by category rather than by whether they
 * actually collided — all land names below a zoom threshold, all sea names with
 * them, and every name the generator had marked cramped. Category hiding cannot be
 * right at both ends: at 390x844 fit it left the map with **0 names**, and the one
 * control that could override it turned all 50 back on at once, **42 of them
 * overlapping**. Neither state is a map you can read a province off.
 *
 * `board.js` decides this now, from the boxes themselves — see `cullLabels`. The
 * names come back one at a time as the board is zoomed, which is the behaviour the
 * thresholds were reaching for and could only approximate in steps.
 */
.prov-label.is-crowded { opacity: 0; }

.sc-ring { fill: none; stroke: #6b5a33; stroke-width: 1.3; }
.sc-core { stroke: rgba(0,0,0,.45); stroke-width: .8; }
.sc-home { stroke: var(--brass); stroke-width: 1.6; stroke-dasharray: 2.5 2.2; fill: none; opacity: .8; }

.unit { cursor: pointer; }
.unit-body { stroke: rgba(10,12,10,.85); stroke-width: 1.4; }
.unit-halo { fill: none; stroke: var(--brass-2); stroke-width: 2.4; opacity: 0; transition: opacity .18s var(--ease); }
/*
 * The shared board is read from across a room and inherited a seat screen's type scale.
 *
 * Measured with `getScreenCTM` including the world transform: the power's initial on a
 * counter — the only cue for whose unit it is besides colour, and colour alone does not
 * separate Germany from England at that size — came to **4.08 CSS px** at 1440x900 and
 * **3.58** at 1280x800. That is the identity of every piece on the board, at a third of
 * the smallest size this file sets anywhere else. The map's own labels have a crowding
 * pass that keeps them legible; the counters had nothing.
 */
.room .unit-letter { font-size: 11px; stroke-width: 2px; }
.unit-letter {
  font-family: var(--ui); font-size: 7.5px; font-weight: 700; letter-spacing: 0;
  fill: rgba(255,255,255,.94); paint-order: stroke;
  stroke: rgba(0,0,0,.55); stroke-width: 1.6px; stroke-linejoin: round;
  pointer-events: none;
}
.cursor-ring {
  fill: rgba(232,205,106,.10); stroke: var(--brass-2); stroke-width: 3;
  stroke-dasharray: 9 5; stroke-linejoin: round;
}
#boardMount svg:focus { outline: none; }
#boardMount svg:focus-visible { outline: 2px solid var(--brass-2); outline-offset: -3px; }
.unit.is-sel .unit-halo { opacity: 1; }
.unit.is-mine .unit-body { stroke-width: 1.7; }
.unit.is-dislodged .unit-body { stroke: var(--bad); stroke-dasharray: 3 2; }

/* The order pad's marks, named beside the sentence that names the board's. Its own line
   and a size down, because it is a key rather than an instruction. */
.hint-marks {
  display: block; margin-top: 5px;
  font-size: 11.5px; color: var(--text-faint); letter-spacing: .01em;
}
.hint-marks b { color: var(--text-dim); font-weight: 600; }
/* The Record's own copy of that key, at the head of the pane rather than under the map —
   the hint box is showing the ordering hint while this pane is selected, so its legend is
   not on screen when these rows are. */
.record-key {
  margin: 0 0 8px; font-size: 11.5px; line-height: 1.5;
  color: var(--text-faint); letter-spacing: .01em;
}
.record-key b { color: var(--text-dim); font-weight: 600; }

.reach { fill: rgba(201,162,39,.19); stroke: var(--brass); stroke-width: 1.6; cursor: pointer; }
.reach:hover { fill: rgba(232,205,106,.34); }
/*
 * Three modes, three colours. They were declared and never styled.
 *
 * `showReach` has emitted `reach--move`, `reach--support` and `reach--convoy` since it was
 * written, and this file defined only `.reach` — so "a province you may move to", "a unit
 * you may support" and "an army you may carry" were painted identically, and which of the
 * three you were in was stated only in the hint box: 700px from the map on a laptop, and
 * a screen away from it on a phone. A modifier set that is declared, emitted and never
 * drawn is the clearest evidence there is that the distinction was meant to exist.
 *
 * Move keeps the gold the hint calls it by. Support takes the verdigris this file already
 * uses for a friendly hand — the ledger marks a human-held seat with it — and convoy a
 * sea blue, a convoy being the one order that happens on water.
 */
.reach--support { fill: rgba(82,160,154,.24); stroke: var(--verdigris); }
.reach--support:hover { fill: rgba(82,160,154,.42); }
.reach--convoy { fill: rgba(106,164,201,.24); stroke: #6aa4c9; }
.reach--convoy:hover { fill: rgba(106,164,201,.42); }

.arrow { fill: none; stroke-linecap: round; }
.arrow--draft { stroke-width: 3; stroke-dasharray: 7 5; opacity: .95; }
.arrow--move { stroke-width: 3.4; }
.arrow--fail { stroke-width: 2.6; stroke-dasharray: 6 4; opacity: .8; }
.arrow--support { stroke-width: 1.9; stroke-dasharray: 1.5 3.5; }
.arrow--convoy { stroke-width: 1.9; stroke-dasharray: 8 4 2 4; }
.arrow-reveal { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); animation: draw .55s var(--ease) forwards; }
@keyframes draw { to { stroke-dashoffset: 0 } }
.bounce-mark { stroke: var(--bad); stroke-width: 2.2; fill: none; }
.hold-ring { fill: none; stroke-width: 2; stroke-dasharray: 3 3; opacity: .85; }
/* A unit written off the board. Same ring, drawn tighter and at full weight, so "I am
   giving this up" does not look like "I am standing still" — the two are the only choices
   a dislodged unit has and they were rendering identically. */
.hold-ring--disband { stroke-width: 2.6; stroke-dasharray: 2 4; opacity: 1; }
/* A unit that is not on the board yet — a build the player has placed and not sealed.
   Outline only, so it reads as intent rather than as a piece already standing there. */
.unit--ghost { pointer-events: none; }
.unit-body--ghost {
  fill: none; stroke: var(--brass-2); stroke-width: 2; stroke-dasharray: 4 3; opacity: .95;
}
/* A retreat: the power's own arrow, broken, because it is a move it did not choose. */
.arrow--retreat { opacity: .95; }
/* The province a successful move left — see `drawResolved`. Solid, so it does not read as
   the dashed hold ring; small, so it does not read as the dislodged ring; and unfilled, so
   it is an annotation over the map rather than a disc sitting on it. It fades in with the
   arrow that leaves it rather than before it.
   Held below full strength: this is a note about where something was, and it must stay
   quieter than the arrow leaving it and than the counters it sits among. */
/* The halo carries the contrast; the coloured ring on top carries the identity. Drawn
   wider and darker so the mark has an edge against a pale land wash and against the sea
   alike — see the note in `drawResolved`. */
.depart-halo {
  fill: none; stroke: rgba(6,10,14,.72); stroke-width: 4.6;
}
.depart-ring { fill: none; stroke-width: 2.4; opacity: .95; }
.depart { animation: depart-in .35s var(--ease) both; transform-box: fill-box; transform-origin: center; }
@keyframes depart-in { from { opacity: 0; transform: scale(.4) } to { opacity: 1; transform: none } }

.grat { stroke: rgba(201,162,39,.08); stroke-width: .6; fill: none; }
.impassable { fill: #9d937c; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .arrow-reveal { stroke-dashoffset: 0 !important; }
}

/* ══════════════════════  Narrow screens  ══════════════════════ */

@media (max-width: 1320px) {
  :root { --rail: 232px; }
  /* The actions need ~377px; below this the status line was being squeezed to
     89px and running to five lines, or the buttons wrapped internally. Either
     way the console grew and took the height out of the map.
     Raised from 1180 because that figure was calibrated when the actions measured 377px
     and they now measure 449–529: the band just above the old threshold had become the
     worst layout in the product, and *widening* the window by a pixel improved it by 78px
     of console and 46px of map. The threshold has to follow what the row actually
     occupies, not what it occupied once. */
  .console { grid-template-columns: 1fr; gap: 8px; }
  .console-actions { justify-content: flex-end; }
  .gate-title { font-size: clamp(44px, 12vw, 80px); }
}

/* A stopped table gets the whole width for its explanation, at any size.
   This is the console's longest content by far — a sentence plus the wax seal that
   restarts the game — and sized by whatever the button row leaves it, it wrapped to nine
   lines in a 90px track with "Raise the ceiling" broken across three lines inside the
   seal. The remedy must never be sized by leftover width. */
.console.is-stalled { grid-template-columns: 1fr; }
.console.is-stalled .console-actions { justify-content: flex-end; }

/* On a laptop the masthead competes with the form for the first screen: at 1280x800 the
   title alone measured 186px and every one of the five arming controls sat below the
   fold. Scaled by height as well as width, so a short viewport spends its pixels on the
   controls.
   Declared here, after the base rules at 150-180 *and* after the width block above, for
   the third time this file has taught the same lesson: a media query adds no specificity,
   so a plain rule written later simply wins and the whole block is inert. Every selector
   it sets is set again above it, which is exactly why it has to come last. */
@media (max-height: 900px) {
  .gate { padding: 14px; }
  /* The scroller owns the padding now that the plate only carries the frame. */
  .gate-scroll { padding: 20px clamp(20px, 3vw, 40px) 24px; }
  .gate-title { font-size: clamp(40px, 5.4vh, 72px); }
  .gate-seal { transform: scale(.7); margin: 0 auto -4px; }
  .gate-eyebrow { margin-bottom: 2px; }
  /* The lede is the one thing here that is flavour rather than a control. On a laptop
     it was costing 96px of the screen the arming form has to fit into, so it stands
     down and the settings come up — the same trade the room's `.room-foot` makes. */
  .gate-lede { display: none; }
  .gate-card h2 { margin-bottom: 3px; }
  .gate-help { margin-bottom: 10px; }
  .power-chip { padding: 6px 10px; }
  .power-pick { margin-bottom: 10px; }
}

/* Four nested paddings — gate, plate, scroller, card — left 190px of a 360px viewport for
   content: 47% of a phone screen was frame, and the vertical cost of that is what put the
   key three screens down. The frame stands down where the screen is small.
   *After* the height block above, not before it. A phone matches both, a media query adds
   no specificity, and the later block simply wins — so written first, two of these four
   selectors were dead and the reduction never happened. Sixth time this file has taught
   the same lesson; the order is the whole fix. */
@media (max-width: 700px) {
  .gate { padding: 6px; }
  .gate-scroll { padding: 16px 12px; }
  .gate-card { padding: 14px 12px; }
  .gate-commit { padding: 10px 12px 0; }
}

/* Three columns stop earning their keep well before 980: at 1024 the centre is
   504px against 464px of rail, and the map letterboxes to 49% of the window. */
@media (max-width: 1100px) {
  body { overflow: auto; }
  .table {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    height: auto; min-height: 100dvh;
  }
  /* The board and your own orders come first. On a narrow screen everything
     else — the ledger, the record, the table code — is reference material you
     scroll to, not the thing you came to do. */
  .board-stage {
  order: 1; }
  .rail--right { order: 2; }
  .rail { overflow: visible; }
  /*
   * The rail's children become items of the page grid here, so the two a player needs at
   * a glance — which power they are, and how long they have — sit above the map, and the
   * ledger stays reference material below it.
   *
   * `order: 3` on the whole rail put all three below the board, and the clock strip is
   * where the running spend is drawn at this width (`.clock-spend` is shown from 1100 down
   * and `.ledger-spend` hidden). So between 701 and 1100 there was no money readout and no
   * deadline above the fold at all: measured at 768x1024, `#clockSpend` at document offset
   * 1314 of a 1584px page, and on a table of mine at 1893 of 2140 — the crest, the table
   * code, the countdown and the only statement of what the game has cost all in the last
   * quarter of the page. Which is precisely the condition this file's own comment calls
   * "a cap you find out about by the table stopping".
   *
   * The 700px block below already does exactly this for a phone. It was the right idea
   * keyed to the wrong width: the *layout* changes at 1100, so the promotion had to.
   */
  .rail--left { display: contents; }
  .rail--left .crest { order: 0; }
  .rail--left .clock { order: 0; }
  /* Everything else in the rail goes below the map. Left on the default `order: 0` these
     three came up with the crest and the clock and put 109px of reference material —
     the table code you hand out, and the Rules and Shortcuts buttons — between the
     player and the board, which pushed 37px of the map below the fold at 1100x800. Only
     the two things a player reads at a glance belong above it. */
  .rail--left .ledger { order: 4; }
  .rail--left .table-code { order: 5; }
  .rail--left .rail-nav { order: 6; }
  .crest-power { font-size: 17px; }
  .clock-phase { font-size: 20px; }
  /* Not hidden: this is now the only place the table's readiness is stated,
     and phone players in a room are its largest audience. */
  .clock-note { font-size: 11px; }

  /* Seven tall rows would push the board off the first screen, so the ledger
     becomes a compact grid of tiles. */
  .ledger-rows {
    overflow: visible;
    /*
     * 150px, not 104px. At 104px a 360px screen fits three tiles and leaves the
     * name 50px, which was enough while the badge read "machine" and is not
     * enough now it reads a diplomat's surname: measured across all 42 of them,
     * twelve overflowed a 50px column, the worst by 12px, and an overflowing
     * inline-block here paints through the centre count in the next tile.
     *
     * At 150px the same screen fits two tiles and the name gets about 110px, which
     * every name in the cast clears. The cost is one more tile-row on the narrowest
     * phones; `auto-fill` still gives three tiles as soon as there is room for
     * them, so nothing wider than about 470px changes at all. A legible name is
     * worth more than a row of height, since the name is the whole point of the
     * seat badge.
     */
    display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 4px;
  }
  /* Three columns, not four: the ⌖ that marks a row as pressable only ever
     appears on hover or keyboard focus, neither of which a phone has, so it
     was eighteen invisible pixels taken off a 104px tile — and the whole tile
     is the button here anyway. */
  .ledger-find { display: none; }
  .ledger-row {
    grid-template-columns: 3px minmax(0, 1fr) auto; gap: 6px;
    padding: 6px 8px; border: 1px solid var(--rule);
  }
  /* The ensign goes back to being a colour rule here. A 104px tile already
     carries the power's name in full, so a flag beside it buys recognition
     that is not needed and spends 16px that the tally and the phase code do
     need. The colour, on the other hand, is the only thing tying the tile to
     the map — and the bar that used to carry it is hidden at this width. */
  .ledger-swatch {
    width: 3px; height: 18px; background: var(--power); box-shadow: none;
  }
  .ledger-swatch > .ensign { display: none; }
  .ledger-bar { display: none; }
  /* The seat badge goes under the name rather than beside it. `min-width: 0`
     on the row's children lets the name track shrink — which is what stopped a
     ledger row scrolling the whole page sideways — but a track that can shrink
     does not clip what is in it, and "PLAYER" is 65px of unbreakable
     inline-block in a 46px column. It came out drawn straight through the
     centre count in the next column: "PLA4ER", on six of seven rows. */
  .ledger-name { display: block; }
  /* A caption here, not a chip. Boxed, the badge measured 59px of a 122px tile
     — half the width of the whole row, and heavier than the power's own name,
     which is the thing the tile is for. Its colour already carries the meaning
     (brass for you, verdigris for a player, faint for the machine) and the
     border was only ever there to hold it apart from the name it sat beside;
     under the name it has a line of its own and needs nothing.
     No font-size: the floor further down this block sets it, and the 9.5px that
     used to be here was overridden sixteen rules later and never once drew. */
  /* `.ledger-seat.is-persona` carries two classes and so outranked the `display: block`
     here, while this rule's `margin: 1px 0 0` still won on the margin — leaving the
     diplomat's badge inline with *no* left gap, which rendered "RussiaLubensky" on every
     mobile and tablet width, and left machine tiles 8-14px shorter than the human tile
     beside them. Named explicitly so the specificities match. */
  .ledger-seat,
  .ledger-seat.is-persona {
    display: block; width: max-content; max-width: 100%;
    margin: 1px 0 0; letter-spacing: .1em; padding: 0; border: 0; border-radius: 0;
    vertical-align: baseline;
  }
  .ledger-ready { display: block; margin-top: 1px; }
  .ledger-tally { font-size: 13px; }
  /* Not shrunk below the size these use on a wide screen. At 9.5px the spend readout —
     the feature's whole cost-accountability surface — was the smallest text in the
     product across a band that includes common laptop and tablet widths, and this file's
     own standard is that "9px was not a size, it was a texture". */
  .ledger-foot, .rail-heading { font-size: 10.5px; }
  .rail-nav { flex: 1 1 100%; }

  /* The page scrolls here, so the board is capped against the viewport rather
     than derived from its width — otherwise a 1024px window gets a 914px-tall
     map and the seal sits below the fold. */
  /*
   * And so no size container: `container-type: size` contains the block axis, which means
   * the stage contributes *no* intrinsic height. In the three-column layout that costs
   * nothing, because its grid track is `1fr` and definite either way. Here the track is
   * `auto`, so it resolved to 0px and the stage stopped occupying the page at all.
   *
   * Measured at 360x780 before this: grid rows "34.5px 65.5px 0px 570.891px 270.75px 44px",
   * the stage 0px tall around a frame drawn from y124 to y439.8, with the tabs at y132 and
   * the order pad at y254.5 — underneath the map. `elementFromPoint` at the Record tab
   * returned `zoomFit`, and at the first order line it returned the board's hit layer.
   * Every phone and tablet width, including 768x1024: the one screen the room mode exists
   * for could not be used at all.
   *
   * Nothing in this branch needs the container — the frame below is sized from `dvh` and
   * `vw`, not from `cq` units — so the fix is to stop declaring one. `.room-board`, which
   * has the same construction, is given the same treatment in its own stacked branch. */
  .board-stage { align-items: stretch; container-type: normal; }
  .board-frame {
    flex: none; min-height: 0; aspect-ratio: 1403 / 1288; width: auto;
    max-width: 100%; margin: 0 auto;
    height: min(62dvh, calc((100vw - 2 * var(--gap)) / var(--map-ratio)));
  }
  /* Kept below this width: the ledger's colour swatch is 3px there, so this is
     the only key to which power is which. */
  .pane { max-height: none; }
  /* The Move/Support/Convoy chips live in the hint box, which was the last
     block on the page — roughly 800px below the unit you just tapped, and on
     touch the only way to reach a support order at all. */
  .hintbox { order: -1; }
  /* While an order is being composed, the hint box carries the only controls
     that reach Support and Convoy — and on a phone it was painted underneath
     the console bar, which is fixed to the bottom and opaque. Measured at
     375x667: the pane begins at y 539, the bar's top is at 488, and the chips
     land at 591-635 with `elementFromPoint` at each of their centres returning
     the face of the Seal button. Every viewport under about 800 tall is the
     same, and the ones that clear it do so by five pixels until the console's
     status line wraps to a third row.
     `order: -1` above was an earlier go at this: it lifts the box above the
     order pad, but only within the pane, and the pane is below the board.
     So while composing it floats directly over the bar it was hiding behind.
     Only while composing — the rest of the time it is one line of instruction
     and can stay in the flow rather than standing permanently on the map. */
  body.is-composing .hintbox {
    position: fixed; z-index: 40;
    left: 12px; right: 12px;
    bottom: calc(var(--console-h, 104px) + 8px + env(safe-area-inset-bottom, 0px));
    background: linear-gradient(180deg, #16232f, #0c151c);
    border: 1px solid var(--rule-2); border-left: 2px solid var(--brass);
    box-shadow: 0 -10px 30px -12px #000;
  }
  /* No nested scroller inside a page that already scrolls, and the paper is
     capped rather than the list inside it.
     `.press-log` belongs in this list and was missing from it: on a phone the whole
     correspondence was confined to a 212px window inside a 1682px scrolling page. */
  /* The measure belongs to the column, not to one card in it. Capping the pad alone left
     it 417px wide and centred, with `margin: 0 289.5px`, directly above a 996px typebox,
     a 996px hint box and a 996px console at 1024x1300 — one narrow card stranded in
     580px of empty space, which reads as a layout fault rather than a reading measure.
     The three cards a player reads and writes in share it; the console is a control bar
     and keeps the full width, because its buttons are targets rather than prose. */
  /* The two cards a player writes in share one measure and one pair of edges. Capping the
     pad alone left it 417px wide and centred, with `margin: 0 289.5px`, directly above a
     996px typebox and a 996px console at 1024x1300 — one narrow card stranded in 580px of
     empty space, which reads as a layout fault rather than a reading measure.
     `rem`, not `ch`: `ch` resolves against each element's own font size, so the same
     declaration on three cards produced three different widths — 416.9, 416.9 and 342.5 —
     and three different left edges, which is worse than the single odd one it replaced.
     The hint box and the console keep the full width: one is a band of guidance and the
     other a row of targets, and neither is something you write into. */
  .flimsy, .typebox {
    max-width: 26rem; margin-left: auto; margin-right: auto; width: 100%;
  }
  .flimsy { max-height: none; }
  .press-log { overflow: visible; flex: none; }
  .press-log.is-more { mask-image: none; -webkit-mask-image: none; }
  /*
   * The composer stays where the DOM puts it — after the log — so reading order and
   * visual order agree.
   *
   * `order: -1` lifted it above the log here, because on a page-scrolling layout the Send
   * button and the message box were otherwise below the fold on every handset. That is a
   * real problem and this was the wrong instrument: it left a screen reader hearing the
   * archive before the form while a sighted user saw the form first, which is precisely
   * the divergence WCAG 1.3.2 is about, and it put the letter that had just arrived some
   * 2,700px further down the page than the composer nobody had asked for yet.
   *
   * `selectTab` now lands the pane on the newest letter instead, with the composer
   * immediately beneath it — so the thing you came to read is on screen, the thing you
   * might write next is one short scroll on, and neither order has to lie about the
   * other.
   */
  .console { grid-template-columns: 1fr; gap: 8px; }
  .console-actions { justify-content: flex-end; }
  .toast-stack { bottom: 12px; width: calc(100% - 24px); }
  
  .toast { max-width: 100%; }
}

/* ── Phones ─────────────────────────────────────────────────────────────
   A player in the room arrived by following their own link. They need the
   map, their orders and the seal; everything else is a tap away. */
@media (max-width: 700px), (max-height: 480px) and (max-width: 950px) {
  :root { --gap: 8px; }
  .table { padding: 8px; gap: 8px; }

  /* Arrived by link — the code is not how this player got here. The sheet's copy is
     exempt: that one appears at the moment the code is meant to be handed out, which
     is exactly when a phone needs it most. */
  .table-code:not(.table-code--sheet) { display: none; }
  /* `#openKeys` is not hidden here either — `wireChrome` hides it from `hasKeys()`, which
     asks the pointer. Hidden by width as well, a narrow desktop window lost both the key
     caps and the shortcuts sheet while the shortcuts themselves still worked. */

  /* The rail's children become grid items in their own right, so the one thing
     a player needs at a glance — which power they are, and how long they have —
     can sit above the map while the ledger stays below it. */
  .rail--left { display: contents; }
  .crest { order: 0; padding: 0; border: 0; }
  .crest { grid-template-columns: 30px 1fr; }
  .crest-mark { width: 30px; height: 20px; }
  .crest-power { font-size: 15px; }
  .crest-role { font-size: 9.5px; }
  /* Its own row rather than a ragged wrap between the season and the clock. */
  /* Season, phase and clock on one line — "Winter 1901 · ADJUSTMENTS" is the
     longest it gets — and the readiness sentence on its own beneath, rather
     than wrapping raggedly between them. */
  .clock {
    order: 0; display: flex; align-items: baseline; flex-wrap: wrap;
    gap: 3px 8px; padding: 7px 10px;
  }
  .clock-phase { font-size: 14px; margin: 0; white-space: nowrap; }
  .clock-kind { margin: 0; font-size: 9px; white-space: nowrap; }
  .clock-meter { flex: 1 1 20px; min-width: 16px; }
  .clock-deadline { order: 1; margin: 0 0 0 auto; font-size: 12px; white-space: nowrap; }
  .clock-deadline-tail { display: none; }
  /* Ordered last so it takes the second row on its own; the countdown stays up
     on the first line beside the season. */
  .clock-note { order: 2; flex: 1 0 100%; margin: 0; font-size: 11px; }
  /* Its own row too, and not a flex item whose width tracks its own text. The clock is a
     wrapping row here, and the spend figure grows from 58px to 94px as the total climbs —
     which pushed the countdown onto a second line at 375x667 and made the strip flip
     between one row and two as the clock crossed from 10:15 to 9:29, i.e. for the first
     third of every fifteen-minute turn. The most urgent number on a phone should not be
     laid out by the least urgent one. Tabular figures so it stops jittering as it
     climbs. */
  .clock-spend {
    order: 3; flex: 1 0 100%; margin: 2px 0 0;
    font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .ledger { order: 4; }
  .rail-nav { order: 5; }

  /* Keeps the map's own shape, so a phone gets no dead sea down its sides,
     and caps it against the screen rather than deriving it from the width. */
  .board-frame {
    flex: none; aspect-ratio: 1403 / 1288; width: auto; max-width: 100%; margin: 0 auto;
    height: min(46dvh, calc((100vw - 2 * var(--gap)) / var(--map-ratio))); min-height: 240px;
  }
  /* Big enough to hit with a thumb. Everything a finger has to land on is at
     least 44px here — the room mode's whole premise is thumbs on a small
     screen, and an inventory of this layout found 22 controls under that. */
  .tool { width: 44px; height: 44px; font-size: 17px; }
  /* One row in the corner. Zoom in and out are dropped here: pinch is the
     native gesture on a touch screen and is verified working, `⤢` still resets
     the view, and five stacked buttons covered an eighth of a 321px board —
     five provinces, two of them supply centres, once you zoomed in at all. */
  .board-tools { flex-direction: row; flex-wrap: nowrap; justify-content: flex-end; }

  /* `position: sticky` had nothing to stick within: the console is the last
     child of a stage whose height is exactly frame + gap + console, so its
     travel was zero and it scrolled away like a static element. */
  /* Toasts carry the game's refusals — "Amiens is out of reach" — so they must
     not land on the control being corrected. */
  .toast-stack { bottom: calc(var(--console-h, 104px) + 12px + env(safe-area-inset-bottom, 0px)); }
  .console {
    position: fixed; left: 8px; right: 8px; z-index: 5; padding: 8px;
    /* viewport-fit=cover is set, so the seal would otherwise sit under the
       home indicator. */
    bottom: env(safe-area-inset-bottom, 0px);
    /* Opaque, not nearly-opaque. Content scrolls under a fixed bottom bar and
       that is ordinary — but RULES and LEAVE were showing *through* it at
       reduced contrast, and a control you can read and cannot tap is worse than
       one you cannot see. The page reserves room below for all of it. */
    background: linear-gradient(180deg, #1b2b38, #070c11);
  }
  .table { padding-bottom: calc(var(--console-h, 104px) + 12px + env(safe-area-inset-bottom, 0px)); }
  /* How much of the viewport the fixed console owns. Anything scrolled into
     view stops above it rather than under it — the padding above only lets the
     end of the document clear the bar, which is not the same thing. */
  :root { scroll-padding-bottom: calc(var(--console-h, 104px) + 16px + env(safe-area-inset-bottom, 0px)); }
  .console-status { font-size: 12.5px; }
  .console-actions { flex-wrap: wrap; }
  .console-actions .ghost-button { min-height: 44px; flex: 1 1 auto; font-size: 12px; }

  /* The gate. Every other phone surface in this file is held to 44px and the arming
     flow was not in any of the lists — nine controls measured 37.5-41px, including the
     four selects that choose a spending tier and the field the API key is typed into.
     And 16px on the text inputs because iOS zooms anything smaller: the page was zooming
     mid-entry of a credential, the hazard `.typebox textarea` is already protected from. */
  .gate-field select,
  .gate-field input,
  .gate-forms .ghost-button { min-height: 44px; }
  .gate-field select { font-size: 15px; }
  #mindsKey, #joinCode { font-size: 16px; }
  /* The seal takes a full row of its own rather than being cut in half by the
     edge of the screen. */
  /* A full row of its own, so it cannot be cut off by the edge of a 360px
     screen with no way to scroll to it. */
  .seal-button--console { flex: 1 0 100%; min-height: 46px; }

  .flimsy-line { min-height: 44px; padding: 10px; font-size: 12.5px; }
  .tab { min-height: 44px; font-size: 12px; }
  .typebox textarea { font-size: 16px; }   /* iOS zooms anything smaller */
  .rail-nav .ghost-button { min-height: 44px; font-size: 12px; }

  /* The rest of what a thumb has to hit. The press pills and the mode chips
     were 26px and 31px — small enough that a miss is likelier than a hit, and
     on the pad a miss lands on Clear. They wrap rather than shrink. */
  .press-pill { min-height: 44px; font-size: 12px; }
  #pressKind, .press-form select { min-height: 44px; font-size: 12px; }
  /* `.press-alert .ghost-button` is here because the hand-enumerated lists missed it:
     the one control in the pane that raises a browser permission prompt measured 34px
     while every control beside it got 44. A `.ghost-button--tiny` nested in a
     `.press-alert` matched none of the selectors above. */
  .press-form button, .hint-actions .ghost-button,
  .press-alert .ghost-button { min-height: 44px; font-size: 12px; }
  .press-alert small { font-size: 11.5px; }
  /* The key caps are NOT hidden here. They are hidden by pointer, in the `pointer: coarse`
     block at the end of this file, because a keyboard is a property of the device and not
     of the window. This rule used to live here as well as there, and leaving both meant
     the width-keyed one still won: measured at 360x780 with a mouse, all four `kbd` were
     `display:none` and the shortcuts they name still worked. Adding the right rule without
     removing the wrong one is the exact slip this file keeps recording. */
  .ledger-row { min-height: 44px; }
  /* The rest of them: Apply commits a typed order, and a dialog cannot be
     dismissed or answered with a 27px target. */
  .typebox-foot button { min-height: 44px; font-size: 12px; }
  .sheet-actions button { min-height: 44px; font-size: 12px; }
  .sheet-close { min-width: 44px; min-height: 44px; }

  /* Nothing a player has to read sits below 12px. The all-caps kickers stay a
     little smaller — caps carry at a size lower case does not — but 9px was
     not a size, it was a texture. */
  .clock-note { font-size: 12px; }
  .clock-kind { font-size: 11.5px; }
  .legend-item { font-size: 11.5px; }
  .hintbox { font-size: 12.5px; }
  /* The rest of what a phone has to read. The PLAYER/MACHINE chips and the
     +2 / 5u deltas are not decoration — they are how a player learns which
     rivals are human and who has gained ground this year. */
  /* Nothing a player reads between turns sits under 12px. The seated count, the
     phase code and each power's surplus are state, not decoration. */
  .flimsy-rule { font-size: 12px; }
  .flimsy-code { font-size: 12px; }
  .crest-role { font-size: 12px; }
  .ledger-foot { font-size: 12px; }
  .ledger-seat { font-size: 12px; }
  .ledger-ready { font-size: 12px; }
  /* `.ledger-units` was named here too and matched nothing — no markup, no JS, zero
     elements at any viewport. A dead rule beside live ones reads as load-bearing. */
  .ledger-delta { font-size: 12px; }
  .ledger-find { font-size: 12px; }
  .clock-kind { font-size: 12px; }
  .typebox-label { font-size: 12px; }
  /* The press log's own metadata — who a note is from, and to. In a game about
     private diplomacy that is the most load-bearing word on the pane. */
  /* The sender's name is never smaller than the letter it heads — it was the smallest
     thing on the card, which made the log scan as anonymous blocks of text. */
  .wire-from { font-size: 12.5px; }
  .wire-when { font-size: 11.5px; }
  /* The line tying a diplomat to the power you addressed. It was the only part of the
     card left under this block's own floor, while every sibling was raised. */
  .wire-power { font-size: 11.5px; }
  .wire-to { font-size: 11.5px; }
  .wire-machine { font-size: 11.5px; }
  /* The composer, matching its twin in the order box: #typeboxNote is 12.5px here and
     #orderInput 16px. #pressNote was 11px sentence-case status copy — below this block's
     own stated floor — and #pressBody 11.5px prose input, which iOS zooms on focus. */
  #pressNote { font-size: 12.5px; }
  .press-form textarea { font-size: 16px; }
  /* The fixed console was clipping the ledger's last line by a few pixels. */
  .table { padding-bottom: calc(var(--console-h, 104px) + 22px + env(safe-area-inset-bottom, 0px)); }
  /*
   * And while the hint box is floating over the pad, the page grows by what it covers.
   *
   * Fixed, it sits on top of the order rows: measured at 360x800 the box spanned y545-650
   * while the pad's first row ran 592-636 and its second 636-680, so `elementFromPoint` at
   * the first row returned the hint's own button. The record of what you have ordered so
   * far disappeared during the two-step support — the only multi-step order in the game,
   * and the one where you most need to see what is already written. Reserving the height
   * lets those rows be scrolled out from under it; `--hint-h` is measured from the box
   * itself, so a two-line hint reserves two lines.
   */
  body.is-composing .table {
    padding-bottom: calc(var(--console-h, 104px) + var(--hint-h, 0px) + 22px
      + env(safe-area-inset-bottom, 0px));
  }
  /* The tab strip is the navigation for everything below it, and on the shortest handset
     in the device table it comes to rest with its bottom edge under the fixed console —
     measured 5.1px at 375x667 with the page at the top. The three tabs stay hittable at
     that depth (probed at 15%, 50% and 85% of each, all returning the tab), so this
     reserves the bar's height for anything that scrolls the strip into view rather than
     restructuring a layout that is working. */
  .tabs { scroll-margin-bottom: calc(var(--console-h, 104px) + 12px); }
  /* And the order pad, for exactly the same reason and by the same means. `keepClear`
     scrolls it with `block: 'end'`, which aligns its bottom with the *viewport's* bottom
     — and the bottom 142px of this viewport is the fixed console, so the rows it was
     bringing into view landed behind the bar. The tab strip's rule two lines up has
     solved this since the round it was written; the pad simply never got it. */
  .flimsy-lines { scroll-margin-bottom: calc(var(--console-h, 104px) + 14px); }
  /* While the board is showing the turn, the pad has one sentence to say and was taking a
     screen and a half to say it — measured 1,253px of empty parchment holding "Held while
     the board shows it." on an 844px viewport. It shrinks to the line it is carrying, so
     the page below the board is not a wall of nothing. */
  body.is-resolving .flimsy { flex: 0 0 auto; }
  /* And the rows themselves take no room while they are invisible. `visibility: hidden`
     keeps a hidden element's box, so the pad stayed 1,394px tall — a screen and a half of
     nothing — for the 14 seconds of the sequence, with its one explanatory line stranded
     at the top of it. Collapsing the list is what makes the holding line legible without
     scrolling, and it is the same rule that hides it. */
  body.is-resolving .flimsy-lines { display: none; }
}

/* A finger is a finger at any width. The layout above is keyed to 700px, which
   is right for the *layout* — a tablet in portrait has room for the desktop
   arrangement — but it left an iPad's controls at 28–34px because it is 768
   wide. Target sizes belong to the pointer, not to the viewport. */
@media (pointer: coarse) {
  .tool { width: 44px; height: 44px; }
  .tab { min-height: 44px; font-size: 12px; }
  .ghost-button--tiny { min-height: 44px; font-size: 12px; }
  .console-actions .ghost-button { min-height: 44px; }
  .rail-nav .ghost-button { min-height: 44px; }
  .typebox-foot button, .press-form button, .hint-actions .ghost-button,
  .press-alert .ghost-button { min-height: 44px; }
  .sheet-actions button { min-height: 44px; }
  .sheet-close { min-width: 44px; min-height: 44px; }
  .press-pill { min-height: 44px; font-size: 12px; }
  .ledger-row { min-height: 44px; }
  .flimsy-line { min-height: 40px; }
  /* The arming flow, which this hand-kept list had missed entirely: on a portrait tablet
     every gate select measured 37.5px, the API-key field 38px and the press composer's
     selects 31px, because they were only ever raised inside the 700px-keyed block. The
     key and code inputs also need 16px here, or iOS zooms the page mid-credential —
     the same reason `.typebox textarea` was raised. */
  .gate-field select,
  .gate-field input,
  /* `#gateArrival`'s two buttons matched none of the selectors in this hand-kept list —
     not `.gate-forms .ghost-button`, which they sit outside of, nor `.ghost-button--tiny`,
     which they are not — so on a real handset they stayed at 39px while everything around
     them got 44. They are the only two exits on the screen a player reaches when their
     seat link is refused. Third time this enumeration has been found short. */
  #gateArrival .ghost-button,
  .gate-forms .ghost-button { min-height: 44px; }
  #mindsKey, #joinCode { font-size: 16px; }
  #pressKind, .press-form select { min-height: 44px; font-size: 12px; }
  .press-form textarea { font-size: 16px; }
  .seal-button--console .seal-button-face { min-height: 44px; }

  /*
   * And the reading floor, for the same reason as the target sizes above it.
   *
   * "Nothing a player has to read sits below 12px" is stated in the 700px block and was
   * only ever enforced there, so a portrait tablet — which matches neither arm of that
   * condition, being 768 wide and 1024 tall — kept the desktop sizes. Measured at
   * 768x1024: the diplomat's name in the ledger 10px, the country a letter comes from
   * 10px, its turn and its subject 11px. The smallest type in the product was the type
   * saying who is talking to you, on the device most likely to be held at arm's length.
   *
   * The same list as the 700px block, and it has to stay the same list — this is the
   * third hand-kept enumeration in this file to fall behind its twin. Contrast was
   * already fine on all of these; this is size alone.
   */
  .clock-note, .clock-kind, .flimsy-rule, .flimsy-code, .crest-role,
  .ledger-foot, .ledger-seat, .ledger-ready, .ledger-delta, .ledger-find,
  .typebox-label { font-size: 12px; }
  .legend-item, .wire-when, .wire-power, .wire-to, .wire-machine,
  .press-alert small { font-size: 11.5px; }
  .wire-mark { font-size: 11px; }
  .hintbox, .wire-from, #pressNote { font-size: 12.5px; }
}

/* ══════════════════════  The shared board  ══════════════════════
   Read from across a room: everything is larger than it would be on a
   personal screen, and nothing here is secret. */

.room {
  display: grid;
  /* `minmax(0, 1fr)`, not `1fr` — the guard `.table` and `.sheet` both use, and the one
     grid in this file that had not got it, despite being the one whose track holds the
     board. A `1fr` track's automatic minimum is its content's intrinsic width, and an
     aspect-ratio board on a tall viewport is wide: measured 16.6px of page overflow at
     1280x1000 and 194.9px at 1024x1300, which clipped every "New link" button to
     "NEW LIN" and pushed all seven join links and Begin off a `100dvh` layout whose only
     recovery was an undiscoverable horizontal scroll. */
  grid-template-columns: minmax(0, 1fr) clamp(300px, 26vw, 420px);
  grid-template-rows: auto 1fr;
  gap: 0 clamp(16px, 2vw, 32px);
  height: 100dvh; padding: clamp(14px, 2vw, 28px);
  background: var(--ink);
}

.room-head {
  grid-column: 1 / -1;
  display: flex; align-items: baseline; gap: clamp(16px, 3vw, 48px);
  padding-bottom: clamp(10px, 1.4vw, 18px); margin-bottom: clamp(8px, 1.2vw, 16px);
  border-bottom: 1px solid var(--rule);
}
/*
 * The season and the clock, reduced — because they were four times the size of the thing
 * this screen exists to hand out.
 *
 * Measured at 1440x900: season 51.84px, clock 46.08px, and the join URL each player has
 * to read across the table and type into a phone at **13px**, with the diplomat's name at
 * 11.5 and the "New link" control at 11. The screen's own job was its smallest content
 * and its label was its largest. Contrast was fine everywhere (6.04:1 up), so this is
 * size alone.
 *
 * Still the head of the page — a room does need to see the season from the back of it —
 * but a heading, not the subject.
 */
.room-phase {
  margin: 0; font-family: var(--display); font-size: clamp(28px, 2.9vw, 44px);
  line-height: 1; letter-spacing: -.01em;
}
/* Says the board is showing a turn again rather than the one in progress. Amber, the
   colour this product uses for a state the viewer should notice but need not fix. */
.room-replay-mark {
  margin: 4px 0 0; font-size: 13px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--warn-soft);
}
.room-kind {
  margin: 4px 0 0; font-size: clamp(10px, 1vw, 14px); letter-spacing: .26em;
  text-transform: uppercase; color: var(--brass-2);
}
.room-clock {
  margin: 0 0 0 auto; font-family: var(--mono);
  font-size: clamp(24px, 2.6vw, 39px); color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.room-clock.is-urgent { color: var(--bad); }
/* Housekeeping, not the game — so quieter and smaller than the clock beside it, and brass
   at the one moment it stops being housekeeping. The same register the rail's spend line
   uses on a phone. */
.room-spend {
  margin: 0; font-family: var(--mono);
  font-size: clamp(12px, 1.1vw, 16px); color: var(--text-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.room-spend.is-spent { color: var(--brass); }
/* Grows into the header's free space instead of being pinned near its minimum. Capped at
   340px it took 316.8px of a 1384px row while 753.4px of that row sat empty, and wrapped
   the room's one status sentence to five lines — 165px of head height, which is what
   pushed the seat list below its own fold. */
.room-ready { flex: 1 1 clamp(180px, 22vw, 340px); min-width: 0; }
.room-meter {
  height: 6px; border-radius: 3px; overflow: hidden;
  background: rgba(255,255,255,.07);
  position: relative;
}
.room-meter-fill {
  height: 100%; width: 0; background: var(--brass);
  transition: width .5s cubic-bezier(.2,.7,.2,1);
}
/*
 * The share of the bar the room can actually do something about.
 *
 * One flat fill counted all seven seats alike, so on a table with two people and five
 * diplomats it stood at 14.3% with one of the two humans sealed — a bar a room reads as
 * progress, which nothing anybody in that room does can push past 2/7. The denominator
 * was not wrong; the bar just could not say which part of it was theirs. The people's
 * share sits over the total in the brighter token, and the diplomats' share is the
 * quieter remainder behind it.
 */
.room-meter-people {
  position: absolute; inset: 0 auto 0 0; width: 0;
  background: var(--brass-2);
  transition: width .5s cubic-bezier(.2,.7,.2,1);
}
.room-meter-fill { background: var(--brass-dim); }
/* The only sentence that tells a room what the table is waiting on, who is writing,
   that the ceiling is spent, or that a power fell into civil disorder. It was capped
   at 15px in --text-faint while the heading beside it scaled to 52px, so on the one
   screen seven people look up at, the status line was the smallest and faintest text
   in the view. Scales with the same viewport basis as its siblings; --text-faint is
   left to .room-foot, which nobody has to read from across a room. */
.room-note {
  margin: 8px 0 0; font-size: clamp(14px, 1.7vw, 26px); color: var(--text-dim);
  line-height: 1.35;
  /* Two lines are kept whatever it currently says. Unreserved, the result summary — which
     runs to three lines when a power falls into civil disorder — grew the head 37px and
     took the same 37px out of the board canvas beneath it, then gave it back six seconds
     later: the map an entire table is watching resized twice every turn, and worst in the
     state produced by a bot failure. */
  min-height: 2.7em;
}
/* The second fact, when there is one — quieter than the sentence it follows, because
   the room can act on the first and only watch the second. */
.room-note-aside { color: var(--text-faint); }

/* `align-items: center` for the same reason `.board-stage` has it, and with the same
   consequence when it is missing: stretch makes both of the frame's axes definite, which
   makes `aspect-ratio` inert, and the frame then draws a border around empty sea — 269px
   of it top and bottom at 1024x1300, 47% of the largest element on the screen a whole
   table is looking at. The table's stage was given this and the room's was not. */
.room-board {
  min-height: 0; display: flex; flex-direction: column; gap: 6px; align-items: center;
  /* A size container, so the frame below can take the smaller of the two fits. */
  container-type: size;
}
/*
 * The frame takes whichever of its two possible sizes is smaller — the one the width
 * allows, or the one the height allows — instead of being stretched to one and clamped on
 * the other.
 *
 * `flex: 1` made the height definite and `max-width: 100%` then clamped the width, which
 * leaves `aspect-ratio` nothing to do: at 1024x1300 the frame was 662.6x1147.4 around a
 * map drawn 662.6x608.3, so 47% of the largest element on the screen a whole table looks
 * at was bordered empty sea. `align-items: center` alone does not fix it — the cross axis
 * was never the problem.
 *
 * `100cqh` is `.room-board`'s own height, so the height-driven width is exact. The legend
 * is the only other child and is subtracted with the gap.
 */
.room-board { justify-content: center; }
/* `margin: auto` on the frame absorbed every spare pixel of the column, which pushed the
   legend to the bottom edge — 275.6px from the map it is the key to, reading as part of
   the button row instead. Centring the column keeps the two together and splits the
   remainder above and below the pair. */
.room-board .board-frame {
  flex: none; min-height: 0; margin: 0 auto;
  aspect-ratio: 1403 / 1288;
  /* The legend's real box, measured — with a fallback that must never be removed.
     `--room-legend-h` is set from `#roomLegend` once the room is on screen. Written
     without the fallback it collapsed this frame to 2x2px and the shared board showed no
     map at all: the variable is unset until the room renders, an unset var makes the
     whole `calc()` invalid at computed-value time, and `width` then falls back to `auto`.
     A measured value is an improvement on a guess; a measured value with no guess behind
     it is a single point of failure on the one screen that exists to show the board. */
  width: min(100cqw, calc((100cqh - var(--room-legend-h, 24px) - 6px) * var(--map-ratio)));
  height: auto;
}

.room-side { min-height: 0; display: flex; flex-direction: column; }
.room-hint {
  margin: 2px 0 12px; font-size: clamp(10.5px, .95vw, 13px);
  line-height: 1.55; color: var(--text-faint);
}
.room-seats { list-style: none; margin: 0; padding: 0; flex: 1; overflow-y: auto; }
.room-seat {
  display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: start;
  padding: 6px 0; border-bottom: 1px solid var(--rule);
}
.room-seat.is-taken .room-seat-name { color: var(--text-dim); }
.room-seat.is-out { opacity: .4; }
.room-seat-flag { width: 22px; height: 15px; margin-top: 3px; }
/* Name over link, always — not side by side and not wrapping between the two.
   The panel is 374px wide and a join link is 254 of them, so whether the two
   fit on one line came down to how long the power's name is: "Italy" made it,
   "Austria-Hungary" did not, and the list came out with rows of two different
   heights carrying identical content. Stacking every row is the only shape
   that is the same for all seven.
   It was tried before and abandoned because it ran three seats off the bottom
   of a 900px board. That was the row height, not the stacking: at 87px seven
   rows need 609 and the list has 495. Everything below is tuned to bring a row
   in at about 60, which fits all seven with room over. */
.room-seat-body { min-width: 0; }
.room-seat-name {
  margin: 0; display: flex; justify-content: space-between; gap: 8px;
  font-size: clamp(13px, 1.25vw, 17px); line-height: 1.25;
}
.room-seat-count { color: var(--text-faint); font-variant-numeric: tabular-nums; }
/* The screen's actual content: a URL to be read from across a room and typed into a
   phone. Raised with the header reduced to pay for it — see the note on `.room-phase`. */
/*
 * The link is the whole mechanic of this mode and it was set at the panel scale of a
 * screen you sit in front of.
 *
 * Measured at 1280x800: 12.16px, against a 37.12px phase title on the same screen — a 3.2x
 * ratio, on the one element seven people have to read off a board across a room and type
 * into a phone, character by character, from a ten-character random string. The seat
 * badges beside it were raised off 9px for exactly this reason and this was left behind.
 * Selectable too, so a laptop at the table can copy it rather than transcribe it.
 */
.room-seat-link {
  margin: 0; font-family: var(--mono);
  font-size: clamp(11px, 1.02vw, 14px); color: var(--brass-2);
  overflow-wrap: anywhere; line-height: 1.4;
  user-select: all; cursor: text;
}
/* The host: identical on all seven rows, and a phone finishes it from history. Quiet, and
   on its own line so the code below it never has to share the width. */
.room-seat-host { display: block; opacity: .6; font-size: .82em; line-height: 1.15; }
/* The code: the ten characters that differ, that get typed, and that a slip in cannot be
   recovered from. This is the size the room reads, and it is only these characters, so
   the row still fits on one line — raising the whole URL wrapped every row and cut the
   seventh seat's link off the bottom of the panel. */
.room-seat-code { display: block; font-size: 1.45em; line-height: 1.2; letter-spacing: .02em; color: var(--brass); }
/*
 * The three states this link can be replaced by, sharing one treatment.
 *
 * `.is-seated` and `.is-gone` each carried the UI face, the tracking, the uppercase and
 * the 12px floor — with the same paragraph of comment copied into both — while `.is-quiet`
 * set only a colour and inherited the base's 10px mono *link* face. So the one state whose
 * copy exists to say "this is not a link you can use" was the one rendered as a link, two
 * pixels under the floor its siblings were explicitly given. Stated once; the variants
 * carry only what differs.
 *
 * The floor was raised off 9px because this is what distinguishes a human seat from a
 * diplomat's on a screen read from across a room — below that, in this file's words, it
 * was "not a size, it was a texture".
 */
.room-seat-link:is(.is-seated, .is-gone, .is-quiet) {
  font-family: var(--ui); letter-spacing: .1em; text-transform: uppercase;
  font-size: clamp(12px, .95vw, 14px);
  /* A status is not a link: nothing here is worth selecting. */
  user-select: auto; cursor: default;
}
.room-seat-link.is-quiet { color: var(--text-faint); font-style: italic; }

/*
 * The diplomat holding an unclaimed seat, on the shared screen.
 *
 * Italic and quieter than the power's own name: the country is what the room reads first
 * and the person is who they will be negotiating with.
 *
 * On its own line rather than competing with the power name for width. Sharing the row,
 * the longest power name left its diplomat the least room, so at 1280x800 — a supported
 * width — "Fürst Leopold von Auwitz" clipped at 167 of 178px on first render, and one row
 * stood 20px taller than the other six. Identifying who holds an unclaimed seat is this
 * list's whole job, so the clip was in the one place it could not be afforded.
 *
 * Declared *before* the `max-height: 800px` block below, not after it: a media query adds
 * no specificity, so this rule sitting later silently defeated that block's compression
 * and quietly took back the height the seat list needs. Second time this file has been
 * caught by it.
 */
.room-seat-who {
  margin: 1px 0 0; font-style: italic; font-weight: 400; color: var(--text-faint);
  min-width: 0; overflow-wrap: break-word; line-height: 1.25;
  /* Names the person a whole room is about to negotiate with, and it was 11.5px on a
     screen read from across that room. */
  font-size: clamp(11.5px, 1vw, 14px);
}
/* One line: link (or state) on the left, the way to replace it on the right.
   Seven of these have to fit on the screen the room reads its links off. */
/*
 * One height, whatever this seat currently says.
 *
 * The line holds a join URL, or the word SEATED, or a disconnection notice, and each is a
 * different height — measured 61.2px, 48.5px and 72.8px at 1440x900. So every time one of
 * seven people sat down, the rows beneath them jumped about 13px, on the one screen the
 * other six are reading their own links off. The list is a reference table being read by
 * a room; it must not move under them.
 *
 * `min-height`, so the taller disconnection notice can still say what it needs to.
 */
.room-seat-line {
  display: flex; align-items: center; gap: 10px; min-width: 0; margin-top: 1px;
  min-height: 26px;
}
/* The diplomat's line keeps its box even when it has nothing to say — see the note in
   `renderRoomSeats`. That is what holds every row to one height, rather than a
   `min-height` guess that is only right at one type size. */
.room-seat-who:empty::before { content: '\00a0'; }
/* And the state line holds one line however long its message is. Reserving the diplomat's
   box fixed the seated/unseated pair, but a disconnected phone puts a longer warning here
   — "PHONE DISCONNECTED — NEW LINK REOPENS THE SEAT" — which wrapped and lifted that row
   6.28px above its six neighbours, against a 2px tolerance, leaving its New link button
   out of line with the rest. Fourth appearance of this measurement in the room script, so
   it is clamped rather than trusted to fit.

   Scoped to the three *state* variants and never to the URL itself: this file already
   records why the link must not be truncated — it is the only way to take the seat and is
   meant to be read off the screen and typed into a phone, so a wrapped link costs ~18px
   of row and an ellipsised one costs the player. A state word has no such claim. */
.room-seat-link:is(.is-seated, .is-gone, .is-quiet) {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.room-seat-line > .room-seat-link { flex: 1 1 auto; min-width: 0; }
/* Quieter than its neighbours, but still a target. At `padding: 2px 8px` it measured
   18.5px against 34px for every other `.ghost-button--tiny` in the product — half the
   size, seven times over, on the control that reissues a join link, and under the 24px
   minimum for the pointer type the shared board is actually driven with. */
.room-seat-fresh {
  flex: none; font-size: 11px; opacity: .7; padding: 4px 8px; min-height: 26px;
}
.room-seat-fresh:hover, .room-seat-fresh:focus-visible { opacity: 1; }
/* If it still cannot all fit, say so rather than clipping in silence.
 *
 * `scrollbar-width: thin` used to be here, and it is exactly what the note beside
 * `.flimsy-lines` warns against: specifying it makes Chrome use the standard overlay
 * scrollbar and ignore the `::-webkit-scrollbar` rules declared for this very element. The
 * scrollbar then occupied no layout and drew nothing, so on 1280x720, 1366x768 and
 * 1440x900 one to three join links sat below an invisible fold on the screen whose whole
 * job is handing those links out. The fade and the scrollbar both come from `.scroller`
 * now, which this list carries — a verbatim second copy of the rules stood here and had
 * already drifted, omitting the `:hover` thumb, which is the divergence the consolidation
 * was written to end.
 *
 * This block was briefly two comments with a stray close-marker between them, and it took
 * the rule below down with it in silence: a seated seat then rendered byte-identical to an
 * open seat's join URL on the shared board. The check that catches this is not brace depth
 * and not comment balance — it is counting style rules in the source against
 * `cssRules.length` in the CSSOM.
 */
.room-seat-link.is-seated { color: var(--verdigris); }
/* Set like SEATED, and against it: the same line in the same place, so the one
   row that has changed is the one that catches the eye from across a table. */
.room-seat-link.is-gone {
  color: var(--warn-soft);
}
/* 12px floor, not 9.5. This is lowercase running prose explaining the win condition and
   how unclaimed seats are played, on the screen whose own section header says everything
   is larger than it would be on a personal screen — and it was the smallest text anywhere
   in the product, against this file's own stated rule that nothing a player has to read
   sits below 12px. */
.room-foot {
  margin: 12px 0 0; font-size: clamp(12px, .85vw, 14px);
  line-height: 1.6; color: var(--text-faint);
}
.room-begin { width: 100%; margin-top: 14px; }
.room-begin .seal-button-face { font-size: clamp(13px, 1.2vw, 17px); padding: 12px 14px; }
.room-actions { display: flex; gap: 6px; margin-top: 12px; align-items: center; }

/* `.ghost-button--lead` lived here: a button carrying its own description in a nested
   <small>. The three ways in are now one shape — label, control, fine print — so nothing
   uses it and it is gone rather than left as a rule with no markup. */

/* A laptop on the table is very often 1280×720. At that height the prose
   pushed two of the seven join links below a fold nobody scrolls — on the one
   screen whose whole job is handing out those links. */
/* Raised from 800px: seven seats plus the head, the hint and the foot do not fit a
   900px-tall laptop either — measured 164px of overflow at 1440x900, hiding two join
   links behind a boundary. The prose is what gives way, and it can: the legend explaining
   the italic names now lives in `#roomNote`, which is on screen at every height. */
/* 980, not 940: measured, the prose plus seven seats needs about 963px, so between 941
   and ~962 the seventh join link was clipped by 10.4px inside a nested scroller a room of
   people will not think to scroll — immediately above the breakpoint written to prevent
   exactly that. */
@media (max-height: 980px) {
  /* `.room-foot` is no longer hidden by height. It carried the only statement of how the
     game ends anywhere on the shared board — "First to eighteen centres wins outright" —
     and this rule removed it at every board size anybody actually uses, 1440x900 and
     1280x800 included. The trade the rule was written for is real, so it is now paid the
     other way: `renderRoomSeats` shows the foot only once the game has begun, which is
     both when Begin's own space is freed and when `#roomNote` stops carrying the legend
     about the italic names. The hint stays hidden — it explains the links, which are
     visible three inches above it. */
  .room-hint { display: none; }
  .room-seat { padding: 2px 0; }
  /* Giving the diplomat's name its own line costs about 19px a row, and seven of those
     put two join links below a fold nobody scrolls — the very thing this block exists to
     prevent. Paid for out of the leading and the replace-link button's padding rather
     than out of any name, link or state word: measured, the list has to come under 418px,
     and these take it from 525 to 405. The button was setting the line's height at
     22.5px while the link it sits beside is 15.8px. */
  .room-seat-who { font-size: 11.5px; line-height: 1.1; margin-top: 0; }
  .room-seat-name { line-height: 1.15; }
  .room-seat-line { margin-top: 0; }
  /* Tighter where every pixel of the seat list is contested. The 24px floor here is for a
     mouse; a finger gets 44px from the coarse-pointer block declared after this one,
     which is the only reason that block can still win — see the note beside it. */
  .room-seat-fresh { padding: 2px 8px; min-height: 24px; }
  /* Not truncated: this link is the only way to take the seat and is meant to
     be read off the screen and typed into a phone. A wrapped link costs ~18px
     of row; an ellipsised one costs the player. One declaration — there were two in
     this block, 1.15 then 1.3, and the first never drew. */
  /* 1.18, not 1.3, now the link is set larger. Raising it so a room can actually read it
     put the list 10px over its scrollport at 1280x800 and dropped the seventh link behind
     the fade — the precise failure this block was written to prevent, reintroduced by the
     fix for the one above it. Paid for out of the leading, like every other pixel in this
     block: 0.12 of a ~15px line, seven times, is the 10px owed.
     The size settled at a 14px cap rather than 16 for a second reason: at 16 the URL
     needed 259px of a ~250px column and spilled a single character onto a second line.
     A link that is meant to be typed off the screen must not wrap, and one trailing
     character on its own line is worse to read than a slightly smaller line that holds.
     What actually caps it is that the host and port are repeated on all seven rows —
     stating those once would buy the rest. */
  .room-seat-link { line-height: 1.18; }
}

@media (max-width: 900px) {
  .room { grid-template-columns: 1fr; grid-template-rows: auto auto auto; height: auto; min-height: 100dvh; }
  .room-head { flex-wrap: wrap; }
  .room-ready { flex: 1 1 100%; }
  /*
   * Stacked, the board is sized the way the table's stacked board is sized: from the
   * width it has, capped against the viewport's height. Not by pinning the container to a
   * slice of the viewport and deriving the width from that.
   *
   * `min-height: 52vh` did the second, and it ran backwards across its own breakpoint.
   * Measured: 901x800 gave a 547.0x502.1 frame in a 547px column; 900x800 gave
   * 428.1x393.0 in an 864px column — one pixel narrower, 118.9px less map, and 217.9px of
   * empty column either side of the largest element on a screen a whole table looks at.
   * The cap was never the width; it was 52vh feeding `100cqh`. The trade it was meant to
   * buy was not bought either — the seat list still began at y592 with 263px of scroll
   * under it.
   *
   * 62dvh is the table's cap, so the two boards now step the same way, and at 900x800 the
   * frame comes out 540x496 against the 547x502 on the other side of the breakpoint. */
  .room-board { min-height: 0; container-type: normal; }
  .room-board .board-frame {
    /* `.room`'s own padding, written out, because the column is the viewport less that —
       and with the container gone there is no `100cqw` to ask. */
    width: auto; max-width: 100%; margin: 0 auto;
    height: min(62dvh, calc((100vw - 2 * clamp(14px, 2vw, 28px)) / var(--map-ratio)));
  }
  /* The page scrolls at this width, so the seat list must not be a scroller
     nested inside it. */
  .room-seats { overflow: visible; flex: none; }
}


/*
 * The spend line under the ledger. Quieter than the standings above it — it is
 * housekeeping, not the game — and it goes brass when the ceiling is reached, which
 * is the one moment it stops being housekeeping.
 *
 * It sets no size of its own: the element already carries .ledger-foot, whose three
 * rules cover every breakpoint, so inheriting is what keeps the two lines in the
 * intended relationship instead of fighting it. Pinned at 10px and declared after both
 * responsive rules at equal specificity, it used to win everywhere — staying 10px while
 * its sibling took the 12px mobile bump, and going *larger* than the standings at 900.
 * Quieter is done with colour and the mono face, neither of which has a breakpoint to
 * disagree with.
 *
 * This block was briefly three comments with two orphaned close-markers between them,
 * which made the parser swallow the rule below entirely and in silence — the readout
 * then rendered byte-identical to the static rules copy above it. One comment, one rule.
 */
.ledger-spend {
  margin-top: 2px; font-family: var(--mono); letter-spacing: .02em;
  color: var(--text-dim);
}
.ledger-spend.is-spent { color: var(--brass); }

/* Civil disorder, on the shared screen. Brass, because a turn nobody wrote orders
   for is the one thing on that screen a room needs to notice. */
.room-note-disorder { color: var(--brass); }

/* ══════════════════════  Last word: the touch floor  ══════════════════════
 *
 * A finger needs 44px whatever else the layout wants, so this is the last thing in the
 * file. It has to be: a media query adds no specificity, so the winner between two
 * equal-specificity rules is simply the later one — and the height-keyed blocks above
 * shrink controls to fit short screens. `.room-seat-fresh` was the proof: the main
 * `(pointer: coarse)` block gives every `.ghost-button--tiny` 44px, but
 * `(max-height: 940px)` is declared after it and set 24px, so on every phone-sized
 * viewport the shared board's per-seat control rendered at 24px and the block that
 * caused it was invisible to anyone reading either block alone.
 *
 * Anything a height-keyed rule compresses belongs here as well.
 */
@media (pointer: coarse) {
  /* The key caps belong to the pointer, not to the viewport — the same lesson the target
     sizes in this block were written for. A wide tablet is not narrow enough to match the
     700px rule above, and has no keyboard either. */
  .hint-actions kbd, .hintbox > kbd { display: none; }
  /*
   * And the zoom buttons, which belong to the pointer for the same reason.
   *
   * Dropping them was right and the reasoning still holds — pinch is the native gesture,
   * `⤢` still resets the view, and five stacked buttons covered an eighth of a 321px
   * board. But the rule was keyed to **width** while the gesture it defers to is keyed to
   * **touch**, so a narrow desktop window got neither: measured at 360x780 with a mouse,
   * `#zoomIn`/`#zoomOut` were 0x0 and a wheel event over the board left the transform at
   * `scale(1.000)`. No way to zoom the map at all, on the screen where the province names
   * are hidden until you do.
   */
  #zoomIn, #zoomOut { display: none; }
  .room-seat-fresh { min-height: 44px; padding: 4px 10px; }
  .gate-field select, .gate-field input { min-height: 44px; }
}
