/*
  Layout and typography only.

  THERE IS NOT ONE GAME COLOR IN THIS FILE. Every terrain fill, overlay ring,
  hexside stroke, seat color, hidden-counter fill, selection ring and mesh
  stroke comes from data/ui/palette.json through /api/v1/catalog, where it is
  joined against data/terrain-vocabulary.json in BOTH directions at server boot -
  an id with no color and a color with no id are startup errors. A color that
  lived here would be a value with no join, and this project has already paid for
  one of those: the terrain editor's palette carries a dead `Hill` entry (the id
  was deleted from the vocabulary because zero hexes ever used it) and is missing
  `Flooded` entirely. The two-sided join found both.

  What IS here is chrome: the shell's grays, the state pills, and the three
  semantic accents for ok / warn / bad, which describe the UI's own state rather
  than anything on the map.

  `gray` is spelled `gray` throughout - tools/check_spelling.py's pair list
  refuses the British form, and it is a legal CSS color name, so this is a real
  trap rather than a stylistic preference.
*/

:root {
  --bg: #0f0f13;
  --panel: #17171d;
  --panel-2: #1e1e26;
  --line: #2c2c37;
  --text: #e8e6e1;
  --muted: #9a978f;
  --ok: #59c06a;
  --warn: #d9a441;
  --bad: #e0605a;
  --accent: #c8a367;
  --mono: ui-monospace, Menlo, Consolas, "DejaVu Sans Mono", monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
}

body { display: flex; flex-direction: column; }

.screen { flex: 1 1 auto; min-height: 0; }
.screen[hidden] { display: none; }

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

.home { overflow: auto; padding: 24px 28px 60px; max-width: 1100px; }
.home h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: 0.01em; }
.home h2 { font-size: 15px; margin: 26px 0 8px; color: var(--accent); }
.home h3 { font-size: 13px; margin: 20px 0 6px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }

.toggles { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 6px; }
.toggle {
  display: grid;
  grid-template-columns: auto auto auto 1fr;
  align-items: start;
  gap: 6px;
  padding: 7px 9px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 5px;
}
.toggle.inert { opacity: 0.62; }
.toggle.readonly { opacity: 0.9; }
.toggle .tname { font-weight: 600; }
.toggle .tdesc { grid-column: 1 / -1; color: var(--muted); }
/* The printed EFFECT, indented under the description. Both were on the unread
   allowlist until M10 and neither reached the browser. */
.toggle .teffect {
  grid-column: 1 / -1; color: var(--muted); font-size: 12px;
  padding-left: 8px; border-left: 2px solid var(--line);
}

.interps { display: grid; grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 8px; }
.interp { padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 5px; }
.interp .iname { display: block; font-weight: 600; margin-bottom: 4px; }
.interp .iq { margin-top: 5px; font-size: 12px; }
.interp.readonly { opacity: 0.9; }
/* One row per option, with its own citations. `cites` has ridden the wire on
   /api/v1/catalog since M2 and was rendered by nothing. */
.interp .ioptions { margin-top: 6px; display: grid; gap: 3px; }
.interp .ioption { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; font-size: 12px; }
.interp .ioption.chosen .iolabel { color: var(--ok); font-weight: 600; }
.interp .ioption.disabled { opacity: 0.6; }

/* "Now standard in BfN2 - not switchable." Read-only, no inputs: the answer to
   "where did the switch go" rather than "what can I turn on". */
.nowstandard { margin-top: 22px; }
.nsrow {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline;
  padding: 5px 9px; margin-bottom: 4px;
  background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid var(--line); border-radius: 5px;
}
.nsrow .tname { font-weight: 600; }
.nsrow .tdesc { flex-basis: 100%; color: var(--muted); font-size: 12px; }

/* The creation form's 109 controls, as collapsed groups. Every group starts
   closed: the defaults are the default path, and the summary line carries the
   count and how many controls differ from their default so a creator knows
   whether there is anything to open. Rows a filter hides use the `hidden`
   attribute, never a style attribute - the deployed CSP is style-src 'self'. */
.settings { display: grid; gap: 8px; margin-top: 12px; }
.settings > p { margin: 0; font-size: 12px; }
.settings-group { border: 1px solid var(--line); border-radius: 5px; padding: 6px 10px; background: var(--panel-2); }
.settings-group summary { cursor: pointer; font-weight: 600; }
.settings-group > .toggles, .settings-group > .interps { margin-top: 8px; }
.changed-only { display: flex; gap: 6px; align-items: center; font-size: 12px; color: var(--muted); cursor: pointer; }

.formbar { margin: 22px 0 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* The create-game token, when the operator armed one. Always rendered, because
   the catalog does not say whether the gate is armed and must not - "is this
   server gated" answered to an anonymous caller is a probe. An empty box sends
   no header at all, so a local server behaves exactly as it did. */
.create-token {
  flex: 1 1 260px; min-width: 200px; padding: 8px 10px;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--line); border-radius: 4px; font-family: var(--mono); font-size: 12px;
}
.seatlist .seatrow { display: flex; gap: 10px; align-items: center; padding: 4px 0; }
/* A game can hold BOTH seats since the F5 fix, so the list has to say which one
   a click will use. The inactive row is dimmed AND its seat is labeled - dimming
   alone would be a state legible only to a reader who can compare two greys. */
.seatlist .seatrow.muted { opacity: 0.62; }
.seatlist .badge {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 5px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ---- play layout ------------------------------------------------------ */

.play { display: flex; flex-direction: column; height: 100%; }

.turnbar {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: center;
  padding: 8px 12px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}
.tb-group { display: flex; gap: 7px; align-items: center; }
.tb-group.grow { flex: 1 1 auto; }
/* The resign confirmation: the authored sentence between the two verbs, set off so it reads as a question and not as a readout. */
.tb-group.confirm { flex-wrap: wrap; padding: 4px 8px; border: 1px solid #593a3a; border-radius: 4px; background: #241a1a; }
.tb-prompt { max-width: 46em; color: var(--text); }
.tb-k { color: var(--muted); text-transform: uppercase; font-size: 10px; letter-spacing: 0.09em; }
.tb-v { font-family: var(--mono); }
.tb-v.strong { font-weight: 700; color: var(--accent); }
.tb-v.warn { color: var(--warn); }
.seat { font-weight: 600; }
.seat-Allied { color: #7fb2e5; }
.seat-German { color: #cf9a8a; }

/* `min-height` IS THE WHOLE POINT OF THIS RULE, and it was measured rather than
   guessed. `.play` is a 100%-height flex column, so `flex: 1 1 auto` gives the
   layout WHATEVER IS LEFT - and in a 418 CSS-pixel-tall browser pane the turn
   bar wrapped to five lines, the notification bar and the 12.3 bar took another
   two, and the log's `max-height: 30vh` took the rest: the map and the whole
   side column were left THIRTEEN pixels tall. Not clipped and not scrolled -
   crushed, with the inbox and the policy pane unreachable by any gesture. A
   floor plus a scrolling page keeps them reachable in a small window and
   changes nothing in a large one, where `flex: 1 1 auto` still wins. */
.play { overflow: auto; }
.layout { flex: 1 1 auto; display: flex; min-height: 420px; }
/* `overflow: hidden` IS A CLICK GUARD, not a tidiness rule. The canvas carries
   an explicit pixel size (see MapRenderer.resize) and is therefore capable of
   being wider than this box for as long as it takes the ResizeObserver to fire.
   A canvas that overhangs is INVISIBLE and still hit-tests: measured, an 83
   pixel overhang put the map on top of the inbox's SUBMIT ALL button, which
   drew correctly and could not be pressed. */
.mapwrap { position: relative; flex: 1 1 auto; min-width: 0; overflow: hidden; }
#map { display: block; width: 100%; height: 100%; cursor: crosshair; touch-action: none; }
.maphint {
  position: absolute; left: 10px; bottom: 8px;
  color: var(--muted); background: rgba(15, 15, 19, 0.72);
  padding: 3px 7px; border-radius: 4px; font-size: 11px; pointer-events: none;
}
/* A focused canvas says so: it is the one thing on the page a keyboard user
   steers a cursor across, and without a ring the arrow keys read as broken. */
#map:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The zoom path for a screen with no wheel. Right of the maphint, above the
   canvas, and unlike the hint it DOES take pointer events. */
.mapcontrols {
  position: absolute; right: 10px; bottom: 8px;
  display: flex; gap: 4px;
}
.mapcontrols button { min-width: 26px; font-family: var(--mono); font-size: 13px; }

/* 460px WHERE THERE IS ROOM, AND NEVER MORE THAN HALF THE WINDOW.
   MEASURED in a 512 CSS-pixel browser pane: `flex: 0 0 460px` left the map 52px
   wide, which put the `.maphint` line on top of the turn bar and made the board
   unclickable - and the board is the only way to select the attackers an
   ATTACK_DECLARATION block needs. A fixed basis is right for a desktop window
   and wrong for the narrow one somebody actually has. */
.side {
  width: min(460px, 50vw); flex: 0 1 min(460px, 50vw);
  border-left: 1px solid var(--line);
  overflow: auto; background: var(--panel);
}
.pane { padding: 10px 12px; border-bottom: 1px solid var(--line); }
.pane h2 { font-size: 13px; margin: 0 0 8px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; }
.pane h3 { font-size: 11px; margin: 14px 0 6px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.pane-head { display: flex; gap: 12px; align-items: baseline; }

.log { flex: 0 0 auto; max-height: 30vh; overflow: auto; background: var(--panel); border-top: 1px solid var(--line); }

/* ---- generic bits ----------------------------------------------------- */

.row { display: flex; gap: 8px; align-items: baseline; padding: 1px 0; }
.row .k { color: var(--muted); min-width: 108px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
.row .v { font-family: var(--mono); word-break: break-word; }
.muted { color: var(--muted); }
.mono { font-family: var(--mono); }
.strong { font-weight: 700; }
.bad { color: var(--bad); }
.hint { margin: 4px 0 8px; font-size: 11px; }

button {
  font: inherit; font-size: 12px;
  color: var(--text); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 9px; cursor: pointer;
}
button:hover:not(:disabled) { border-color: var(--accent); }
button:disabled { opacity: 0.42; cursor: not-allowed; }
button.primary { background: #2b3a2c; border-color: #3d5540; }
button.danger { background: #372323; border-color: #593a3a; }
button.small { padding: 2px 6px; font-size: 11px; }
button.big { padding: 8px 18px; font-size: 14px; }

select, input[type="number"], input[type="text"], input[type="email"] {
  font: inherit; font-size: 12px;
  color: var(--text); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: 4px; padding: 3px 5px;
}

.pill {
  display: inline-block; padding: 1px 6px; border-radius: 9px;
  background: var(--panel-2); border: 1px solid var(--line);
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.pill.ok { color: var(--ok); border-color: #2f5a35; }
.pill.warn { color: var(--warn); border-color: #5c4620; }
/* A STANDING POLICY ANSWERED THIS, NOT THE PLAYER. Deliberately the loudest pill
   on the page: the attribution defect this milestone exists to close is a log
   that reads as if the opponent decided something a policy decided while they
   slept, and a badge nobody notices is the same defect with extra steps. */
.pill.policy { color: #d6c2f2; border-color: #4a3a68; background: #241d33; text-transform: none; letter-spacing: 0; }

.panel { margin: 8px 0; padding: 8px 10px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px; }
.panel.ok { border-color: #2f5a35; }
.panel.bad { border-color: #5b3030; }
.panel-title { font-size: 10px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); margin-bottom: 5px; }

/* ---- citations -------------------------------------------------------- */

/* A real <button>, so it is focusable and opens on Enter and on a tap - a
   title-only span was unreachable by keyboard and invisible on any tablet. The
   button reset below undoes the chrome the generic `button` rule gives it. */
.cite {
  display: inline-block; margin: 0 2px;
  padding: 0 5px; border-radius: 3px;
  background: #2a2a34; border: 1px solid #3a3a48;
  font-family: var(--mono); font-size: 10px; color: var(--accent);
  line-height: 1.5; vertical-align: baseline;
  cursor: help;
}
.cite:hover:not(:disabled) { border-color: var(--accent); }
.cite:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cite-dialog h2 { font-size: 15px; }
.cite-unknown { color: var(--warn); border-style: dashed; }
/* A number BfN2 itself prints wrong. The chip shows what the page says and the
   hover carries the corrected cite - "unknown" is the wrong word for a
   cross-reference this project has already researched. */
.cite-corrected { border-style: dotted; }
/* A source that is real and is not a BfN2 rule number: a Playbook scenario, a
   TEC row, a worked example, the glossary, an owner ruling, a file in this
   repository. SOLID like a resolved rulebook cite, because it IS resolved - the
   hover carries an authored section and summary - but not the same border, so a
   reader can still tell "the rulebook says" from "the scenario book says". It
   was 27 of 298 option chips drawing the dashed cite-unknown border before the
   second corpus existed. */
.cite-source { border-style: solid; border-color: var(--muted); }

/* ---- the build's own disclosure --------------------------------------- */

/* Optional rules that are ON in this game and that this build does not
   evaluate. Standing, not dismissible: three of them are Omega, so they are on
   in every game nobody deliberately switched them off in. */
.unwired { padding: 8px 12px; border-bottom: 1px solid var(--line); background: #2b2418; }
.unwired.empty { display: none; }
.unwired .unwired-title { font-weight: 600; color: var(--warn); margin-bottom: 4px; }
.unwired .unwired-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; padding: 3px 0; }
.unwired .unwired-row .tname { font-weight: 600; }
.unwired .unwired-row .tdesc { flex-basis: 100%; font-size: 12px; }
.unwired .unwired-row .muted { flex-basis: 100%; font-size: 12px; }

/* Mid-game: which optional rules are in force and which reading of each
   contested rule this game took. Collapsed, because it is reference. */
.rulesinforce summary { cursor: pointer; font-weight: 600; }
.rulesinforce .rulesbody { margin-top: 8px; display: grid; gap: 6px; }
.rulesinforce h3 { font-size: 12px; margin: 10px 0 2px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }

/* ---- chain ------------------------------------------------------------ */

.chain-head { margin-bottom: 8px; }
.blocks { list-style: none; margin: 0; padding: 0; }
.block { margin: 0 0 8px; padding: 7px 9px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 5px; }
.block-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.block-head .bp { display: flex; gap: 3px; align-items: center; color: var(--muted); font-size: 11px; }
.params { margin: 5px 0; }
.sites { margin-top: 6px; border-top: 1px dashed var(--line); padding-top: 5px; }
.site { margin: 5px 0; }
.site-head { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; }
.declare, .declared { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-top: 3px; }
.problem { margin-top: 5px; font-size: 12px; }
.problem.bad { color: var(--bad); }
.problem.warn { color: var(--warn); }
.adders { display: flex; flex-direction: column; gap: 3px; }
.adder { display: flex; gap: 6px; align-items: center; }
.adder.unbuilt { opacity: 0.6; }
.runbar { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.runrow { display: flex; gap: 8px; align-items: center; }

/* ---- inbox ------------------------------------------------------------ */

.inbox { padding: 10px 12px; background: #241f14; border-bottom: 1px solid #5c4620; }
.inbox.empty { display: none; }
.inbox-card { margin-bottom: 8px; }
.inbox-head { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.inbox-message { margin: 6px 0; font-size: 13px; }
.inbox-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
/* 12.10's compound answer: one button, one line per authored leg, in the order
   data/rules/step-loss.json retreat.compoundChoice gives - currency, then hex,
   then step. A segment the pretty-printer did not recognize is shown verbatim
   and marked, so a new leg is visible rather than silently missing. */
.inbox-actions button .key-leg { display: block; text-align: left; white-space: nowrap; }
.inbox-actions button .key-leg.raw { font-family: ui-monospace, monospace; opacity: 0.75; }
/* One card, N INDEPENDENT sub-rows, one submit. Each row draws its own options;
   there is deliberately no merged list and no "steps remaining" readout, because
   12.9 scopes its one-step-each rule to the current combat. */
.inbox-card.batch { border: 1px solid #5c4620; border-radius: 3px; padding: 8px; }
.batch-row { display: flex; gap: 6px; align-items: center; margin: 4px 0; }

/* 12.10 / 12.11 asked ONE LEG AT A TIME, in the order the data authored. The
   rows below are a narrowing of the server's own option list - every value is a
   substring of a key the kernel enumerated - and .answer-preview is the exact
   string the ANSWER button will post, shown because the phrases above it are
   this client's reading of the key and not the key. */
.answer-picker { border: 1px solid #5c4620; border-radius: 3px; padding: 8px; margin-top: 6px; }
.answer-picker .leg-head { display: flex; gap: 6px; align-items: baseline; margin: 8px 0 2px; }
.answer-picker .leg-head:first-child { margin-top: 0; }
.answer-picker .leg-head .k { font-weight: 600; }
.answer-picker .row { display: flex; gap: 8px; align-items: center; margin: 2px 0 2px 12px; }
.answer-picker .row .k { min-width: 90px; opacity: 0.85; }
.answer-picker select { max-width: 22rem; }
.answer-preview { margin: 8px 0 0; padding: 4px 6px; background: #1b1710; border-radius: 3px; word-break: break-all; }
/* Never removed, only collapsed: the picker must not be able to make an answer
   the server offered unreachable, and a player reporting a bug needs the exact
   string. */
.raw-options { margin-top: 8px; }
.raw-options summary { cursor: pointer; opacity: 0.8; }
.raw-options button { display: block; text-align: left; margin: 4px 0; }
.raw-options button .key-leg { display: block; text-align: left; white-space: nowrap; }
.raw-options button .key-leg.raw { font-family: ui-monospace, monospace; opacity: 0.75; }

/* ---- notifications ---------------------------------------------------- */

/* NEVER `display:none` WHEN EMPTY, unlike .notice. A notice is about the request
   you just made, so hiding it is right; this bar answers "is anything waiting
   for me", and a bar that vanished would make "nothing waiting" and "the bar is
   broken" look identical to somebody opening the page after two days away. */
.notifybar {
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding: 6px 12px; border-bottom: 1px solid var(--line); background: var(--panel-2);
}
.notifybar.unread { background: #21261b; border-bottom-color: #46512f; }
/* THE TAB'S OWN STATE, not the game's: the server has not answered the poll for
   a while. The bad color, because "Nothing waiting for you." under it is now a
   claim about the past and the bar must not read as current. */
.notifybar.offline { background: #2a1c1c; border-bottom-color: #5b3030; }
.notifybar .notify-label { font-weight: 600; }
.pill.bad { color: var(--bad); border-color: #5b3030; }

/* ---- 12.3 Mandatory Attacks ------------------------------------------- */

/* HIDDEN WHEN EMPTY, like .notice and unlike .notifybar: an obligation bar with
   nothing in it is not news, and the seat that does not own the phase gets an
   empty array on every poll. What it must never do is be SUBTLE when it is not
   empty - decision B6 lets the advance succeed with an obligation unmet, so
   this bar is the only thing standing between a player and an opponent-executed
   retreat of their own counters. */
.obligations { padding: 7px 12px; border-bottom: 1px solid var(--line); }
.obligations.empty { display: none; }
.obligations.warn { background: #2b2416; border-bottom-color: #6a5620; }
.ob-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.ob-head .ob-message { font-weight: 600; }
.ob-row { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 2px 0 0; font-size: 12px; }
.ob-row .k { font-family: var(--mono); font-weight: 600; }

/* ---- victory ----------------------------------------------------------
   4.0 D's progress panel. NOT styled as a warning: it is true for as long as
   the game exists and it is not news, so it reads as a standing readout until
   `decisive` arrives - the one turn where the answer is final and the panel
   earns the warn color the rest of this sheet reserves for B6.
   `.empty` is the pre-first-End-of-Day state and disappears entirely, rather
   than drawing a row of zeroes for objectives nobody has evaluated yet. */
.victory { padding: 8px 12px; border-bottom: 1px solid var(--line); }
.victory.empty { display: none; }
.victory.decisive { background: #2b2416; border-bottom-color: #6a5620; }
.v-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.v-head .v-title { font-weight: 600; }
.v-head .v-held { font-family: var(--mono); }
.v-obj { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 3px 0 0; font-size: 12px; }
.v-obj .k { font-weight: 600; }
.v-hex { display: inline-flex; gap: 5px; align-items: baseline; }
.v-hex .v-control { font-size: 11px; color: var(--muted); }
.v-obj.held .v-control { color: var(--ok); }
.v-verdict { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 5px 0 0; }
.v-verdict .v-quote { font-style: italic; flex-basis: 100%; font-size: 12px; }
.v-semantics { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11px; padding-top: 4px; font-family: var(--mono); }

/* ---- 18.9.4 isolation --------------------------------------------------
   Twelve checks a game and, on the shipped board, usually nothing to say -
   so the resting state is a one-line readout and `.fired` earns the warn
   color only when a counter was actually removed. `.empty` disappears
   entirely: before the 7 June Night turn the rule has not run, and a row of
   zeroes would claim a check nobody made. The counter ID column is
   deliberately the same mono treatment as `.v-hex`, because for the Allied
   seat it is a HEX - the id is omitted for a face-down Strongpoint. */
.isolation { padding: 8px 12px; border-bottom: 1px solid var(--line); }
.isolation.empty { display: none; }
.isolation.fired { background: #2b2416; border-bottom-color: #6a5620; }
.i-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.i-head .i-title { font-weight: 600; }
.i-head .i-count { font-family: var(--mono); }
.i-none { font-size: 12px; padding-top: 3px; }
.i-subj { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 3px 0 0; font-size: 12px; }
.i-subj.gone .i-die { color: var(--warn); }
.i-subj .i-reach { font-size: 11px; }
.i-semantics { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11px; padding-top: 4px; font-family: var(--mono); }

/* ---- notice ----------------------------------------------------------- */

/* A LIST OF ENTRIES, each coloured by its own kind and each with its own
   dismiss. One phase close can say five things, and a single slot showed the
   last of them. The mount itself is only a container now. */
.notice { border-bottom: 1px solid var(--line); }
.notice.empty { display: none; }
.notice-entry { padding: 8px 12px; border-bottom: 1px solid var(--line); }
.notice-entry:last-child { border-bottom: none; }
.notice-entry.error { background: #2a1c1c; border-bottom-color: #5b3030; }
.notice-entry.info { background: #1b2430; border-bottom-color: #2f4560; }
/* The B6 color: the request SUCCEEDED and the position is one the rules object
   to. Neither red (nothing failed, nothing to retry) nor blue (not routine). */
.notice-entry.warn { background: #2b2416; border-bottom-color: #6a5620; }
/* A new build is running under this tab. No dismiss, a RELOAD instead, and the
   loudest border on the page: every request this tab makes from here may be
   refused by a schema it has never seen. */
.notice-entry.stale { background: #2a1c1c; border: 2px solid var(--bad); }
.notice-message { font-weight: 600; margin-bottom: 3px; }
.notice-line { font-family: var(--mono); font-size: 11px; color: var(--muted); padding: 1px 0; }

/* ---- log -------------------------------------------------------------- */

.logrows { display: flex; flex-direction: column; gap: 2px; }
.logrow { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 2px 0; border-bottom: 1px solid #1d1d24; }
.logrow .ord { font-family: var(--mono); color: var(--muted); min-width: 22px; text-align: right; }
.logrow .label { font-weight: 600; }
.logrow.role-OPPONENT .label { color: #cf9a8a; }
/* The whole ROW is marked, not just the badge: a reader scanning a log after two
   days away is looking for "which of these did I not decide". */
.logrow.by-policy { background: #1e1a27; border-bottom-color: #382c50; }
.facts { display: flex; gap: 10px; flex-wrap: wrap; }
.fact { display: flex; gap: 4px; }
.fact .fk { color: var(--muted); font-size: 11px; }
.fact .fv { font-family: var(--mono); }
.subjects { color: var(--muted); font-size: 11px; }

/* ---- hex inspector ---------------------------------------------------- */

.hexsides, .occupants, .editor { margin-top: 9px; }
.unit { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; padding: 3px 0; border-bottom: 1px solid #1d1d24; }
.unit.selected { outline: 1px solid var(--accent); outline-offset: 2px; }
.hiddenunit { color: var(--muted); font-style: italic; }
.overlaybox { display: flex; gap: 10px; flex-wrap: wrap; margin: 5px 0; }
.ovl { display: flex; gap: 3px; align-items: center; font-size: 11px; color: var(--muted); }
.hexbuttons { display: flex; gap: 6px; margin-top: 8px; }

.answer-value { width: 90px; }

/* ---- standing policies ------------------------------------------------ */
/*
 * The pane is GENERATED from data/policies.json, so these rules name widget
 * SHAPES and never a policy, a token or an option - the same reason there is no
 * per-toggle and no per-terrain rule anywhere in this file.
 */

.pane.policy h4 { font-size: 11px; margin: 8px 0 4px; color: var(--muted); }
.policy { border-bottom: 1px solid #1d1d24; padding-bottom: 8px; margin-bottom: 8px; }
.policy .hint { color: var(--muted); font-size: 11px; }
.policy .warn { color: #d8a657; font-size: 12px; margin: 4px 0; }
.policy.blocked { opacity: 0.75; }
.policy.blocked ul { margin: 4px 0 4px 16px; padding: 0; font-size: 11px; color: var(--muted); }
.permutation { margin: 4px 0 4px 18px; padding: 0; }
.permutation li { padding: 2px 0; display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
.enum-table .row, .unit-list .row { padding: 2px 0; }
.unit-list { max-height: 180px; overflow-y: auto; }

/* ---- Landing Queue (9.2.2) -------------------------------------------- */
/*
 * CHROME ONLY, like the rest of this file. Not one rule here names a beach, a
 * queue, a phase index or a seat: the lanes are generated from
 * /api/v1/map/queues, the ceilings from /state, and the one game color the pane
 * uses - the seat fill on a counter chip - is written INLINE from
 * data/ui/palette.json, which is joined against the vocabulary in both
 * directions at boot. A seat color in this file would be a value with no join.
 */

.q-beachhead, .q-cap, .q-tally { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 3px 0; font-size: 12px; }
.q-cap .k, .q-tally .k, .q-beachhead .k { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
.q-cap .v.warn { color: var(--warn); }
.q-warn { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 3px 6px; margin: 3px 0; font-size: 12px; background: #2b2416; border: 1px solid #6a5620; border-radius: 4px; }

.q-lanes { display: flex; flex-direction: column; gap: 3px; margin: 8px 0; }
.q-lane { display: flex; gap: 4px; align-items: stretch; flex-wrap: wrap; }
.q-beach { min-width: 92px; font-size: 11px; color: var(--muted); align-self: center; }
/* The entry queue is the ONE pipe every arrival comes through without a
   Mulberry, so it is the one lane label that is not muted. */
.q-beach.entry { color: var(--accent); font-weight: 700; }
.q-box { min-width: 58px; padding: 2px 5px; font-size: 10px; color: var(--muted); border: 1px dashed var(--line); border-radius: 4px; text-align: center; align-self: center; }
.q-box.marked { color: var(--text); border-style: solid; border-color: var(--accent); }

.q-slot { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; padding: 2px 5px; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-2); font-size: 11px; }
.q-slot.busy { border-color: #3d5540; }
.q-slot.full { border-color: #5c4620; }
/* "no Beach hex" is a MEASUREMENT (Pointe du Hoc has none) and "(off box)" means
   terrain.json was never asked. Two different facts, two different treatments. */
.q-slot.none { color: var(--warn); border-style: dashed; }
.q-slot.out { color: var(--muted); border-style: dotted; }
.q-slot-label { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
.q-count { font-family: var(--mono); font-size: 10px; color: var(--muted); }
.q-unit { padding: 0 4px; border-radius: 3px; font-size: 10px; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-unit.hidden { background: var(--panel); color: var(--muted); }
/* "+27" rather than twenty-seven more names: bloody-omaha.json stages 31
   counters on one Beach hex at setup, and the count is the number that matters. */
.q-unit.more { background: var(--panel); color: var(--muted); font-family: var(--mono); }

.q-pool { margin: 8px 0; }
.q-avail { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; padding: 2px 0; border-bottom: 1px solid #1d1d24; }

/* THE ARTIFACT: one column per turn, arrivals in against placements out. */
.q-strip { display: flex; gap: 3px; align-items: flex-end; margin: 10px 0 2px; }
.q-turn { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1 1 0; min-width: 0; }
.q-turn.now .q-turn-index { color: var(--accent); font-weight: 700; }
.q-turn-bars { display: flex; gap: 1px; align-items: flex-end; height: 46px; width: 100%; justify-content: center; }
.q-bar { width: 6px; min-height: 1px; border-radius: 1px 1px 0 0; }
.q-bar.scheduled { background: var(--line); }
.q-bar.placed { background: var(--ok); }
.q-turn-index { font-family: var(--mono); font-size: 10px; color: var(--muted); }
.q-turn-weather { font-size: 9px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

/* ---------------------------------------------------------------------------
   3.6's help dialog, and the circled question mark that opens it.

   A NATIVE <dialog>, so the backdrop, the focus trap and Escape-to-close all
   come from the browser and this client adds no keyboard handling of its own.
   Every rule below is a CLASS in this file rather than a style attribute: the
   deployed CSP is `style-src 'self'`, so an inline style is refused by the
   browser and panes/victory.test.ts already pins that for another pane.
   --------------------------------------------------------------------------- */

.help-icon {
  border-radius: 50%;
  width: 16px;
  height: 16px;
  padding: 0;
  margin-left: 6px;
  font-size: 11px;
  line-height: 1;
  vertical-align: middle;
}

.help-dialog {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 16px 18px;
  max-width: 46rem;
  /* The two lists can run to 17 rows; the dialog scrolls rather than the page. */
  max-height: 80vh;
  overflow-y: auto;
}
.help-dialog::backdrop { background: rgb(0 0 0 / 62%); }
.help-dialog h2 { margin: 0 0 8px; }
.help-dialog p { margin: 0 0 10px; max-width: 60ch; }
.help-dialog ul { margin: 4px 0 12px; padding-left: 18px; }
.help-dialog li { padding: 1px 0; }
.help-side { margin-top: 10px; }

/* ---------------------------------------------------------------------------
   6.2's correction dialog. The BOX comes from .help-dialog above - this adds
   only what an election form needs that a help panel does not: one bordered
   block per over-limit hex, a relief line the eye can find, and a radio list
   that does not read as a bulleted paragraph.

   NO COLOR CARRIES MEANING ON ITS OWN. The relief line is bordered as well as
   tinted, because "you can still move out of this one" is the difference
   between paying and not paying and must not be legible only to a reader who
   can tell two greens apart.
   --------------------------------------------------------------------------- */
.stacking-correction-lede { max-width: 62ch; }
.stacking-correction-hex {
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 10px;
  margin: 0 0 10px;
}
.stacking-correction-hex h3 { margin: 0 0 6px; font-size: 13px; }
.stacking-correction-relief {
  border-left: 3px solid var(--line);
  padding-left: 8px;
  margin: 0 0 8px;
  font-size: 12px;
}
.stacking-correction-options { list-style: none; margin: 0; padding: 0; }
.stacking-correction-options li { padding: 2px 0; }
.stacking-correction-options label { display: flex; align-items: baseline; gap: 6px; cursor: pointer; }
.stacking-correction-actions { display: flex; gap: 8px; margin-top: 4px; }

/* ---------------------------------------------------------------------------
   The known-gaps register: printed rules this build does not enforce as
   written. Collapsed beside "Rules in force" mid-game and above CREATE GAME
   on the home screen, in the same <details> shape. Every word in a row is the
   data row's; these rules only keep the quotation apart from the consequence.
   --------------------------------------------------------------------------- */
.knowngaps { margin-top: 10px; }
.knowngaps summary { cursor: pointer; font-weight: 600; }
.knowngaps .gapsbody { margin-top: 8px; display: grid; gap: 6px; }
.knowngaps h3 { font-size: 12px; margin: 10px 0 2px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.knowngaps .gap-kind { margin: 0 0 4px; font-size: 12px; }
.gap-row {
  padding: 6px 9px;
  background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid #5c4620; border-radius: 5px;
}
.gap-head { display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; }
.gap-head .gap-id { font-size: 11px; }
.gap-printed, .gap-instead, .gap-question { display: flex; gap: 8px; align-items: baseline; margin-top: 4px; font-size: 12px; }
.gap-printed .k, .gap-instead .k, .gap-question .k {
  color: var(--muted); min-width: 92px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; flex: 0 0 auto;
}
/* The rulebook's own sentence, set apart from the author's so a reader can
   tell "the rule says" from "the build does". */
.gap-quote { font-style: italic; }
