/* Console chrome. Colour comes entirely from theme.css custom properties. */

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--metal-lo);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 13px;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

button { font: inherit; color: inherit; }
kbd {
  font: inherit; font-size: .85em; opacity: .7;
  border: 1px solid var(--edge); border-radius: 2px; padding: 0 3px; margin-left: 4px;
}

.shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100%;
  overflow: hidden;
  /* The console's own coordinate space, so a question asked over it (the
     LEAVE POST card) covers the console and nothing else. */
  position: relative;
}
.shell[hidden] { display: none; }

/* ---------------------------------------------------------------- topbar */

/*
 * Three fixed columns, not a wrapping row.
 *
 * The bar was a flex row that wrapped, so at 1280 the speed rack — four caps
 * that had grown to four different widths — was ejected out of the top-right
 * corner onto a second row at x=9, three hundred and fifty pixels of dead
 * space from the identity plates and divorced from the clock it drives. Three
 * columns: what the watch is, who is sitting here, and what you can do to the
 * clock. They stay where they are at every desktop width, and what gives when
 * the window narrows is the readouts between them, which are figures and can
 * afford to be tight. Below 900px it is a different piece of hardware and is
 * laid out as one — see the phone console at the end of this file.
 */
.topbar {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center; gap: 4px 12px;
  /* A gutter on both ends. The bar used to be padded 9px and the rack's last
     cap had no housing at all, so the one irreversible control on the console
     sat as bare text against the right edge of the window. */
  padding: 5px 12px;
  border-bottom: 1px solid var(--metal-edge);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .45);
}
/*
 * The master annunciator spells its words. At every width.
 *
 * At 1280 the three lamps used to wrap onto a second row underneath the works
 * plate — an L-shaped bar with a dead gutter in it — and the fix for that was
 * to swap all three captions for RAD / ARM / FLT below 1400px. That is the
 * console's primary status row reduced to a code nobody was taught, with FLT
 * for FAULT reading as "flight" on a screen full of aircraft. The room came
 * out of the speed rack instead, which lost a dim second line saying nothing
 * its own figure did not.
 */
.lamps { display: inline-flex; gap: 8px; align-items: center; margin-left: 2px; flex: 0 0 auto; flex-wrap: nowrap; }
.topbar .lamp { font-size: 9px; letter-spacing: .06em; white-space: nowrap; }
.topbar .lamp-dome { width: 10px; height: 10px; }
.topbar-group { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: nowrap; }
/*
 * Nothing on this bar wraps, and the two things that identify the watch — the
 * works plate and the mission's name — are the two things that stay. The plate
 * used to be dropped entirely below 1400px, which left FIRST LIGHT five pixels
 * from the window's edge with no gutter at all; the name used to ellipsise to
 * "WEASEL H…" the moment the ARM lamp grew a countdown. The name reserves its
 * own width now, and what gives instead is the readouts between them, which
 * are figures with labels and can afford to be tight.
 */
.topbar-group:first-child > * { flex: 0 0 auto; }
.topbar-group:first-child > .badge { flex: 0 1 auto; min-width: 11em; }
@media (max-width: 1400px) {
  .topbar { gap: 4px 8px; padding: 5px 10px; }
  .topbar-group:first-child > .badge { min-width: 9.5em; }
  .topbar .readout label { display: none; }
  #unit-plate { font-size: 7px; padding: 2px 4px; }
}
/* Below 1400 the readouts themselves start to go, youngest first, rather than
   anything on the bar abbreviating itself into a code: the standing figure
   is the first to go, because at 1280 the card reader was drawn across the
   master lamps for want of its ninety pixels, and the standing is on the
   file and the debrief besides. The rack's caps give up a little padding
   too. Measured at 1280: the reader now starts thirty pixels clear of the
   last lamp. */
@media (max-width: 1400px) {
  .topbar .readout.standing { display: none; }
  .topbar .rack .pb { padding: 0 18px 0 8px; }
}

.badge {
  color: var(--engrave); letter-spacing: .13em; text-transform: uppercase;
  font-weight: 700; font-size: 12px;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .55);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/*
 * The card reader, and the identity card standing out of it.
 *
 * The player's reference was a photograph: a white smart card pushed halfway
 * into a black desktop reader with a row of status lamps on top. So this is a
 * reader — the top of the box seen from a little above, a trapezoid with the
 * lamps and an emblem on it, and the front face with the slot mouth cut into
 * it — and a card, white plastic on every theme because it is an object and
 * not a display, standing out of the slot and leaning toward the operator far
 * enough to read as a card and not so far that its print cannot be read. The
 * reader sets the bar's row height; the tilt is on the card alone, so nothing
 * beside it moves.
 */
/*
 * The reader is mounted in the desk lip, not in the top strip.
 *
 * The player: "the card/card reader on the user console should be put
 * somewhere that makes more visual sense rather than just jammed into the top
 * of the display." A desktop reader belongs on the desk, and the opening beat
 * has been showing one there all along — so it sits on the lip below the
 * panels, under the left of the tube, with the card standing out of the slot
 * toward the operator. It is laid out by the ticker's grid, so it takes the
 * middle column and the event log keeps its own; nothing is covered.
 */
.reader { position: relative; width: 196px; height: 78px; flex: 0 0 auto; align-self: end; margin: 0 14px 6px 0; }
.reader::after {
  /* the machined lip the reader is sunk into */
  content: ''; position: absolute; left: -10px; right: -10px; bottom: -6px; height: 12px;
  background: linear-gradient(180deg, #2b2f2b, #14171400);
  border-top: 1px solid var(--metal-edge);
}
.reader-top {
  position: absolute; left: 0; right: 0; top: 30px; height: 16px;
  background: linear-gradient(180deg, #343434 0%, #222 55%, #191919 100%);
  clip-path: polygon(7% 0, 93% 0, 100% 100%, 0 100%);
}
.reader-top::before {
  content: ''; position: absolute; left: 12%; right: 12%; top: 3px; bottom: 2px;
  border: 1px solid rgba(255, 255, 255, .07); border-radius: 2px;
}
.reader-emblem {
  position: absolute; left: 10px; top: 3px; width: 11px; height: 11px; margin-left: -5.5px;
  border-radius: 50%; z-index: 2;
  background: radial-gradient(circle at 40% 35%, #4a4a4a, #0d0d0d 75%);
  box-shadow: inset 0 0 0 1px #050505, 0 0 0 1px #3a3a3a;
}
.reader-emblem::after {
  content: ''; position: absolute; left: 3px; top: 5px; width: 5px; height: 1.5px;
  background: #2e2e2e; transform: rotate(-20deg);
}
.reader-leds { position: absolute; right: 7px; top: 5px; display: flex; gap: 6px; z-index: 2; }
.led { width: 5px; height: 5px; border-radius: 50%; background: #262626; box-shadow: inset 0 0 1px 1px #000; }
.led.is-blue.is-lit { background: #5a92ff; box-shadow: 0 0 5px 1px rgba(90, 146, 255, .9); }
.led.is-green.is-lit { background: #45e874; box-shadow: 0 0 5px 1px rgba(69, 232, 116, .9); }
.led.is-amber.is-lit { background: #ffb43c; box-shadow: 0 0 5px 1px rgba(255, 180, 60, .9); }
.reader-front {
  position: absolute; left: 0; right: 0; top: 46px; height: 12px; z-index: 2;
  background: linear-gradient(180deg, #262626, #0e0e0e); border-radius: 0 0 4px 4px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, .7), inset 0 1px 0 rgba(255, 255, 255, .12);
}
.reader-slot {
  position: absolute; left: 36px; right: 36px; top: 4px; height: 4px; background: #000;
  border-radius: 1px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 1), 0 1px 0 rgba(255, 255, 255, .12);
}
.idcard {
  position: absolute; left: 34px; top: 0; width: 130px; height: 54px; z-index: 1;
  transform-origin: 50% 100%; transform: perspective(320px) rotateX(-24deg) rotate(-1.4deg);
  background: linear-gradient(120deg, #fbfaf6 0%, #f2f0e9 45%, #e7e4db 100%);
  color: #17191c; border-radius: 4px; overflow: hidden;
  font-family: var(--font-mono); text-shadow: none;
  box-shadow: 0 8px 12px rgba(0, 0, 0, .6), inset 0 0 0 1px rgba(0, 0, 0, .12), inset 0 -10px 14px rgba(0, 0, 0, .07);
}
.idcard[hidden] { display: none; }
/* The laminate: one sheen across the face. */
.idcard::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, .5) 47%, rgba(255, 255, 255, 0) 60%);
}
.idc-seal { position: absolute; left: 34px; top: 8px; width: 40px; height: 40px; border-radius: 50%; border: 2px double rgba(40, 60, 120, .16); }
.idc-seal::after { content: ''; position: absolute; inset: 8px; border-radius: 50%; border: 1px solid rgba(40, 60, 120, .16); }
.idc-band {
  position: absolute; left: 0; right: 0; top: 0; height: 13px; background: #2a3a6a; color: #fff;
  font-size: 5px; letter-spacing: .1em; display: flex; align-items: flex-end; gap: 4px; padding: 0 5px 2px;
  white-space: nowrap; overflow: hidden;
}
.idc-emblem { width: 6px; height: 6px; border-radius: 50%; border: 1px solid #fff; box-sizing: border-box; margin-bottom: 1px; flex: 0 0 auto; }
.idc-name { position: absolute; left: 6px; top: 15px; font-size: 8.5px; font-weight: 700; letter-spacing: .04em; white-space: nowrap; }
.idc-fields { position: absolute; left: 6px; top: 26px; display: flex; flex-direction: column; gap: 1.5px; }
.idc-f { display: flex; flex-direction: column; line-height: 1; }
.idc-f label { font-size: 4.5px; color: #7a7d84; letter-spacing: .06em; }
.idc-f b { font-size: 6.5px; font-weight: 700; display: flex; align-items: center; gap: 3px; white-space: nowrap; }
.idc-f b .rank-board { width: 7px; height: 10px; }
.idc-no { position: absolute; left: 6px; bottom: 3px; font-size: 5px; color: #4a4d55; letter-spacing: .06em; white-space: nowrap; }
.idc-photo {
  position: absolute; right: 6px; top: 16px; width: 26px; height: 32px;
  image-rendering: pixelated; border: 1px solid rgba(0, 0, 0, .25); background: #888;
}
/* A rank's board, wherever the rank is written on the paperwork. */
.rank-board { display: inline-block; vertical-align: middle; }
.rank-badge { display: inline-flex; align-items: center; gap: 7px; }
.rank-badge b { font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-bright); }
h3 .rank-board, .score-cell .rank-board, p .rank-board, td .rank-board { vertical-align: -4px; margin-right: 4px; }
.file-ident { display: flex; align-items: flex-start; gap: 18px; padding: 4px 190px 10px 0; }
/*
 * The photograph on the file is a PRINT, stuck to a form: a white border, a
 * grey edge, a drop shadow, one corner clipped by the guillotine and a dry
 * stamp across it. It was a 48 x 60 canvas with a hairline round it.
 */
.file-photo {
  width: 96px; height: 120px; image-rendering: pixelated; flex: 0 0 auto;
  border: 3px solid #f4efe2;
  outline: 1px solid #8d938a;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, .5);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
}
.file-ident-photo { position: relative; flex: 0 0 auto; line-height: 0; }
.file-photo.is-small { width: 48px; height: 60px; border-width: 2px; }
/*
 * A REGISTRY CORNER MARK, not a ring.
 *
 * The reader judge, at every size the print appears at: "a dark green disc
 * overlaps the right shoulder of the portrait in every size, with nothing in
 * the picture to explain it: at dossier size it reads as a lens or a blot, at
 * end-card size the whole face is an unreadable smudge with a coloured lump on
 * it." A circle over a photograph reads as a lens whatever it is meant to be.
 * What a registry actually leaves is an embossed corner: two short rules, dark
 * on one side and light on the other, pressed into the print's bottom corner.
 */
.file-ident-photo::after {
  content: ''; position: absolute; right: 7px; bottom: 7px;
  width: 15px; height: 15px; border-radius: 0;
  border: 0;
  border-right: 2px solid rgba(240, 244, 232, .42);
  border-bottom: 2px solid rgba(240, 244, 232, .42);
  box-shadow: 1px 1px 0 rgba(10, 13, 10, .5);
  opacity: .9; pointer-events: none;
}
.record-head .file-ident-photo::after { width: 9px; height: 9px; right: 4px; bottom: 4px; }
.screen-inner.is-paper .file-ident-photo::after {
  border-right-color: rgba(35, 38, 31, .4);
  border-bottom-color: rgba(35, 38, 31, .4);
  box-shadow: 1px 1px 0 rgba(244, 239, 226, .7);
}
.file-ident-text { display: flex; flex-direction: column; gap: 5px; }
.file-no { font-size: 9px; letter-spacing: .12em; color: var(--ink-dim); }

/*
 * The rack: one cap, six times.
 *
 * Six controls in four hundred pixels used to be built to four different
 * treatments — a 62×27 cap with a chip, three 62×37 two-line caps with a chip
 * in a different place, a 61×27 cap, and LEAVE POST with no housing at all.
 * One height, one chip corner, one bevel. The speed caps are wider than the
 * word caps because a segmented selector reads as one instrument, and that is
 * the only difference between them.
 */
.topbar .rack { gap: 10px; }
.topbar .rack .pb { height: 28px; padding: 0 22px 0 8px; display: flex; align-items: center; }
.topbar .rack .pb .lg { align-items: flex-start; }
.topbar .rack .pb .lg b { font-size: 11.5px; letter-spacing: .07em; }
/* Four caps of one width, set by the widest legend in the group rather than by
   a number somebody guessed — HOLD used to be cut to "HOL" by a 48px cap. */
#speed-group { display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; }
#speed-group .pb { min-width: 0; justify-content: flex-start; }
#speed-group .pb .lg b { font-size: 12.5px; letter-spacing: .04em; }

.readout { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.readout label { color: var(--engrave-dim); font-size: 8px; letter-spacing: .1em; }
.readout b {
  color: var(--engrave); font-variant-numeric: tabular-nums; font-size: 13px;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .5);
}
/* Expenditure past the allocation is something sector command will ask about. */
.readout.is-over b { color: var(--warn); }

.standing-bar {
  display: inline-block; width: 62px; height: 7px; margin-left: 4px;
  background: var(--paper); border: 1px solid var(--metal-edge); border-radius: 1px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .8);
  overflow: hidden; vertical-align: middle;
}
.standing-bar i { display: block; height: 100%; background: var(--accent); transition: width .4s, background .4s; }
.standing.is-low .standing-bar i { background: var(--hostile); }
.standing.is-mid .standing-bar i { background: var(--warn); }

.btn {
  background: var(--bg-raised);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: 3px 9px;
  cursor: pointer;
  letter-spacing: .08em;
  transition: background .12s, border-color .12s, color .12s;
}
.btn:hover { border-color: var(--accent); color: var(--ink-bright); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.btn.is-danger { border-color: var(--hostile); color: var(--hostile); }
/* Dead, not dim: the fill flattens into the panel and the ink goes grey. */
.btn:disabled {
  cursor: not-allowed; opacity: .75;
  background: transparent; border-style: dashed;
  border-color: color-mix(in srgb, var(--edge) 60%, transparent); color: var(--ink-dim);
}
.btn:disabled:hover { border-color: color-mix(in srgb, var(--edge) 60%, transparent); color: var(--ink-dim); }

/* ----------------------------------------------------------------- stage */

.stage {
  display: grid;
  grid-template-columns: minmax(210px, 300px) minmax(0, 1fr) minmax(230px, 320px);
  min-height: 0;
  overflow: hidden;
}

.panel {
  display: flex; flex-direction: column; min-height: 0;
  overflow-x: hidden;
  /* The command net floats over the bottom of the stage; panels.js hands its
     height down here so no control ever sits underneath it. */
  padding-bottom: var(--net-h, 0px);
  border-right: 1px solid var(--metal-edge);
  box-shadow: inset -3px 0 8px rgba(0, 0, 0, .35);
}
.panel-right { border-right: none; border-left: 1px solid var(--metal-edge);
  box-shadow: inset 3px 0 8px rgba(0, 0, 0, .35); }

.panel h2 {
  margin: 0; padding: 5px 9px;
  border-bottom: 1px solid var(--metal-edge);
  background: linear-gradient(180deg, rgba(0,0,0,.25), transparent);
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
}
.panel h2 .lg { align-items: flex-start; }
.panel h2 .lg b { font-size: 10.5px; }
.hint { font-size: 8.5px; letter-spacing: .1em; color: var(--engrave-dim); }
.hint.is-bad { color: var(--hostile); animation: blink 1s infinite steps(1); }

/* ------------------------------------------------------------ track list */

/*
 * The head and the rows are one grid in one box. ALT carries five figures at
 * twenty-five thousand metres and had 2.8em to do it in, so 10800 overflowed
 * its cell by two pixels on every watch that flies high; the head also sat
 * three pixels right of the rows it labels, because it padded 14px against
 * their 8px plus a three-pixel selection rail.
 */
.track-head, .track-row {
  display: grid;
  /* Wide enough for the heads to be words: TRACK, TYPE, BRG°, RANGE, HEIGHT, BATTERY. */
  grid-template-columns: 3.7em 5.6em 2.8em 3.4em 4.4em 1fr;
  gap: 3px; padding: 2px 8px;
  border-left: 3px solid transparent;
  /* 15 + 4 + 1 = the 20px pitch the empty board is ruled at. */
  font-size: 11px; line-height: 15px; font-variant-numeric: tabular-nums;
}
.track-head {
  color: var(--engrave-dim); font-size: 8.5px; letter-spacing: .1em;
  padding-top: 5px; padding-bottom: 3px;
  /* The list below is inset by its own 5px margin and 1px border; the head
     has to stand off by the same, or the columns do not line up under it. */
  margin: 0 6px;
}

/*
 * The list is as tall as the board, not as tall as the panel.
 *
 * It was `flex: 1`, so four contacts sat in a 687 px well of flat black —
 * ninety per cent of the column, with no rule, no empty state and nothing
 * underneath. `flex: 1 1 auto` only half fixed it: the basis was the content
 * but the list still GREW into whatever the column had spare, so at 1920 four
 * rows still sat above two hundred and eighty pixels of empty box with THE
 * SECTOR floated to the bottom of it. It takes exactly the height its rows
 * need, shrinks and scrolls when there are more of them than the column has,
 * and everything below it rides up under the last row.
 */
.track-list {
  list-style: none; margin: 5px; padding: 2px 0; overflow-y: auto;
  flex: 0 1 auto; min-height: 60px;
  /* Ruled, at the pitch of a row. An empty board is a board with empty
     slots on it; it was a flat black well, and with four contacts on a
     watch that is most of the column. */
  background:
    repeating-linear-gradient(180deg, transparent 0 19px, rgba(255, 255, 255, .085) 19px 20px),
    linear-gradient(180deg, transparent, rgba(0,0,0,.45)), var(--bg);
  border: 1px solid var(--metal-edge); border-radius: 2px;
  box-shadow: inset 0 2px 8px rgba(0, 0, 0, .7);
}

/*
 * The state of the board, at the foot of the panel the board is on.
 *
 * Six figures the operator otherwise has to assemble by reading four battery
 * cards one at a time — how much is out there, how much of it nobody has
 * taken, how much is being shot at, and how much of your own kit can still
 * fire. See renderBoardState in panels.js.
 */
.board-state {
  margin: 7px 5px 0; padding: 7px 8px 8px;
  border: 1px solid var(--metal-edge); border-radius: 2px;
  background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(0,0,0,.18));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06);
}
.bs-head {
  font-size: 8.5px; letter-spacing: .2em; color: var(--engrave-dim);
  padding-bottom: 5px; margin-bottom: 6px;
  border-bottom: 1px solid var(--metal-edge);
}
.bs-figures { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px 6px; }
.bs-figure { display: block; min-width: 0; }
.bs-figure label {
  display: block; font-size: 7.5px; letter-spacing: .09em; color: var(--engrave-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bs-figure b {
  font-size: 16px; color: var(--engrave); font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .5);
}
.bs-figure.is-bad b { color: var(--hostile); }
.bs-figure.is-warn b { color: var(--warn); }
.bs-figure.is-good b { color: var(--good); }
.bs-note {
  margin-top: 7px; padding-top: 5px; border-top: 1px solid rgba(0, 0, 0, .3);
  font-size: 9.5px; letter-spacing: .04em; color: var(--engrave-dim);
}
.bs-head.is-second { margin-top: 10px; }
/* What is being defended and whether it is still there — the thing the watch
   is scored on, which the console never showed until the debrief. */
.bs-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  font-size: 10px; letter-spacing: .05em; padding: 2px 0;
  border-bottom: 1px solid rgba(0, 0, 0, .28);
}
.bs-row span { color: var(--engrave-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bs-row b { color: var(--good); font-weight: 400; white-space: nowrap; }
.bs-row.is-hurt b { color: var(--warn); }
.bs-row.is-lost b { color: var(--hostile); }
.bs-row.is-lost span { color: var(--hostile); }
.track-row > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track-row {
  cursor: pointer; align-items: center;
  border-bottom: 1px solid color-mix(in srgb, var(--edge) 35%, transparent);
}
.track-row:hover { background: var(--bg-raised); }
.track-row.is-selected { background: var(--bg-raised); border-left-color: var(--accent); }
.track-row.is-hostile .tn { color: var(--hostile); }
.track-row.is-friendly .tn { color: var(--friendly); }
.track-row.is-unknown .tn { color: var(--unknown); }
.track-row.is-coasting { opacity: .55; font-style: italic; }
.track-row.is-engaged .asgn { color: var(--good); }
/* A target the net called to you, as against one you picked yourself. */
.track-row .asgn .cued { color: var(--accent); font-style: normal; margin-right: 2px; }
.unit-type.store.is-empty, .unit-type.store.is-empty b { color: var(--hostile); }
.track-row .asgn .own-call { color: var(--warn); font-style: normal; font-size: 8px; letter-spacing: .08em; margin-right: 3px; vertical-align: 1px; }
.chan.is-own { box-shadow: inset 2px 0 0 var(--warn); }
.track-row .pips { color: var(--hostile); font-style: normal; }
/* "no contacts" is a sentence, not a row of columns: it had been landing in
   the five-em TYPE cell and being clipped in half. */
.track-row.is-empty { opacity: .5; cursor: default; display: block; }
.track-row.is-empty:hover { background: none; }

/* Shootlist headings: the battery a run of rows belongs to, and whether it
   can take another one. Sticky so the battery stays named while you scroll
   its list. */
.shootlist-head {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: flex; justify-content: space-between; align-items: baseline; gap: 6px;
  padding: 4px 8px 2px; margin-top: 3px;
  font-size: 8.5px; letter-spacing: .11em;
  color: var(--engrave-dim);
  background: linear-gradient(180deg, var(--bg-raised), color-mix(in srgb, var(--bg-raised) 70%, transparent));
  border-top: 1px solid var(--metal-edge);
  border-bottom: 1px solid color-mix(in srgb, var(--edge) 55%, transparent);
}
.shootlist-head:first-child { margin-top: 0; border-top: 0; }
.shootlist-head .sl-name { color: var(--engrave); font-weight: 700; }
.shootlist-head .sl-note { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.shootlist-head.is-urgent .sl-name, .shootlist-head.is-urgent .sl-note { color: var(--hostile); }
.shootlist-head.is-busy .sl-note { color: var(--warn); }
.shootlist-head.is-down .sl-note { color: var(--hostile); }
/*
 * The two closing headings — UNCOMMITTED and OUT OF REACH — carry a list
 * rather than a status, and the list runs to eleven battery names at district
 * command. Truncating it to "BASTION DISTRICT · LANCE VILLE · THISTLE…" hid
 * two hundred and forty-four pixels of the answer; it wraps instead.
 */
.shootlist-head.is-idle { opacity: .6; align-items: flex-start; }
.shootlist-head.is-idle .sl-note {
  white-space: normal; overflow: visible; text-align: right; line-height: 1.35;
}

/*
 * What the selected contact is doing: the readout at the foot of the picture
 * panel, where the cabin keeps its works plates. The instruments group at the
 * top of the column, one continuous surface of painted metal runs under them,
 * and the panel closes on its own bottom readout — which is what both columns
 * now do, rather than one of them being a box of black.
 */
.panel-foot {
  margin-top: auto;
  border-top: 1px solid var(--metal-edge); padding: 6px 8px;
  font-size: 10.5px; color: var(--engrave-dim); min-height: 62px; line-height: 1.55;
}
.panel-foot b { color: var(--engrave); }
.panel-foot em { color: var(--warn); }

/* --------------------------------------------------------------- weapons */

/*
 * The weapons rack. In the cabin the crew console below it is the seat the
 * player is actually sitting in, so the rack gives way: it takes a third of
 * the column and scrolls, and the cabin takes the rest. Measured before this,
 * at 1280x800 on The Two Cities, LOCK and the LAUNCH cap were rendered below
 * the bottom of the panel with no scrollbar anywhere that could reach them.
 */
/* The rack runs under the bottom edge of the panel rather than stopping at it:
   a shadow cast up from the edge says the column continues, which a clipped
   card with a cap half under the rail otherwise does not. */
.battery-list {
  overflow-x: hidden; overflow-y: auto; flex: 1 1 auto; min-height: 0;
  box-shadow: inset 0 -14px 12px -12px rgba(0, 0, 0, .8);
  /* A drawn scrollbar, not the overlay one Chromium gives a panel by default
     (measured: a zero-pixel gutter), because a list that hides half the net
     behind an invisible scrollbar is how a battery goes unnoticed. */
  scrollbar-width: thin; scrollbar-color: var(--metal-hi) var(--well);
}
.battery-list::-webkit-scrollbar { width: 7px; }
.battery-list::-webkit-scrollbar-track { background: var(--well); }
.battery-list::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, var(--metal-hi), var(--metal-lo));
  border: 1px solid var(--metal-edge); border-radius: 1px;
}

.pill {
  display: inline-block; padding: 0 5px; border-radius: 999px; font-size: 9.5px;
  border: 1px solid currentColor; letter-spacing: .08em;
}
.pill.tight { color: var(--warn); }
.pill.free { color: var(--good); }

/*
 * In the cabin both of the panels above give way to the seat itself.
 *
 * They were capped at 30% each, which on a 1280x800 laptop left the console
 * 282 px of a 512 px card and put LOCK below the bottom of the screen. In the
 * crew seat the rack and the formation panel are reference material — the
 * batteries that are not yours, and the standing orders you are fighting
 * under — and reference material does not get to push the seat's own controls
 * off the panel. They keep a scrollbar of their own.
 */
/*
 * In the cabin the rack hugs its rows and the seat takes the rest of the
 * column. Rows are on a 32px pitch and the cap is a whole number of them, so
 * a cut lands between rows and never through a name; the list says in as many
 * words where it ends, rather than trailing off into painted steel.
 */
.battery-list.is-secondary { flex: 0 0 auto; max-height: 128px; min-height: 0; }
/* On a laptop the column is the seat's, and the reference rack gets two strips
   and the word that says how many more there are. Measured at 1280x800 on the
   two-city watch: with the rack at 96 px and the orders panel at 65 the cabin's
   readout block was squeezed to ZERO — the target, the envelope, the kill
   estimate and both ELINT gauges rendered into a nought-pixel scroller. */
@media (max-height: 860px) {
  .battery-list.is-secondary { max-height: 64px; }
  .formation-list.is-secondary { max-height: 48px; min-height: 0; }
}
.rack-empty {
  padding: 10px 8px; font-size: 9px; letter-spacing: .14em;
  color: var(--engrave-dim); opacity: .75;
}

/*
 * The rest of the net in the cabin: one strip per set, all the same height.
 *
 * The rack is a scroller, so as full cards its first card was sliced straight
 * through the middle of its ELINT EXPOSURE row by the console's top edge and
 * the cards under it were out of sight entirely. On one pitch a cut can only
 * land between rows, so nothing is ever cut through a line of text, and four
 * or five units fit in the height one card used to take.
 */
.rack-strip {
  display: flex; align-items: center; gap: 6px; overflow: hidden;
  height: 32px; padding: 0 7px 0 8px;
  border-bottom: 1px solid var(--metal-edge);
  font-size: 10px; font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, rgba(255, 255, 255, .05), rgba(0, 0, 0, .12));
}
.rack-strip.is-dead { opacity: .5; }
.rs-name {
  flex: 1 1 auto; min-width: 0; color: var(--engrave);
  letter-spacing: .08em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rs-arm {
  flex: 0 0 auto; font-size: 8.5px; letter-spacing: .1em;
  color: var(--hostile); border: 1px solid currentColor; padding: 0 3px;
  animation: lamp-blink 1s infinite steps(1);
}
.rs-gauge { flex: 0 0 30px; min-width: 30px; }
.rs-fig { flex: 0 0 auto; color: var(--engrave-dim); font-size: 9px; letter-spacing: .06em; }
/* The same bat-handle as everywhere else — up is RADIATE, down is SILENCE,
   both engraved, one press to throw — at the size a 32px strip allows. */
.rack-strip .sw { flex: 0 0 auto; padding: 2px 4px; gap: 4px; }
.rack-strip .sw-legends { gap: 0; }
.rack-strip .sw-body { width: 11px; height: 22px; }
.rack-strip .sw-lever { height: 8px; width: 5px; margin-left: -2.5px; }
.rack-strip .sw[aria-pressed="true"] .sw-lever { top: 2px; }
.rack-strip .sw[aria-pressed="false"] .sw-lever { top: 11px; }
.rack-strip .sw-pos { font-size: 8px; line-height: 11px; padding: 0 3px; }

/* -------------------------------------------------------- crew console */

/*
 * The cabin: a scrolling instrument block over a command pad that never moves.
 *
 * What was here was one scroller holding everything, which is how the seat
 * came to render its own primary controls off the bottom of the panel. At
 * 1280x800 in the crew seat the console was 391 px of window onto 450 px of
 * card, with no scrollbar drawn and nothing on screen to say the seat
 * continued: the LOCK / SILENCE / RELOAD row was bisected by the panel edge
 * and the DISPLACE cap — the survival control — was 37 px below it, invisible.
 * Scrolling down to reach it took the battery's name, its lamps and its
 * envelope readout off the top, so the operator could see the state or reach
 * the control and never both.
 *
 * The command pad is out of the scroller. Whatever the readouts do, LOCK, the
 * launch cap, the emissions switch, RELOAD and DISPLACE are in the same
 * rectangle at the foot of the panel at every supported size.
 */
.crew-console {
  border-top: 2px solid var(--metal-edge); padding: 0;
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  background: linear-gradient(180deg, color-mix(in srgb, var(--metal-hi) 22%, var(--bg-panel)), var(--bg-panel) 90px);
}
/*
 * One grid, one type scale, one row pitch — the cabin's whole measure.
 *
 * A blind pass over the card counted ten type sizes (8, 8.5, 9, 9.5, 10, 10.5,
 * 11, 12, 12.5 and 15 px) and seventeen distinct letter-spacings inside one
 * 320 px column; the readout rows stepped 20, 20, 22, 15 and 26 px; and the
 * text columns started at x = 10, 20, 26, 52, 103, 104, 177, 180 and 210, none
 * of which was a column of anything else. A panel is a grid or it is a pile.
 *
 *   --cab-row    every readout row, exactly, and the scroller's cut pitch
 *   --cab-gut    the card's one side gutter: every left origin and every
 *                right-aligned figure lands on it, the banner included
 *   --cab-mark   the marker column — lamp domes, channel numbers
 *   --cab-tn     the track-number column
 *   --cab-gap    the one gutter between the card's two columns, used by the
 *                lamp pairs and by the command pad, so they share an axis
 *
 * Four sizes: --fs-plate for stamped sub-legends, --fs-legend for engraved
 * row labels, --fs-value for everything a figure is written in, --fs-head for
 * the card's name and its condition strip. Two trackings: wide for engraved
 * lettering, tight for figures.
 */
.crew-console .unit.cabin {
  --cab-row: 20px;
  --cab-gut: 8px;
  --cab-mark: 26px;
  --cab-tn: 48px;
  --cab-gap: 6px;
  --fs-plate: 8.5px;
  --fs-legend: 9.5px;
  --fs-value: 11px;
  --fs-head: 13px;
  /* Absolute, not em: two trackings on the card means two numbers on the
     glass. In em they resolve to one value per type size, which is how a
     column with four sizes came to be measured at seventeen of them. */
  --tr-wide: 1.3px;
  --tr-tight: .5px;
  margin: 0; height: 100%; min-height: 0;
  padding: 6px var(--cab-gut) 8px;
  display: flex; flex-direction: column; gap: 0;
  border-left: 2px solid color-mix(in srgb, var(--accent) 34%, var(--metal-edge));
}
/* The seat says whose it is. It is not another card in the rack. */
/* One line. Wrapping, the plate dropped under the battery's name and the head
   took two rows of a column whose readouts had none to spare. */
.cabin-head {
  flex: 0 0 auto;
  display: flex; align-items: baseline; gap: 7px; flex-wrap: nowrap;
  height: calc(var(--cab-row) + 3px);
  border-bottom: 1px solid var(--metal-edge);
}
.cabin-head .unit-name, .cabin-head .unit-type {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.cabin-station {
  flex: 0 0 auto; white-space: nowrap;
  font-size: var(--fs-plate); letter-spacing: var(--tr-wide); color: var(--bg-panel);
  background: var(--accent); padding: 1px 5px; border-radius: 1px; font-weight: 700;
}
.cabin-head .unit-name { font-size: var(--fs-head); letter-spacing: var(--tr-wide); }
/* The battery's name is the one thing on this line that may not be cut; the
   nomenclature plate beside it gives its width up first. */
.cabin-head .unit-name { flex: 0 1 auto; }
.cabin-head .unit-type {
  flex: 1 1 0; min-width: 0; text-align: right;
  font-size: var(--fs-plate); letter-spacing: var(--tr-tight);
}

/*
 * The readouts scroll; the pad below them does not. A scroller with nothing to
 * say that it scrolls is how a control ends up unreachable, so this one fades
 * into its own cut and lights a strip under it saying there is more.
 *
 * No right padding: with one, the readout rows ended three pixels short of the
 * cap row below them and the column had two right edges.
 */
.cabin-body {
  flex: 0 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column;
  scrollbar-width: thin; scrollbar-color: var(--metal-hi) var(--well);
}
/*
 * Rows do not compress; the body scrolls instead.
 *
 * Without this every readout in the block shrank towards zero on a short
 * panel and the glyphs of OUT OF ZONE printed through the CHANNELS header
 * above it. A row is the height a row is, and when they no longer fit the
 * scroller says so.
 *
 * Every child is exactly one row, which is what lets the renderer floor the
 * scroller's height to the row pitch and put the cut between rows. There is no
 * fade: a gradient over an arbitrary cut greys out half a line of type, which
 * is what it was doing to REACH on the two-set watch. A whole row, a hard
 * edge, a drawn scrollbar and a lit chevron under it.
 */
.cabin-body > * { flex: 0 0 auto; height: var(--cab-row); min-height: var(--cab-row); }
/*
 * The cue has a strip of its own at the foot of the scroller.
 *
 * It used to share the advisory line with the one instruction the panel gives
 * — 'NO TARGET — CLICK A CONTACT ON THE TUBE OR THE SHOOTLIST' — in a slot
 * fifty-six pixels too narrow for it, so the instruction ended in an ellipsis
 * at both reference widths and the cue was crammed against it. Its own strip,
 * full width, centred, and only there when there is something below the cut.
 */
/*
 * The strip is always there and only sometimes lit.
 *
 * Appearing with the overflow it caused, it took thirteen pixels off the
 * scroller the moment the scroller ran out of room — which made the scroller
 * overflow harder, which kept the strip: a card that resized itself in a loop,
 * and a cut that never settled on a row boundary.
 */
.cabin-more {
  flex: 0 0 auto; height: 13px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  border-top: 1px solid transparent;
}
.cabin-body.is-overflowing + .cabin-more {
  border-top-color: color-mix(in srgb, var(--accent) 30%, transparent);
}
.cabin-more em {
  font-style: normal; font-size: var(--fs-plate); letter-spacing: var(--tr-wide);
  color: var(--accent); visibility: hidden;
}
.cabin-body.is-overflowing + .cabin-more em { visibility: visible; }
.cabin-body::-webkit-scrollbar { width: 7px; }
.cabin-body::-webkit-scrollbar-track { background: var(--well); }
.cabin-body::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, var(--metal-hi), var(--metal-lo));
  border: 1px solid var(--metal-edge); border-radius: 1px;
}
/*
 * The annunciator block: four seated lamps on a fixed 2x2 pitch.
 *
 * As a wrapping flex row the fourth lamp dropped to a line of its own at some
 * widths and not at others, so the block that is the console's primary status
 * changed height with the window. Two by two at every width, on the same two
 * columns and the same gutter as the command pad at the foot of the card.
 *
 * And they are lamps, not dots. For most of a watch all four are dark, and
 * dark they were four identical flat grey circles with a word beside them —
 * CSS, next to capped buttons and a bat-handle switch. Each one is now a
 * housing punched into the panel: a recessed plate, an engraved caption on it,
 * and a glass dome seated in a machined bezel that catches the light whether
 * or not there is a filament behind it.
 */
.cabin-lamps {
  flex: 0 0 auto;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px var(--cab-gap); margin: 5px 0 2px;
}
.cabin-lamps .lamp {
  display: grid; grid-template-columns: var(--cab-mark) minmax(0, 1fr) auto;
  align-items: center; gap: 0; min-width: 0; height: 18px;
  padding-right: 4px;
  grid-template-areas: none;
  border: 1px solid var(--metal-edge); border-radius: 2px;
  background: linear-gradient(180deg, rgba(0, 0, 0, .30), rgba(255, 255, 255, .04));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 1px 0 rgba(255, 255, 255, .04);
  font-size: var(--fs-legend); letter-spacing: var(--tr-tight);
}
.cabin-lamps .lamp-cap {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .65);
}
/* A lamp's own figure: the seconds on an ARM warning, in its own column so
   the engraved legend beside it is never the thing that gets truncated. */
.cabin-lamps .lamp-fig {
  font-size: var(--fs-plate); letter-spacing: var(--tr-tight);
  font-variant-numeric: tabular-nums; color: inherit; padding-left: 4px;
}
/* The dome sits down in a machined bezel — two rings of plating and a shadow
   under the glass — rather than floating on the panel as a flat disc. */
.cabin-lamps .lamp-dome {
  justify-self: center; width: 10px; height: 10px;
  box-shadow:
    inset 0 0 3px rgba(0, 0, 0, .85),
    0 0 0 1px var(--metal-lo),
    0 0 0 2px var(--metal-edge),
    0 1px 2px rgba(0, 0, 0, .55);
}
.cabin-lamps .lamp.is-lit .lamp-dome {
  box-shadow:
    inset 0 0 3px rgba(255, 255, 255, .6),
    0 0 0 1px var(--metal-lo),
    0 0 0 2px var(--metal-edge),
    0 0 9px var(--lamp-colour, var(--accent));
}
/* Lit, the housing lights with the dome: a lamp behind glass throws onto its
   own plate, and it means the state survives a monochrome print. */
.cabin-lamps .lamp.is-lit {
  border-color: color-mix(in srgb, var(--lamp-colour, var(--accent)) 45%, var(--metal-edge));
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--lamp-colour, var(--accent)) 13%, transparent),
    rgba(0, 0, 0, .22));
}

/*
 * A readout row: what it is on the left, what it says on the right.
 *
 * Every figure is tabular, so a range counting down does not shuffle the
 * digits beside it, and every row is the same height whether or not it has
 * anything to report — the rows print an em dash rather than being left out,
 * because a row that appears when an engagement starts moves every control
 * under it at exactly the wrong moment.
 */
/*
 * Exactly one row pitch, with no padding of its own: the block used to step
 * 20, 20, 22, then 15 px into the exposure strip and 26 px out of it, so no
 * two readouts on the card sat on the same rhythm. Label left on the gutter,
 * figure right on the gutter, nothing in between.
 */
.crew-row {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  font-size: var(--fs-value); padding: 0; height: var(--cab-row);
  border-bottom: 1px solid rgba(0, 0, 0, .35);
}
.crew-row .lg { flex: 0 1 auto; min-width: 0; }
.crew-row .lg b {
  font-size: var(--fs-legend); letter-spacing: var(--tr-wide); color: var(--engrave-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.crew-row > b {
  flex: 0 0 auto; color: var(--engrave); font-variant-numeric: tabular-nums;
  font-size: var(--fs-value); letter-spacing: var(--tr-tight);
}
.crew-row.is-hot > b { color: var(--hostile); }
.crew-row.is-good > b { color: var(--good); }
.crew-row.is-dim > b { color: var(--engrave-dim); }
/* An exposure row: engraved name, bar, figure — on the row pitch like the
   rest, instead of a packed strip of its own height with its own two type
   sizes and its own two column origins. */
.crew-row.is-gauge .gauge { flex: 1 1 auto; min-width: 40px; margin: 0 2px; }
.crew-row.is-gauge > b { min-width: 3.1em; text-align: right; }

/*
 * The condition strip: IN ACTION, OUT OF ACTION, ANTENNAS GONE, DESTROYED.
 *
 * Said once, across the top of the card, above the scroller — a caution that
 * can be scrolled off the panel is not a caution, and it must not go grey with
 * the readouts it is explaining. Always drawn, so the controls beneath it do
 * not move the moment it has something to say.
 */
/*
 * Full-bleed, and its figure lands on the card's gutter like every other
 * right-aligned value.
 *
 * With six pixels of padding of its own inside a card padded by eight, ON THE
 * AIR sat sixteen pixels from the panel's edge while BASTION, 0/4, 98 km, 56%,
 * CLOSED UP, 6–120 km and MAGAZINE 28 all sat at nine — a seven-pixel ragged
 * right edge on the loudest line in the column. The negative margin takes the
 * strip out to the card's edges and the padding brings its lettering back to
 * the same gutter the rows use.
 */
.cabin-banner {
  position: relative; overflow: hidden; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin: 6px calc(var(--cab-gut) * -1) 2px; padding: 0 var(--cab-gut);
  height: var(--cab-row);
  border-top: 1px solid var(--metal-edge); border-bottom: 1px solid var(--metal-edge);
  color: var(--engrave-dim);
  background: linear-gradient(180deg, rgba(255, 255, 255, .05), rgba(0, 0, 0, .16));
}
.cabin-banner.is-warn { border-color: var(--warn); color: var(--warn);
  background: color-mix(in srgb, var(--warn) 14%, transparent); }
.cabin-banner.is-bad { border-color: var(--hostile); color: var(--hostile);
  background: color-mix(in srgb, var(--hostile) 16%, transparent); }
.cabin-banner .cond-name {
  flex: 1 1 auto; min-width: 0; font-weight: 700;
  font-size: var(--fs-legend); letter-spacing: var(--tr-wide); color: inherit;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The figure on the right only — scoped to the direct child, because as a
   descendant selector it also caught the stencil's own <b> and set a second
   size and a numeric variant on it. */
/*
 * The figure has a width of its own, so ON THE AIR and SILENT sit in the same
 * box: measured, the flip used to change the figure's box by 28 px and the
 * name's by the same, which on a strip whose one job is to hold still was
 * the strip moving.
 */
.cabin-banner > b {
  flex: 0 0 auto; min-width: 78px; text-align: right;
  font-variant-numeric: tabular-nums; font-size: var(--fs-value);
  letter-spacing: var(--tr-tight); font-weight: 700;
}
/* The clock's progress, as the banner's own underline. The card used to draw
   it as a free-standing stub of a rule under the rail lamps, ninety pixels
   long and captioned by nothing at all. */
.cabin-banner .banner-bar {
  position: absolute; left: 0; bottom: 0; height: 2px;
  background: currentColor; opacity: .65; transition: width .3s;
}

/*
 * The channel block: one row per fire-control channel, occupied or not, plus
 * the selection's own row at the head of it. Every row is always drawn,
 * because a block that grows a row when a channel opens moves everything
 * under it at the worst moment — and it is a well, not a list floating in
 * the card.
 *
 * The seat has more column than its readouts need at the reference size, and
 * the slack used to collect as a hundred-and-fifty-pixel unfilled band in the
 * middle of the panel between EST. KILL PROB and REACH — no frame, no label,
 * no rules: a rendering hole, and read as one. The channel table takes it
 * instead. It is the one block on the card that is a LIST — a battery holds
 * two channels or four, and the free ones are as much of the answer as the
 * busy ones — so it is drawn as a recessed instrument well, ruled at the row
 * pitch all the way down, and the empty part of it is obviously the empty
 * part of an instrument rather than a gap in the panel.
 */
.chan-block { flex: 0 0 auto; margin-top: 7px; display: flex; flex-direction: column; }
/*
 * The well is exactly as deep as the battery has channels.
 *
 * Ruled at a fixed six rows it left about a hundred and fifty pixels of empty
 * staves inside a bordered box on a two-channel battery — a rendering hole
 * with a frame round it — and the ruling made the emptiness look like rows
 * that had failed to draw. The rows rule themselves; the box hugs them; the
 * slack goes below the readouts as panel metal, where it reads as panel.
 */
.chan-rows {
  flex: 0 0 auto; margin-top: 3px; padding: 3px;
  border: 1px solid var(--metal-edge);
  background: var(--well);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .5);
}
.chan-head {
  display: flex; align-items: center; justify-content: space-between;
  height: 18px;
  border-top: 1px solid var(--metal-edge);
}
.chan-head .lg b {
  font-size: var(--fs-legend); letter-spacing: var(--tr-wide); color: var(--engrave-dim);
}
.chan-count {
  font-size: var(--fs-value); color: var(--engrave);
  font-variant-numeric: tabular-nums; letter-spacing: var(--tr-tight);
}
.chan {
  display: grid; grid-template-columns: var(--cab-mark) var(--cab-tn) 1fr auto;
  align-items: center; gap: var(--cab-gap); width: 100%; height: 19px;
  margin-bottom: 3px; padding: 0 5px; text-align: left;
  font: inherit; font-size: var(--fs-legend); letter-spacing: var(--tr-tight);
  border: 1px solid var(--metal-edge); border-radius: 1px;
  background: linear-gradient(180deg, var(--bg-raised), var(--well));
  color: var(--engrave);
}
.chan:last-child { margin-bottom: 0; }
/* The channel's number, with the caret marking the one the readouts describe. */
.chan-n {
  color: var(--engrave-dim); font-size: var(--fs-legend); text-align: center;
  letter-spacing: var(--tr-tight); border-right: 1px solid var(--metal-edge);
}
.chan-tn { font-weight: 700; letter-spacing: var(--tr-tight); }
.chan-state { color: var(--ink); }
.chan-fig {
  color: var(--engrave-dim); font-variant-numeric: tabular-nums;
  font-size: var(--fs-plate);
}
.chan.is-free { color: var(--engrave-dim); border-style: dashed; background: none; }
.chan.is-free .chan-tn, .chan.is-free .chan-state { color: var(--engrave-dim); font-weight: 400; }
.chan.is-live { cursor: pointer; }
.chan.is-live:hover { filter: brightness(1.15); }
.chan.is-guiding .chan-state { color: var(--warn); }
/* The channel the readouts below are about. */
.chan.is-focus {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}
.chan.is-focus .chan-n { color: var(--accent); }

/*
 * There is no HIGH VOLTAGE placard at the foot of the cabin any more.
 *
 * The card used to end in a two-column block of stamped nomenclature and a
 * HIGH VOLTAGE placard — about ninety pixels of decoration at the bottom of
 * the one column whose ELINT gauge and DISPLACE cap were being rendered below
 * the bottom of the panel. The scope bezel and the topbar carry the works
 * plates, four inches away and unclipped; the seat carries controls. The
 * battery's own nomenclature plate is still on the card's head, which is
 * where a plate saying what this equipment IS belongs.
 */

/*
 * The stampings, and the card's only flexible element.
 *
 * `flex: 1 1 0` — no base size at all, so it is exactly zero on a column that
 * has nothing to spare and exactly the slack on one that has. The plates sit
 * at the BOTTOM of it against the command rule, so what grows is the blank
 * field of painted steel above them, which is what a panel with room on it
 * looks like. Two hundred and forty pixels of unexplained black is not.
 */
.cabin-plates {
  flex: 1 1 0; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: flex-end; gap: 4px; padding-top: 6px;
}
/* Nothing at all unless the whole block fits — see renderCrewConsole. */
.cabin-plates > *, .cabin-plates::before { display: none; }
.cabin-plates.is-fitted > * { display: block; }
.cabin-plates .data-plate { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.cabin-plates .placard { align-self: stretch; }
/* The blank field is a louvre, because a cabinet with room in it has one.
   It takes whatever height is going and is nothing at all when there is
   none — so a short column loses the decoration and keeps every readout. */
.cabin-plates.is-fitted::before {
  content: ''; display: block; flex: 1 1 auto; min-height: 0; align-self: stretch;
  margin-bottom: 7px;
  border-top: 1px solid var(--metal-edge);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05);
  background: repeating-linear-gradient(180deg,
    rgba(0, 0, 0, .55) 0 3px,
    rgba(255, 255, 255, .07) 3px 4px,
    transparent 4px 11px);
}

/*
 * Four sizes and two trackings on this card, with no exceptions.
 *
 * The hardware builders in panel.css are shared with the rack, the topbar and
 * the thumb rail, and they bring their own type: a 15px launch face on .24em,
 * a 9px key chip, a 9px sub-legend, a 12px switch position. Inside the cabin
 * they are pulled onto the card's own scale, so a blind pass over this column
 * counts 8.5 / 9.5 / 11 / 13 and nothing else.
 */
.crew-console .unit.cabin .lg b,
.crew-console .unit.cabin .sw-pos {
  font-size: var(--fs-value); letter-spacing: var(--tr-tight);
}
.crew-console .unit.cabin .lg i,
.crew-console .unit.cabin .kc {
  font-size: var(--fs-plate); letter-spacing: var(--tr-tight);
}
/* The commit control is the one thing on the card set in the head size, and
   it earns it by being the only control the card is built around. */
.crew-console .unit.cabin .pb-fire .lg b {
  font-size: var(--fs-head); letter-spacing: var(--tr-wide);
}
.crew-console .unit.cabin .tubes i { letter-spacing: var(--tr-tight); }
/* Engraved lettering — row labels, block headings — keeps the wide tracking;
   figures and control faces keep the tight one. That is the whole rule. */
.crew-console .unit.cabin .crew-row .lg b,
.crew-console .unit.cabin .chan-head .lg b {
  font-size: var(--fs-legend); letter-spacing: var(--tr-wide);
}

/* ------------------------------------------------------- the command pad */

/*
 * The pad takes the slack, so the readouts above it can end where they end
 * and the caps are still in the same rectangle they were last time.
 */
.cabin-commands {
  flex: 0 0 auto; margin-top: 4px; padding-top: 5px;
  border-top: 2px solid var(--metal-edge);
  box-shadow: 0 -1px 0 color-mix(in srgb, var(--metal-hi) 40%, transparent);
}
/* One line, one voice, one height — whether or not it has anything to say. */
.cabin-advisory {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  height: 14px; font-size: var(--fs-legend); letter-spacing: var(--tr-tight);
  color: var(--engrave-dim);
}
/* One line, always: a caution that wrapped to two would move the pad under
   it, which is the whole thing this block exists not to do. */
.cabin-advisory > span {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cabin-advisory.is-warn { color: var(--warn); }
.cabin-advisory.is-bad { color: var(--hostile); }
.cabin-advisory.is-good { color: var(--good); }

/* Fixed at two lines of its own — the rail lamps and their figures, with the
   loaders' hairline under them — so a long loading word cannot grow the block
   and take a readout row off the panel above it. */
.cabin-ammo {
  display: flex; align-items: center; flex-wrap: wrap; gap: 1px 6px; margin-top: 2px;
  height: 28px; align-content: center;
  font-size: var(--fs-value); font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-tight);
}
.cabin-ammo b { color: var(--engrave); }
.ammo-label {
  font-size: var(--fs-plate); letter-spacing: var(--tr-wide); color: var(--engrave-dim);
}
.cabin-ammo .tubes { flex: 0 1 auto; }
/* The store's label and figure are one unbreakable item at the row's right. */
.ammo-mag { margin-left: auto; flex: 0 0 auto; white-space: nowrap; display: inline-flex; gap: 6px; align-items: baseline; }
/* What the loaders are doing, in the same row as what they have loaded. */
/* Inline, on the rails' own row: as a full-width line of its own it cost the
   readouts above another eight pixels to say LOADERS STOWED. */
.ammo-work {
  flex: 0 0 auto; font-size: var(--fs-plate); letter-spacing: var(--tr-wide);
  color: var(--engrave-dim); margin-left: 2px; flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cabin-ammo.is-warn .ammo-work { color: var(--warn); }
/* Nothing left in store: the count and the loaders' line say so in the
   colour the panel keeps for things that cannot be fixed from the seat. */
.cabin-ammo.is-empty .ammo-work, .cabin-ammo.is-empty .ammo-store { color: var(--hostile); }
.ammo-bar {
  flex: 1 0 100%; height: 3px; margin-top: 1px;
  background: var(--well); border: 1px solid var(--metal-edge);
}
.ammo-bar i { display: block; height: 100%; background: var(--metal-hi); transition: width .25s; }
.cabin-ammo.is-warn .ammo-bar i { background: var(--warn); }

/*
 * The pad itself: two columns on one pitch, in engagement order.
 * LOCK, then the launch cap across both columns, then the housekeeping row.
 */
/*
 * Two columns on a round pitch. At 1fr 1fr with a five-pixel gutter the
 * columns computed to 148.5 px, so the seam between LOCK and RELOAD — and
 * between the switch and DISPLACE — landed on a half pixel and their one-pixel
 * borders rendered soft. The gutter is even and the card's side padding is
 * even, so the two tracks are whole pixels on any even-width panel.
 */
.cabin-pad {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--cab-gap); margin-top: 6px;
}
.cabin-pad > .pb, .cabin-pad > .cabin-emcon { min-width: 0; }
.cabin-pad > .pb { min-height: 28px; }
/* The launch cap keeps its height when the others give theirs up. */
.cabin-pad > #btn-fire { min-height: 34px; }
/*
 * The emissions switch is a cell in the pad like everything else, and its
 * lettering starts and ends where a cap's does.
 *
 * As drawn it kept the bat-handle's own 16px key gutter against a cap's 22px,
 * so the last row of the card read as a 163px block beside a 150px one with a
 * six-pixel seam where the row above had ten — four objects that did not look
 * like one row of hardware.
 */
.cabin-pad .cabin-emcon { display: flex; }
.cabin-pad .cabin-emcon .sw { padding: 2px 22px 2px 5px; }
.cabin-pad .cabin-emcon .sw-body { height: 18px; }
.cabin-pad .cabin-emcon .sw-legends { gap: 2px; }
/*
 * On a desk with room on it the caps are cap-sized.
 *
 * A console's commit control is the biggest object on its panel; at the
 * reference height the pad was three rows of thirty pixels under two hundred
 * of blank field, which makes the launcher's own controls look like web
 * buttons parked at the bottom of a column. They keep their small sizes on a
 * laptop, where the readouts need every pixel.
 */
@media (min-height: 900px) {
  .cabin-pad > .pb { min-height: 40px; }
  .cabin-pad > #btn-fire { min-height: 54px; }
  .cabin-pad .cabin-emcon .sw-body { height: 26px; }
  .cabin-pad .cabin-emcon .sw-legends { gap: 3px; }
}
.cabin-pad .cabin-emcon .sw-pos { font-size: var(--fs-value); line-height: 14px; }
.cabin-pad #btn-fire { grid-column: 1 / -1; }
.cabin-pad .cabin-emcon .sw { width: 100%; height: 100%; }
/* On a watch with no DISPLACE cap the emissions switch has the row to
   itself rather than leaving a hole beside it. */
.cabin-pad .cabin-emcon:last-child { grid-column: 1 / -1; }
.cabin-pad .pb .lg { width: 100%; }
.cabin-pad .pb .lg b, .cabin-pad .pb .lg i {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
/* Every cap in the pad reserves the same corner for its key chip, so the
   chips cannot make one cap wider than its neighbour. */
.cabin-pad .pb { padding-right: 22px; }
.cabin-pad .pb:not(#btn-fire) .lg b {
  font-size: var(--fs-value); letter-spacing: var(--tr-tight);
}
.cabin-pad .pb .lg i { font-size: var(--fs-plate); letter-spacing: var(--tr-tight); }

/* A battery on the road cannot see, guide or shoot, and the card says so
   rather than looking exactly like one that is fighting. */
.unit.cabin.is-displacing .cabin-body { filter: saturate(.3) brightness(.78); }
.unit.cabin.is-displacing .cabin-pad { opacity: .96; }
/*
 * And the launch cap goes with it.
 *
 * Disabled, it was still the red cap — the loudest object on the panel, six
 * pixels under a banner reading OUT OF ACTION 53s. A control that cannot fire
 * reads as dead metal, the same grey as LOCK and RELOAD beside it, and the
 * red comes back when the battery is back on the air.
 */
.pb-fire.is-safed {
  background-image: linear-gradient(180deg, var(--metal-hi), var(--metal-lo));
  border-color: var(--metal-edge);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 2px 0 var(--metal-edge);
}
.pb-fire.is-safed .lg b, .pb-fire.is-safed .lg i { color: var(--engrave-dim); }

.meter { height: 6px; background: var(--bg-raised); border: 1px solid var(--edge); border-radius: 999px; overflow: hidden; margin: 3px 0 6px; }
.meter i { display: block; height: 100%; background: var(--warn); transition: width .3s; }

/* ----------------------------------------------------------------- scope */

/*
 * The tube reserves the net banner's height too.
 *
 * Only the two panels did, so a directive — which is most of a watch — was
 * painted over the bottom of the instrument: the kilometre scale, the horizon
 * tag and the crew seat's own instruction line all went under it. The banner
 * docks against the bottom of the stage; everything in the stage gives it the
 * same room.
 */
.scope-wrap {
  position: relative; min-width: 0; min-height: 0; overflow: hidden;
  padding-bottom: calc(12px + var(--net-h, 0px));
}
@media (max-width: 900px) { .scope-wrap { padding-bottom: calc(6px + var(--net-h, 0px)); } }
#scope { display: block; width: 100%; flex: 1 1 auto; min-height: 0; }
/* Curved glass over the tube: a highlight across the top-left, nothing more. */
.tube::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: radial-gradient(120% 90% at 22% 8%, rgba(255,255,255,.055), transparent 55%);
}
/* The bezel's own control strip: range selector, plates, and a caution placard. */
.scope-side {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: 98px; flex: 0 0 auto; padding: 4px 2px 2px 8px;
  text-align: center;
}
.scope-side[hidden] { display: none; }
.scope-side .placard { width: 100%; }
.scope-side .data-plate { width: 100%; text-align: left; white-space: normal; }
.scope-side .data-plate.is-footer { margin-top: auto; }
/* The works plate needs a column to be read in. At 76px it broke to one word
   a line — ЗАВОД / ИМ. / КОРНЕЛА stacked down a gutter — and the red placard
   beside it did the same. It gets a wider strip, and below the width where
   that strip fits it is simply not drawn: the same plates are on the topbar. */
@media (max-width: 1400px) { .scope-side { display: none; } }

.scope-overlay {
  position: absolute; inset: 0 0 30px; pointer-events: none;
  /* --flash-a is set per frame by the app from the effect's magnitude. */
  background-color: rgba(var(--flash-rgb), var(--flash-a, 0));
  display: flex; align-items: center; justify-content: center;
  text-align: center; font-size: 15px; letter-spacing: .2em; color: var(--ink);
}
.scope-overlay .boot { max-width: 30em; text-align: left; line-height: 1.8; font-size: 12px; }
.scope-overlay .boot .line { opacity: .85; }
.scope-overlay .huge { font-size: 22px; color: var(--hostile); text-shadow: var(--glow); animation: blink .7s infinite steps(1); }

/*
 * The line under the tube has its own strip, out of the picture.
 *
 * It used to be an overlay pinned to the bottom of the glass, in the display's
 * own ink, over the range-height plot's x-axis: at 1280 the sentence printed
 * straight through the 0, 20, 40, 60, 80, 100 and 120 km numerals and the
 * horizon label, two pieces of text at one size in one colour occupying the
 * same pixels. It is a caption, so it is under the instrument rather than on
 * it, in a strip of fixed height — fixed because this line doubles as the
 * hover readout, and a caption that grew a second line as the pointer moved
 * would resize the canvas and rescale the whole picture.
 */
/*
 * Two lines of room, because it takes two.
 *
 * At 1280 the crew seat's sentence wrapped onto a second line and the strip
 * was 30px tall with `overflow: hidden`, so the second line was sliced off by
 * the tube's bottom edge — half an instruction, which is worse than none. The
 * strip reserves both lines at every width; it is the same height all watch,
 * so nothing above it moves when the sentence changes.
 */
.scope-legend {
  flex: 0 0 auto; height: 44px; overflow: hidden;
  padding: 5px 9px; pointer-events: none;
  border-top: 1px solid var(--metal-edge);
  background: linear-gradient(180deg, var(--bg-panel), var(--bg));
  font-size: 10px; color: var(--ink-dim); line-height: 1.5; letter-spacing: .06em;
}

/*
 * The whole cabin moves, not the picture inside a rigid bezel.
 *
 * This animated `.scope-wrap` alone: measured through the shake that follows
 * the operator's own launch, the phosphor translated by up to two pixels while
 * the bezel around it, the panels beside it and the steel the operator's hands
 * are on all stayed at `transform: none`. A missile left the rail forty feet
 * away and only the image twitched, which reads as a rendering artefact rather
 * than as a launch. The shell carries it now — bezel, rack, cabin and ticker
 * travelling together — with a little rotation, because a room does not slide
 * on rails.
 *
 * --shake-px is set per-effect by the app from the engine's magnitude.
 */
.shell.is-shaking { animation: shake .3s linear infinite; will-change: transform; }
@keyframes shake {
  0% { transform: translate(0, 0) rotate(0deg); }
  20% { transform: translate(var(--shake-px, 2px), calc(var(--shake-px, 2px) * -0.55))
    rotate(calc(var(--shake-px, 2px) * 0.03deg)); }
  45% { transform: translate(calc(var(--shake-px, 2px) * -0.9), calc(var(--shake-px, 2px) * 0.5))
    rotate(calc(var(--shake-px, 2px) * -0.035deg)); }
  70% { transform: translate(calc(var(--shake-px, 2px) * 0.5), var(--shake-px, 2px))
    rotate(calc(var(--shake-px, 2px) * 0.02deg)); }
  100% { transform: translate(0, 0) rotate(0deg); }
}

/* A weapon aimed at one of your own sets: a red breath at the tube's edges,
   pulsing for the effect's life. */
.shell.is-alarm .scope-wrap { animation: alarm-breathe 0.9s ease-in-out infinite; }
@keyframes alarm-breathe {
  0%, 100% { box-shadow: inset 0 0 0 rgba(var(--alarm-rgb), 0); }
  50% { box-shadow: inset 0 0 42px rgba(var(--alarm-rgb), .18); }
}
.shell.is-hit { animation: flicker .5s linear; }

/* ---------------------------------------------------------------- ticker */

.ticker {
  position: relative;   /* the command net anchors to this */
  border-top: 1px solid var(--metal-edge); box-shadow: 0 -2px 6px rgba(0,0,0,.4);
  /* the desk lip: the same columns as the stage, so the reader lands under
     the left of the tube rather than under the left-hand panel */
  display: grid; grid-template-columns: minmax(210px, 300px) auto minmax(0, 1fr);
  align-items: end;
}
.ticker > #card-reader { grid-column: 2; grid-row: 1; }
.ticker > .event-log { grid-column: 3; grid-row: 1; }
.ticker > .action-bar { grid-column: 1 / -1; grid-row: 2; }

/*
 * The command net floats above the ticker rather than sitting in it.
 *
 * It used to be an ordinary block inside the footer, so every directive grew
 * the footer, shrank the stage, and forced the scope canvas to resize and
 * rescale the entire picture mid-watch — and since directive text runs from
 * 101 to 277 characters, the banner changed height between messages too, so
 * the tube jumped a second time. Anchored to the top edge of the ticker it
 * overlays the bottom slice of the scope for as long as it is up, and nothing
 * below or beside it moves at all.
 */
.command-net {
  position: absolute; left: 0; right: 0; bottom: 100%; z-index: var(--z-command);
  display: flex; align-items: center; gap: 12px; padding: 7px 10px;
  border-top: 1px solid var(--metal-edge);
  border-bottom: 1px solid var(--metal-edge);
  background: color-mix(in srgb, var(--hostile) 16%, var(--metal-lo));
  box-shadow: 0 -6px 18px rgba(0, 0, 0, .55);
  animation: alarm-pulse 2s infinite;
}
.command-net[hidden] { display: none; }
.command-text { flex: 1; color: var(--engrave); line-height: 1.5; font-size: 12px; }
.command-tag { color: var(--hostile); letter-spacing: .1em; font-size: 9px; margin-right: 6px; }
.command-actions { display: flex; align-items: center; gap: 6px; }
.command-timer {
  font-variant-numeric: tabular-nums; color: var(--hostile); min-width: 3.5em;
  text-align: right; font-size: 15px; font-weight: 700;
}

.event-log {
  list-style: none; margin: 0; padding: 5px 10px;
  height: 92px; overflow-y: auto; font-size: 11px; line-height: 1.55;
}
.event-log li { display: flex; gap: 8px; }
.event-log .t { color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.event-log .kind-alert { color: var(--hostile); }
.event-log .kind-warn { color: var(--warn); }
.event-log .kind-good { color: var(--good); }
.event-log .kind-launch { color: var(--accent); }
.event-log .kind-command { color: var(--ink-bright); }
.event-log .kind-info { color: var(--ink-dim); }
/* Radio traffic between the elements of the net: the crews talking, as
   against the system reporting on itself. Distinct enough to scan for,
   quiet enough not to shout over an alert. */
.event-log .kind-comms { color: var(--ink); font-style: italic; letter-spacing: .01em; }
.event-log .kind-comms > span:last-child::before { content: '▸ '; opacity: .5; font-style: normal; }
/* Lines about the operator's own street: brighter ink, no new colour —
   the difference should read as attention, not as an alarm. */
.event-log .is-personal { color: var(--ink-bright); font-weight: 600; }

/* ---------------------------------------------------------------- screens */

.screen {
  position: fixed; inset: 0; z-index: var(--z-screen); overflow-y: auto;
  background: var(--bg);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 28px 20px;
}
.screen[hidden] { display: none; }
/*
 * `margin: auto` rather than `align-items: center`: a screen shorter than the
 * page sits in the middle of it instead of leaving a hand's width of empty
 * black under the buttons, and a screen taller than the page still starts at
 * the top, because auto margins have no leftover space to take.
 */
.screen-inner { width: min(940px, 100%); margin: auto; }
/*
 * Prose keeps its measure; a page of figures gets the width it needs — and the
 * outcome of a watch sits at the top of the page, not in the middle of it.
 * `margin: auto` centred the whole stack vertically, so the headline that says
 * what happened to the sector floated a hundred and fifty pixels down a black
 * page with a matching band of nothing under the buttons. It is the first
 * thing on the screen; it belongs at eye level.
 */
.screen-inner.is-debrief { width: min(1240px, 100%); margin: 0 auto; }

/*
 * THE PAGES THAT ARE DOCUMENTS.
 *
 * The full report and the service record are the two longest reads in the
 * game, and all three round-three judges called them styled console divs:
 * "no paper ground, no rule work, no form furniture", "beside the drawn scenes
 * it reads as a debug dump". They are printed now. Every colour on these
 * screens is a token, so the sheet is made by re-declaring the tokens as ink
 * on paper: the cards become printed blocks with hairline rules, the figures
 * keep their ledger ruling, the reds become a stamp-pad red and the glow goes.
 * Nothing in the markup had to move for it.
 */
.screen-inner.is-paper {
  --bg: #ded3b8;
  --bg-panel: #e6ddc3;
  --bg-raised: #efe8d5;
  --grid: #c2b694;
  --edge: #a2957a;
  --ink: #23261f;
  --ink-bright: #101309;
  --ink-dim: #6f6552;
  --accent: #7b4a18;
  --hostile: #9a2b26;
  --friendly: #33556d;
  --unknown: #7a6420;
  --warn: #8a5a12;
  --good: #2c5637;
  --shadow: none;
  --glow: none;
  --radius: 0;
  position: relative;
  color: #23261f;
  background: #ded3b8;
  /* the fibre of the stock, and a shadow so the sheet lies on the desk */
  background-image:
    repeating-linear-gradient(90deg, rgba(35, 38, 31, .035) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(0deg, rgba(35, 38, 31, .028) 0 1px, transparent 1px 4px);
  box-shadow: 0 16px 44px rgba(0, 0, 0, .62), inset 0 0 0 1px rgba(35, 38, 31, .32);
  padding: 22px 30px 34px 62px;
  margin: 0 auto 8px;
}
/* the filing margin, punched, with the red rule a form has down its gutter */
.screen-inner.is-paper::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 38px;
  border-right: 1px solid rgba(154, 43, 38, .5);
  background-image: radial-gradient(circle at 19px 24px, #c4b795 0 7px, transparent 8px);
  background-size: 38px 176px; background-repeat: repeat-y; background-position: 0 54px;
  pointer-events: none;
}
/* the letterhead: the form's name at the left, its number at the right, over a
   double rule, which is how every piece of paper in this army starts */
.screen-inner.is-paper .paper-head {
  display: flex; justify-content: space-between; gap: 16px; align-items: baseline;
  font-size: 9.5px; letter-spacing: .22em; text-transform: uppercase; color: #6f6552;
  padding-bottom: 6px; margin-bottom: 16px;
  border-bottom: 3px double #6f6552;
}
.screen-inner.is-paper .title { text-shadow: none; color: #101309; }
.screen-inner.is-paper .card {
  background: transparent; border: 0; border-top: 1px solid #a2957a;
  padding: 12px 0 6px; margin-bottom: 10px;
}
.screen-inner.is-paper .card h3 {
  color: #43392a; letter-spacing: .2em; font-weight: 700;
  border-bottom: 1px solid #c2b694; padding-bottom: 4px;
}
/*
 * ...except a heading knocked out of a red bar, which is printed white on the
 * ink and not typed on the paper. The art judge: "dark olive-grey type on a
 * brick-red field. At 1x the title of the campaign's biggest reveal is barely
 * readable, while the red stamp two cards above it reads cleanly."
 */
.screen-inner.is-paper .revelation-card h3 {
  color: #f4efe2; border-bottom: 0; padding-bottom: 8px;
}
.revelation-card h3 .tm { opacity: .9; }
.revelation-card h3 .plate-sep { opacity: .7; }
/* the printed panels the deck already had keep their own stock and sit ON the
   sheet, with an edge and a shadow so they read as something laid on it */
.screen-inner.is-paper .letter-card,
.screen-inner.is-paper .revelation-card,
.screen-inner.is-paper .file-entry,
.screen-inner.is-paper .record-card {
  border-top: 0; padding-left: 16px; padding-right: 16px;
  box-shadow: 0 4px 14px rgba(35, 38, 31, .28), inset 0 0 0 1px rgba(35, 38, 31, .26);
}
.screen-inner.is-paper .tile, .screen-inner.is-paper .score-grid > * {
  background: rgba(244, 239, 226, .5); border-color: #a2957a;
}
/* a figure on paper is typed, not lit */
.screen-inner.is-paper table.ledger td { border-bottom: 1px solid #c2b694; }
.screen-inner.is-paper .ending-card { border-top-color: #6f6552; border-bottom-color: #6f6552; }
.screen-inner.is-paper .record-head-line,
.screen-inner.is-paper .record-foot-line { color: #6f6552; }
.screen-inner.is-paper .record-foot-line { border-top-color: #a2957a; }
/* the buttons under the sheet are the console's again: they are not printed */
.screen-inner.is-paper .actions { color: #23261f; }
@media (max-width: 560px) {
  .screen-inner.is-paper { padding: 16px 14px 24px 34px; }
  .screen-inner.is-paper::before { width: 22px; background-image: radial-gradient(circle at 11px 20px, #c4b795 0 5px, transparent 6px); background-size: 22px 150px; }
}
/* The ground and the arithmetic are one band: two tables of a similar height,
   side by side, each hugging its own content rather than one stretching to
   the other's and ending on ninety pixels of empty panel. */
/* `min(420px, 100%)`, not a bare 420px: a fixed minimum wider than the screen
   makes the track wider than the screen, and at 390px the GROUND and POINTS
   tables ran off the right edge with their values unreadable. */
.debrief-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); gap: 14px; align-items: start; }
.debrief-cols > .card { margin-bottom: 14px; }
/*
 * The score is a rack of six across the page.
 *
 * It used to be one column of a two-column band, so six cells wrapped 3+3 in
 * 420px — a rack that ran out — while the card beside it was four tight rows,
 * and the two never shared a baseline.
 */
.score-card .score-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
@media (max-width: 1000px) { .score-card .score-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px) { .score-card .score-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.title {
  font-size: clamp(28px, 6vw, 54px); letter-spacing: .28em; margin: 0 0 2px;
  color: var(--ink-bright); text-shadow: var(--glow); font-weight: 700;
}
.subtitle { color: var(--ink-dim); letter-spacing: .2em; margin: 0 0 22px; font-size: 12px; }
/*
 * The deck line under an outcome: the cause, in one clause. It is set in the
 * headline's voice, so it is held to a headline's width — a full-measure line
 * of letterspaced capitals wrapped to a one-word widow ("BANKED.") and read as
 * body copy pretending to be a title.
 */
.subtitle.grave { max-width: 46ch; letter-spacing: .12em; line-height: 1.5; margin-bottom: 10px; }

.card {
  border: 1px solid var(--edge); background: var(--bg-panel);
  padding: 14px 16px; margin-bottom: 14px; border-radius: var(--radius);
}
.card h3 {
  margin: 0 0 8px; font-size: 11px; letter-spacing: .18em;
  color: var(--ink-dim); text-transform: uppercase;
}
.card :where(p) { margin: 0 0 8px; line-height: 1.65; color: var(--ink); }
.card p:last-child { margin-bottom: 0; }

/*
 * The screens' vocabulary.
 *
 * Everything below replaces a style attribute that used to be written out by
 * hand in screens.js and dossier.js — the same six colours and four sizes,
 * eighty-odd times, with the usual result that two of them had drifted. The
 * names say what the text IS, not what colour it is, so the palette can move
 * without going back through the templates.
 */
.title.is-watch   { font-size: 34px; }   /* the name of a watch, or its outcome */
.title.is-outcome { font-size: 30px; }   /* an ending, and the enlistment form */
.title.is-file    { font-size: 26px; }   /* a service record */
.subtitle.is-lead { margin-bottom: 2px; }
.subtitle.is-tail { margin-top: 18px; }

.note      { color: var(--ink-dim); }        /* the quieter half of a card */
.note small, .note span { color: inherit; }
.quoted    { font-style: italic; }           /* somebody else's words */
.grave     { color: var(--hostile); }        /* what it cost */
.warned    { color: var(--warn); }           /* what you have been told */
.gained    { color: var(--good); }           /* what it earned */
.urgent    { color: var(--accent); }         /* what is waiting for you */
.stencil   { color: var(--ink-bright); font-weight: 700; }
.card p.lede  { color: var(--ink-dim); margin: -2px 0 10px; }
.card p.aside { margin-top: 8px; }
.card p.verdict { margin-top: 9px; }
/* The rank ladder on the dossier: a plain line over its own meter. */
.rank-progress { margin-top: 10px; }
.crew-row.is-plain { border: 0; }
.card.is-tight { padding: 9px 14px; }

/* A score cell carrying a word rather than a figure, with an optional gloss. */
.score-cell.is-word b { font-size: 12.5px; }
.score-cell .sub { display: block; font-size: 9px; color: var(--ink-dim); }

/* The file entry's own treatment is with the other documents, further down:
   it is a form, not a panel with a coloured edge. */

/*
 * Subordinate commands — district and national watches only. It is a
 * bounded scroller rather than a block that grows: on The Two Cities with
 * the cabin open, two formation cards and the reserve strip pushed the rack
 * and the whole crew console out of the bottom of the panel, so LOCK and the
 * launch cap were rendered three hundred pixels below anything a scrollbar
 * could reach.
 */
/* Like the rack below it, the column runs under its own bottom edge rather
   than stopping at it: a shadow cast up from the rail says there is more, which
   a card sliced through its last cap otherwise does not. */
/*
 * A hidden panel is hidden. `display: grid` below outranks the browser's own
 * `[hidden] { display: none }`, so on every watch with fewer than two
 * formations this element kept its 44px minimum and its bottom margin: fifty
 * pixels of empty painted steel under the WEAPONS heading, on the column
 * whose seat was short of room, on every battalion watch in the game.
 */
.formation-list[hidden] { display: none; }
.formation-list {
  display: grid; gap: 7px; margin-bottom: 9px; padding-bottom: 6px;
  min-height: 0; overflow-y: auto;
  box-shadow: inset 0 -14px 12px -12px rgba(0, 0, 0, .8);
}
.formation-list.is-secondary { max-height: 13%; min-height: 44px; }
/* Same bargain as the rack above: on a short column the seat takes the room.
   See the note on .battery-list.is-secondary. */
@media (max-height: 880px) { .formation-list.is-secondary { max-height: 10%; } }
/* On a laptop the orders panel is one strip: see .battery-list.is-secondary. */
@media (max-height: 860px) { .formation-list.is-secondary { max-height: 44px; } }
/*
 * Where there are formations, they are what the watch is played on and the
 * rack is mostly detached batteries you may not touch — so the rack takes
 * the smaller half. Left to shrink in proportion to their content the two
 * split 75px to 504px the wrong way round.
 */
.battery-list.with-formations { flex: 0 1 40%; }
.fmn-bar {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 2px 2px 4px; border-bottom: 1px solid var(--edge);
}
.fmn-count { color: var(--accent); font-size: 11px; letter-spacing: .1em; }
.fmn {
  padding: 7px 9px 9px; border-radius: var(--radius);
  border: 1px solid var(--edge); background: var(--bg-panel);
}
.fmn.is-direct { border-color: var(--accent); background: var(--bg-raised); }
.fmn.is-hq { border-color: var(--good); }
.fmn.is-handover { border-style: dashed; border-color: var(--warn); opacity: .8; }
.fmn.is-detached { opacity: .72; }
/*
 * The reserve is pinned to the foot of the column, not scrolled off it.
 *
 * It is the one decision at this appointment that cannot be taken back —
 * rounds nobody below you can move, four minutes of road — and it was the last
 * card in a bounded scroller, so at 1600 it was sliced through its own head
 * and its RELEASE caps were below anything on screen. Sticky, with the
 * formations running behind it.
 */
.fmn.is-reserve {
  border-color: var(--friendly);
  position: sticky; bottom: 0; z-index: var(--z-sticky);
  background: var(--bg-panel);
  box-shadow: 0 -8px 14px -8px rgba(0, 0, 0, .85);
}
/* An officer with every engagement he can personally hold already running. */
.fmn.is-saturated { border-color: var(--warn); opacity: 1; }
.fmn.is-saturated .fmn-state { color: var(--warn); }
.fmn-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
/* Who is commanding it, on its own instrument line: label left, answer right,
   the way every other figure on this console is set. */
.fmn-state {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  margin-top: 4px; padding-bottom: 3px;
  border-bottom: 1px solid color-mix(in srgb, var(--edge) 50%, transparent);
}
.fmn-state label { color: var(--ink-dim); font-size: 8.5px; letter-spacing: .14em; }
.fmn-state b {
  color: var(--ink-bright); font-size: 10.5px; letter-spacing: .06em; font-weight: 400;
  text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fmn.is-saturated .fmn-state b { color: var(--warn); }
/*
 * The formation card's head is the battery card's head: the English name at
 * the left and the Cyrillic nomenclature on a plate at the right, so one
 * scrolling column does not carry two naming systems. `.unit-head` reserves
 * fourteen pixels for a corner screw these cards do not have.
 */
.fmn-head.unit-head { padding-right: 0; }
.fmn-figures { display: flex; gap: 12px; margin: 5px 0; flex-wrap: wrap; }
.fmn-figures span { color: var(--ink-bright); font-size: 12px; }
.fmn-figures label { display: block; color: var(--ink-dim); font-size: 9px; letter-spacing: .1em; }
.fmn-controls { display: flex; gap: 5px; flex-wrap: wrap; }
/* The selected contact is outside this battery's envelope, said up front. */
.unit-unfit span { color: var(--warn); font-size: 10px; letter-spacing: .05em; }
/* "Nothing is selected yet" is guidance, not a fault: same row, quieter ink. */
.unit-unfit.is-quiet span { color: var(--engrave-dim); }

/* A battery in somebody else's command: on the board, not on your net. */
.unit.is-detached { opacity: .6; }
.unit.is-detached .unit-name::after {
  content: ' — DETACHED'; color: var(--ink-dim); font-size: 9px; letter-spacing: .08em;
}

/* The protected flight: one strip, one aircraft, one watch. */
.flight-strip {
  margin: 6px 4px 0; padding: 7px 9px; border-radius: var(--radius);
  border: 1px solid var(--friendly); background: color-mix(in srgb, var(--friendly) 8%, transparent);
}
.flight-strip.is-threatened { border-color: var(--hostile); animation: flight-pulse 1.1s infinite; }
.flight-strip.is-lost { border-color: var(--hostile); opacity: .8; }
.flight-strip.is-clear { border-color: var(--good); }
.flight-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.flight-head b { color: var(--friendly); letter-spacing: .12em; }
.flight-head span { color: var(--ink-dim); font-size: 10px; letter-spacing: .06em; text-align: right; }
.flight-figures { display: flex; gap: 12px; margin-top: 5px; }
.flight-figures span { color: var(--ink-bright); font-size: 12px; }
.flight-figures span.is-bad { color: var(--hostile); }
.flight-figures label { display: block; color: var(--ink-dim); font-size: 9px; letter-spacing: .1em; }
@keyframes flight-pulse { 50% { background: color-mix(in srgb, var(--hostile) 14%, transparent); } }

/* The campaign, in four appointments. */
.act { margin-bottom: 14px; }
.act.is-locked { opacity: .5; }
.act-head {
  display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
  padding-bottom: 5px; margin-bottom: 7px; border-bottom: 1px solid var(--edge);
}
.act-head > span:last-child { color: var(--ink-dim); font-size: 11px; text-align: right; max-width: 60%; }

.mission-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.mission {
  text-align: left; padding: 11px 13px; cursor: pointer;
  border: 1px solid var(--edge); background: var(--bg-panel); border-radius: var(--radius);
}
.mission:hover:not(:disabled) { border-color: var(--accent); background: var(--bg-raised); }
.mission:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mission.is-active { border-color: var(--accent); background: var(--bg-raised); }
.mission b { display: block; color: var(--ink-bright); letter-spacing: .1em; margin-bottom: 3px; }
.mission small { color: var(--ink-dim); display: block; line-height: 1.5; }
.mission .flags { margin-top: 6px; display: flex; gap: 5px; flex-wrap: wrap; }
/* A watch that is not on this operator's roster. Present, and unreadable. */
.mission.is-sealed { cursor: not-allowed; opacity: .55; border-style: dashed; }
.mission.is-sealed b { color: var(--ink-dim); letter-spacing: .18em; }

.choice-row { display: flex; gap: 8px; flex-wrap: wrap; }
.choice {
  flex: 1 1 160px; text-align: left; padding: 9px 11px; cursor: pointer;
  border: 1px solid var(--edge); background: var(--bg-panel); border-radius: var(--radius);
  /*
   * TOP-ALIGNED, so a row of cards shares one baseline.
   *
   * The art judge, on the dossier: "the cards are vertically centred, so the
   * four titles in one row sit at four different baselines ('Signal Discipline'
   * highest, 'Cold Blood' lowest) and the file stops looking typed." A button
   * centres its own content by default and these are stretched to a common
   * height, so every title floated by half the difference in blurb length.
   */
  display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start;
}
.choice:hover:not(:disabled) { border-color: var(--accent); background: var(--bg-raised); }
.choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice.is-active { border-color: var(--accent); background: var(--bg-raised); }
.choice b { display: block; color: var(--ink-bright); letter-spacing: .1em; }
.choice :where(small) { color: var(--ink-dim); line-height: 1.45; display: block; margin-top: 2px; }
/* Struck out rather than faded: a seat this watch does not offer is not a
   dim seat, it is one that is not on the establishment. */
.choice:disabled {
  cursor: not-allowed; opacity: .8;
  border-style: dashed; border-color: color-mix(in srgb, var(--edge) 55%, transparent);
  background: transparent;
}
.choice:disabled b, .choice:disabled small { color: var(--ink-dim); }

.actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; align-items: center; }
.btn-primary {
  padding: 9px 24px; font-weight: 700; letter-spacing: .2em;
  background: var(--accent); color: var(--bg); border: 1px solid var(--accent);
  border-radius: var(--radius); cursor: pointer;
}
.btn-primary:hover { filter: brightness(1.12); }
.btn-primary:focus-visible { outline: 2px solid var(--ink-bright); outline-offset: 2px; }
.btn-primary:disabled { cursor: not-allowed; filter: grayscale(.9) brightness(.7); }

.score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.score-cell { border: 1px solid var(--edge); padding: 8px 10px; border-radius: var(--radius); }
.score-cell label { display: block; font-size: 9.5px; letter-spacing: .14em; color: var(--ink-dim); }
.score-cell b { font-size: 19px; color: var(--ink-bright); font-variant-numeric: tabular-nums; }
.score-cell.is-bad b { color: var(--hostile); }
.score-cell.is-good b { color: var(--good); }
/*
 * A rack of figures that were never earned reads as WITHHELD.
 *
 * `var(--ink-dim)` is a dim phosphor green, which is the colour this screen
 * uses for a watch that went well — so six zeros under a headline in the
 * failure red were printed in the credit ink, and were also the same ink as
 * their own labels, so TOTAL 0 carried no tone at all. They take the failure
 * tone, muted: not a score, and not a success either.
 */
.score-grid.is-unscored .score-cell b {
  color: color-mix(in srgb, var(--hostile) 62%, var(--ink-dim));
}
.score-grid.is-unscored .score-cell {
  border-style: dashed;
  border-color: color-mix(in srgb, var(--hostile) 40%, var(--edge));
}
.score-withheld { margin: 10px 0 0; font-size: 11px; letter-spacing: .04em; }

table.ledger { width: 100%; border-collapse: collapse; font-size: 11.5px; }
table.ledger td { padding: 2px 0; border-bottom: 1px solid color-mix(in srgb, var(--edge) 40%, transparent); }
table.ledger td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
table.ledger th { padding: 2px 0; text-align: left; font-weight: 400; color: var(--ink-dim); }
table.ledger th.is-figure { text-align: right; }
/* Three columns of headings ran into each other — "the filethe night" — because
   only the cells had a gutter between them and the headings had none. */
table.ledger th + th, table.ledger td + td { padding-left: 14px; }
/* A cell of prose in a column of figures. Colour comes from a tone class. */
table.ledger td.is-prose, table.ledger td.is-prose:last-child { text-align: left; }
/* A place name is a name: PRESIDENTIAL PALACE was force-wrapped over two lines
   in a column with room to spare, opposite a one-word status. */
table.ledger td.is-place { white-space: nowrap; padding-right: 12px; }
@media (max-width: 460px) { table.ledger td.is-place { white-space: normal; } }
/* The gloss about the player's own quarter: its own line, under the row it
   belongs to, at the body's measure and in the body's ink. */
table.ledger tr.is-gloss td { border-bottom: 1px solid color-mix(in srgb, var(--edge) 40%, transparent); color: var(--ink-dim); padding: 0 0 4px; line-height: 1.5; }
table.ledger tr.is-gloss + tr td { padding-top: 5px; }
table.ledger.is-unscored td, table.ledger.is-unscored td:last-child { color: var(--ink-dim); }
table.ledger .up { color: var(--good); }
table.ledger .down { color: var(--hostile); }

/*
 * The handbook's key column.
 *
 * A key group is one expression and must never break inside itself: the column
 * was narrow enough that "Space / 0" set as "Space" over "/ 0", "1 / 2 / 4" as
 * "1 / 2" over "/ 4", and "Shift+1 … 4" as two lines, which turns a list of
 * bindings into a list of riddles. The keys take their own natural width and
 * never wrap; the description takes what is left.
 */
.keys { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 3px 28px; font-size: 11.5px; }
/* The key rail: the caps in one column and what they do in the other, so the
   eye runs down the keys instead of hunting for them inside sentences. */
.keys div { border-bottom: 1px solid color-mix(in srgb, var(--edge) 45%, transparent); }
.keys div:last-child { border-bottom: 0; }
.keys div { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 10px; padding: 1px 0; }
.keys b { white-space: nowrap; }
.keys span { color: var(--ink-dim); }

.toggle-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 11.5px; color: var(--ink-dim); }

/* ------------------------------------------------------------ thumb rail */

/*
 * The verbs of the watch, on the glass, for a hand with no keyboard.
 *
 * Above 900px this console has a keyboard and two full panels and does not
 * need one; below it, the picture panel is gone and the rack is a third of a
 * short screen that scrolls, which is how the launch cap ended up off the
 * bottom of a scroller in portrait and off the screen entirely in landscape.
 * The rail is docked between the stage and the ticker, is never inside
 * anything that scrolls, and its caps are sized for a thumb rather than for a
 * mouse. `renderActionBar` builds it at every width; this rule is the only
 * thing that decides whether the console has one.
 */
.action-bar { display: none; }
/* A dark console has no rail either. */
.is-console-dark .action-bar { visibility: hidden; pointer-events: none; }

/* ------------------------------------------------------- the phone console */

/*
 * Under 900px this is a different piece of hardware, and it is laid out as
 * one rather than as the desktop console with things falling off the edges.
 *
 * The rules: no control the watch needs is off the screen or inside something
 * that scrolls; no key chips, because there is no keyboard to press; the
 * picture keeps the room it needs; and the bar at the bottom is the console.
 */
@media (max-width: 900px) {
  /*
   * The topbar goes to two rows: what the watch IS on top, what you can do to
   * the clock underneath. At 390px the works plate, the identity card and the
   * standing meter were being drawn on top of each other and cut off by the
   * window — set dressing that belongs to a desk with room on it.
   */
  .topbar {
    grid-template-columns: minmax(0, 1fr);
    gap: 3px; padding: 4px 8px;
  }
  .topbar-group:nth-child(2) { display: none; }
  /*
   * The desk furniture folds down rather than away.
   *
   * The reader judge: "the card reader and the standing identity card, which
   * the opening beat is entirely built around, measure 0x0 at phone width —
   * the object the player has just watched their own hand seat in the desk
   * simply is not there when the watch starts." It is there now: the same
   * reader and the same card, scaled to two thirds and set on the desk lip
   * above the log, so the opening beat pays off at every width.
   */
  .ticker { display: block; }
  /*
   * And it gets its own strip of desk.
   *
   * The art judge, at 390: "the reader plate is clipped by the left edge of the
   * screen and its top overlaps the bottom of the battery panel above it,
   * hiding that panel's border and its lower rows." The machined lip it is sunk
   * into runs ten pixels past the plate on both sides, so it needs a margin
   * wider than that, and a clear row above it.
   */
  #card-reader {
    transform: scale(.62); transform-origin: 0 100%;
    height: 50px; width: 196px; margin: 12px 0 6px 18px;
  }
  .topbar-group.rack { justify-content: flex-end; gap: 5px; }
  #unit-plate, #mission-name { display: none; }
  /*
   * The status row wraps rather than shortening anything. Everything on it is
   * a word or a figure a player was taught; a narrow window is a reason for
   * the bar to take a second line, not for the console to start speaking in
   * three-letter codes.
   */
  .topbar-group:first-child { flex-wrap: wrap; }
  .topbar .readout label { display: inline; }
  .topbar .lamp { font-size: 8.5px; }
  .topbar .rack .pb { height: 26px; padding: 0 6px; }
  .topbar .rack .pb .lg b { font-size: 9.5px; white-space: nowrap; }
  /*
   * No keyboard, no key chips. Every cap on a phone loses the stencilled
   * letter that presses it — it is furniture for a finger that does not exist
   * here, and it was costing twenty-two pixels of legend on every control.
   */
  .kc, #rack-keys { display: none; }
  .pb:has(.kc), .pb:has(.kc-wide), .unit-controls .pb { padding-right: 8px; }
  /*
   * A directive is a paragraph and this is a phone: it stacks, it keeps its
   * own answer buttons under it, and it is bounded — it wrapped to thirteen
   * lines and took half the screen, and the panels reserved that height.
   */
  .command-net { flex-direction: column; align-items: stretch; gap: 6px; padding: 6px 8px; }
  .command-text { font-size: 11px; line-height: 1.4; max-height: 5.6em; overflow-y: auto; }
  .command-actions { justify-content: space-between; }

  .stage { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; }
  .panel-left { display: none; }
  /*
   * Thirty per cent of the screen for the rack and the seat — of VISIBLE
   * screen. The panel reserves the net banner's height as bottom padding like
   * every other panel does, so on a phone a live directive ate the whole of
   * the cabin out of the same thirty per cent and left a sliced card head. The
   * reservation is added back on top of the cap, not taken out of it.
   */
  /*
   * On a phone the COLUMN scrolls, not the boxes inside it.
   *
   * Nested scrollers on 250 px of panel is how the cabin came to report a body
   * 0 px tall against 247 px of content: the channel table, the target, the
   * envelope, the kill estimate and the ELINT gauges were all unreachable, and
   * the condition strip was sliced to six pixels of a thirteen-pixel line, cut
   * straight through the words IN ACTION and ON THE AIR. The card is drawn at
   * its natural height and the panel it is in scrolls, so every row of it can
   * be reached by dragging the column — while LOCK, LAUNCH and the emissions
   * switch stay on the thumb rail at the foot of the screen, out of the
   * scroller entirely.
   */
  .panel-right {
    border-left: none; border-top: 1px solid var(--edge);
    max-height: calc(34vh + var(--net-h, 0px));
    overflow-y: auto; scrollbar-width: thin;
    scrollbar-color: var(--metal-hi) var(--well);
  }
  .crew-console { flex: 0 0 auto; overflow: visible; }
  .crew-console .unit.cabin { height: auto; }
  .cabin-body { overflow: visible; max-height: none !important; }
  .cabin-commands { margin-top: 6px; }
  .battery-list.is-secondary { max-height: none; }
  .event-log { height: 52px; }
  /* The tube's instruction line is the rail's job here, and the tube needs
     every pixel it has. */
  .scope-legend { display: none; }
  .bezel { padding: 6px; }

  /*
   * One cap per verb on a phone.
   *
   * LOCK, LAUNCH and the emissions switch are on the thumb rail at the foot of
   * the screen, where a finger can reach them without scrolling a panel. The
   * cabin's own copies of the same three were a second set of the same
   * controls in a different size and a different state — greyed in the console
   * while live on the rail — and the pad they were in was tall enough that the
   * rail sliced through its bottom row. The seat keeps the two verbs the rail
   * does not carry.
   */
  .cabin-pad > #btn-fire,
  .cabin-pad > [data-act="lock"],
  .cabin-pad > .cabin-emcon { display: none; }
  .cabin-pad > [data-act="reload"] { grid-column: 1; }
  .cabin-pad > [data-act="scoot"] { grid-column: 2; }

  .action-bar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px;
    padding: 5px 8px calc(5px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--metal-edge);
    background-color: var(--metal);
    background-image: linear-gradient(180deg, rgba(255, 255, 255, .10), rgba(0, 0, 0, .18));
    box-shadow: 0 -3px 10px rgba(0, 0, 0, .5);
  }
  .action-bar[hidden] { display: none; }
  /* Four verbs that all act on "the selection" have to print the selection. */
  .ab-aim { display: flex; align-items: baseline; gap: 7px; flex: 1 1 100%; min-width: 0; }
  .ab-aim label { font-size: 7.5px; letter-spacing: .13em; color: var(--engrave-dim); flex: 0 0 auto; }
  .ab-aim b {
    font-size: 11px; letter-spacing: .05em; color: var(--engrave);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .ab-caps { display: flex; align-items: stretch; gap: 7px; flex: 1 1 100%; min-width: 0; }
  /* Thumb-sized, and all one size: 44px is the floor a finger hits reliably,
     and these are the caps a watch cannot be played without. */
  .ab-caps > * { flex: 1 1 0; min-width: 0; min-height: 46px; }
  .ab-caps .pb { display: flex; align-items: center; justify-content: center; padding: 5px 4px; }
  .ab-caps .pb .lg { align-items: center; text-align: center; width: 100%; }
  /*
   * A legend wraps before it loses letters. The caps are 46px tall, which is
   * two lines of this type, and on the five-cap rail of a two-seat watch
   * NEXT TARGET was being cut to NEXT TAR… — a control that cannot say its
   * own name. It sets on two lines there and on one everywhere else.
   */
  .ab-caps .pb .lg b {
    font-size: 10px; letter-spacing: .04em; line-height: 1.15;
    white-space: normal; overflow-wrap: normal; max-width: 100%;
  }
  .ab-caps .pb .lg i { font-size: 7.5px; }
  .ab-caps .pb-rail { padding: 5px 4px; margin-top: 0; }
  /* On the rail a dead launch cap goes grey like every other dead cap: at
     thumb size the safed red read as live, and a thumb pressed it. */
  .ab-caps .pb-fire:disabled {
    background: var(--metal-lo); border-color: var(--metal-edge);
    filter: grayscale(.85) brightness(.72); opacity: .72;
  }
  .ab-caps .pb-fire:disabled .lg b, .ab-caps .pb-fire:disabled .lg i { color: var(--engrave-dim); }
  /* The seat cap is as wide as its two words and no wider, so the four
     verbs that share the rest of the rail do not each lose a fifth. */
  .ab-caps .pb-seat { flex: 0 0 auto; padding: 5px 9px; }
  /*
   * The switch is as wide as its engraved legends, never narrower. Sharing
   * the row equally gave it 73px on the five-cap rail of a two-seat phone
   * in portrait and RADIATE was cut to RADIAT… — a switch whose positions
   * are labelled permanently cannot lose the label. The verbs beside it
   * share what is left, and NEXT TARGET sets on two lines when it must.
   */
  .ab-caps .sw { flex: 0 0 auto; padding: 3px 7px 3px 5px; gap: 5px; }
  .ab-caps .sw-pos { font-size: 9.5px; line-height: 13px; padding: 0 4px; }
  .ab-caps .sw-body { width: 13px; height: 21px; }
  .ab-caps .sw-lever { height: 9px; width: 6px; margin-left: -3px; }
  .ab-caps .sw[aria-pressed="true"] .sw-lever { top: 3px; }
  .ab-caps .sw[aria-pressed="false"] .sw-lever { top: 9px; }
}
/*
 * Landscape on a phone is 390px of height for a topbar, a tube, a ticker and
 * the rail. The rack goes — every verb on it that the watch needs is on the
 * rail — the rail's readout moves back beside its caps, and the ticker keeps
 * two lines, because the ticker is how this game speaks.
 */
@media (max-width: 900px) and (max-height: 460px) {
  .panel-right { display: none; }
  .event-log { height: 34px; }
  .topbar { grid-template-columns: minmax(0, 1fr) auto; }
  .topbar-group.rack { grid-column: 2; }
  .topbar-group:first-child { flex-wrap: nowrap; }
  /* 390 pixels of height: the bar is one line, and the mission's name earns
     its place back because it is the only context left. */
  .topbar .readout label { display: none; }
  #mission-name { display: inline; }
  .action-bar { flex-wrap: nowrap; padding-top: 4px; padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px)); }
  .ab-aim { flex: 0 1 130px; flex-direction: column; align-items: flex-start; gap: 0; }
  .ab-caps { flex: 1 1 auto; }
  .ab-caps > * { min-height: 40px; }
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
}

/* ------------------------------------------------------- service record */

/* Paperwork, not a character sheet: a card with a stamp in the corner. */
.record-card { position: relative; border-left: 3px solid var(--accent); }
/* The same sheet of paper, stamped by a different office. */
.record-card.is-abandoned {
  border-left-color: var(--hostile);
  background: color-mix(in srgb, var(--hostile) 5%, var(--bg-panel));
}
.record-card.is-abandoned h3 { color: var(--hostile); }
/*
 * The rubber stamp sits INSIDE the card it is stamped on.
 *
 * It was pinned 8 px from a top edge it is rotated about, so the corner of the
 * box rode over the card's border and its rounded corner — a stamp that had
 * been applied to the desk rather than to the paper. The offsets now clear the
 * rotation: a 16 px box turned 4° needs about 5 px of headroom, and it has 14.
 */
.record-stamp.is-grave { transform: rotate(-4.5deg); }
.record-stamp {
  position: absolute; top: 14px; right: 16px;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  /* Bigger type, a lighter break-up and less rotation: at ten pixels with a
     nine-pixel mask over it the impression came out as an illegible smear,
     which is not what a stamp on a form does. */
  font-size: 12px; line-height: 1.25; letter-spacing: .16em; text-align: center;
  color: color-mix(in srgb, var(--hostile) 74%, #6b5a52);
  border: 1px solid currentColor; box-shadow: 0 0 0 2px transparent, inset 0 0 0 2px transparent;
  outline: 1px solid currentColor; outline-offset: 2px;
  padding: 5px 12px; transform: rotate(-4deg); opacity: .9;
  pointer-events: none;
  mask-image: repeating-linear-gradient(58deg, #000 0 15px, transparent 15px 16px);
  text-shadow: 1px 1px 0 color-mix(in srgb, var(--hostile) 30%, transparent);
}
/* On a narrow window a stamp in the corner prints across the name of the
   person the form is about: it takes its own row under the heading instead. */
@media (max-width: 900px) {
  .record-stamp {
    position: static; transform: rotate(-2deg); align-self: flex-start;
    margin: 8px 0 4px; font-size: 11px;
  }
}
/*
 * A stamp is applied to a clear part of the form, not across the type.
 *
 * The judge: "the red rotated stamp is printed across the end of the first
 * paragraph, so both the stamp's second line and the words are unreadable",
 * and on the posting order "the stamp straddles the card's bottom-right border
 * and its second line, TM ADF, is cut in half by it". So the head of every
 * stamped card keeps a gutter for it, and a one-line card is tall enough to
 * take the impression inside its own edges.
 */
@media (min-width: 901px) {
  .record-card > h3,
  .record-card > .ident-row,
  .record-card > p:first-of-type,
  .record-card > .record-stamp + h3 + p { padding-right: 178px; }
  .record-card.is-tight { min-height: 78px; display: flex; flex-direction: column; justify-content: center; }
  .record-card.is-tight > p { padding-right: 178px; }
  .record-card.is-tight .record-stamp { top: 50%; transform: translateY(-50%) rotate(-4deg); }
}
.record-stamp .tm { display: block; }
.record-stamp .en { display: block; font-size: 8.5px; opacity: .9; }
.record-stamp + .score-grid { margin-top: 26px; }

.field-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-top: 10px; }
.field { display: flex; flex-direction: column; gap: 3px; flex: 1 1 210px; }
.field span { font-size: 9px; letter-spacing: .12em; color: var(--ink-dim); }
.field input, .field select {
  font: inherit; font-size: 13px; color: var(--ink-bright);
  background: var(--bg); border: 1px solid var(--edge); border-radius: var(--radius);
  padding: 6px 8px;
}
.field input:focus-visible, .field select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.field input:hover, .field select:hover { border-color: var(--accent); }


/*
 * The documents.
 *
 * A letter, a leaked file, a form and an ending are four different objects and
 * they were four tinted panels with a coloured left border. Each is built here
 * as the thing it is — paper with a ground, a rule, a stamp and a type — so a
 * page of the report reads as a folder rather than as a stack of cards.
 */

/* The post: cream paper, a cold ink, ruled lines and a torn top edge. */
.letter-card {
  position: relative; border: 0; border-radius: 0;
  padding: 26px 30px 24px;
  max-width: 780px; color: #2b2f4a;
  background-color: #efe6cf;
  background-image: repeating-linear-gradient(180deg,
    transparent 0 23px, rgba(43, 47, 74, .14) 23px 24px);
  background-position: 0 11px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(43, 47, 74, .12);
  /* the torn edge: a sawtooth cut out of the top of the sheet */
  mask:
    conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) 0 0 / 14px 7px repeat-x,
    linear-gradient(#000 0 0) 0 7px / 100% calc(100% - 7px) no-repeat;
}
.letter-card h3 {
  color: #2b2f4a; font-size: 13px; letter-spacing: .1em; font-style: italic;
  border-bottom: 1px solid rgba(43, 47, 74, .3); padding-bottom: 6px; margin-bottom: 10px;
}
.letter-card p { color: #2b2f4a; font-style: italic; line-height: 24px; margin-bottom: 0; }
.letter-card p + p { margin-top: 24px; }
.letter-card .note, .letter-card .quoted { color: #55597a; }
/*
 * How the post arrived, shown and not narrated: the disposition stencilled
 * beside the letter's own title, and the section's docket as a buff slip
 * clipped to the sheet. First cut; the artist owns the finish.
 */
.letter-card .letter-plate {
  float: right; margin-left: 14px;
  font-style: normal; font-size: 9.5px; letter-spacing: .18em; color: #7a2f2a;
  border: 1px solid rgba(122, 47, 42, .55); padding: 2px 7px;
}
.letter-card .letter-slip {
  display: inline-block; float: right; clear: right;
  max-width: 46%; margin: 4px 0 10px 18px; padding: 8px 11px;
  font-style: normal; font-size: 11px; line-height: 1.45; color: #3a3226;
  background: #cdbf9c; border: 1px solid #6f6552;
  box-shadow: 3px 3px 0 rgba(10, 13, 10, .28); transform: rotate(-1.1deg);
}
.letter-card .letter-salutation { margin-top: 4px; }
.letter-card .letter-sign { margin-top: 24px; text-align: right; }
/* On a narrow sheet nothing is set beside anything: the stamp and the slip go
   across the page and the letter starts under them. */
@media (max-width: 760px) {
  .letter-card .letter-plate { float: none; display: inline-block; margin: 0 0 10px; }
  .letter-card .letter-slip { float: none; max-width: 100%; margin: 6px 0 16px; }
}

/* The file nobody meant you to read: buff stock, a red header, a stamp. */
.revelation-card {
  position: relative; border: 0; border-radius: 0;
  padding: 0 0 22px; overflow: hidden; max-width: 860px;
  color: #23261f; background: #ded3b8;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(35, 38, 31, .25);
}
.revelation-card h3 {
  margin: 0 0 18px; padding: 9px 26px 8px;
  background: #9a2b26; color: #f4efe2;
  font-size: 11.5px; letter-spacing: .26em;
}
/* The plate and the title, separated by a printed middot rather than by a
   margin — the margin was invisible to anything reading the text itself, so
   the heading read "НАРЯДTHE ALLOCATION". */
.revelation-card h3 .tm { opacity: .78; }
.revelation-card h3 .plate-sep { opacity: .55; }
.revelation-card p {
  color: #23261f; line-height: 1.8; margin: 0 30px 12px; max-width: 74ch;
}
.revelation-card .doc-stamp {
  position: absolute; right: 22px; bottom: 18px;
  border: 2px double #9a2b26; color: #9a2b26;
  padding: 5px 12px; font-size: 11px; font-weight: 700; letter-spacing: .24em;
  transform: rotate(-7deg); opacity: .8; pointer-events: none;
  mask-image: repeating-linear-gradient(62deg, #000 0 9px, transparent 9px 10px);
}

/* A file entry: a form, with a rule under its heading and a number on it. */
.file-entry {
  position: relative; border: 0; border-left: 0; border-radius: 0;
  background: #d9dcd4; color: #23261f; max-width: 860px;
  padding: 14px 22px 16px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45), inset 0 0 0 1px rgba(35, 38, 31, .28);
}
.file-entry h3 {
  color: #23261f; border-bottom: 2px solid #23261f; padding-bottom: 5px;
  margin-bottom: 10px; padding-right: 120px;
}
.file-entry p { color: #23261f; }
.file-entry .note, .file-entry .aside { color: #4a4f48; }
.file-entry .form-no {
  position: absolute; right: 20px; top: 14px;
  font-size: 9px; letter-spacing: .16em; color: #4a4f48;
}
.file-entry.is-good h3 { border-bottom-color: #2f5a3a; }

/* The last watch's outcome: a black field with margin, and no card at all. */
/* The ending is not another panel in the deck: it is set on the black field,
   centred, with a hand's width of margin and no chrome of any kind. */
/*
 * The story judge measured the last cut: "the letterhead rules run from x180 to
 * x1420 but the prose column starts at x540 and ends at x1060, leaving a 360px
 * void down the left of the block and 360px on the right. The one screen
 * designed as a printed record is set off-centre inside its own rules." So the
 * rules are pulled in to the measure: the block is one width from the head
 * rule to the foot rule, and the type fills it.
 */
.ending-card {
  position: relative;
  border: 0; border-radius: 0; background: transparent; box-shadow: none;
  border-top: 2px solid var(--edge); border-bottom: 1px solid var(--edge);
  /*
   * ONE MARGIN for the whole sheet. Two judges measured the last cut: "the
   * narrative block is inset to about x=527 while the page's own header rule,
   * punch holes and SCORE section start at x=240", "the finding's prose column
   * starts about 290px to the right of the letterhead rule it sits under, so
   * the type floats in the middle of the page with a wide unexplained gutter
   * that is not the punched filing margin". A document has one left edge:
   * `auto` centred this block inside a page everything else was ranged left on.
   */
  padding: 12px 0 14px; margin: 0 0 22px; max-width: none;
}
/*
 * The finding, across the sheet: the prose at a reading measure and a ruled
 * rail of particulars beside it, so the page has one left edge, one right edge
 * and nothing empty between them.
 */
.ending-card .record-body {
  display: grid; grid-template-columns: minmax(0, 74ch) minmax(200px, 1fr);
  gap: 34px; align-items: stretch;
}
.ending-card .record-prose { min-width: 0; }
.record-rail {
  border-left: 1px solid var(--edge); padding-left: 20px;
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-dim);
}
.record-rail .rail-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 6px 0; border-bottom: 1px dotted var(--edge);
}
.record-rail .rail-row b { font-weight: 400; color: var(--ink); text-align: right; }
/* A form's own remarks block: ruled lines nobody has written on, which is what
   the right-hand column of a record sheet actually carries. */
.rail-remarks { margin-top: 18px; }
.rail-remarks > span { display: block; padding-bottom: 6px; }
.rail-remarks > i { display: block; height: 17px; border-bottom: 1px dotted var(--edge); }
.record-stamp.is-inline {
  position: static; display: inline-flex; transform: rotate(-3deg);
  margin: 22px 0 4px 10px; font-size: 11px;
}
.ending-card .title.is-outcome { text-wrap: balance; }
@media (max-width: 900px) {
  .ending-card .record-body { grid-template-columns: 1fr; gap: 16px; }
  .record-rail { border-left: 0; padding-left: 0; border-top: 1px solid var(--edge); padding-top: 12px; }
}
/* the letterhead and the foot of the record, so the finding is a document */
.record-head-line, .record-foot-line {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 9px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-dim);
  padding: 2px 0 10px;
}
.record-foot-line { padding: 12px 0 0; border-top: 1px solid var(--edge); margin-top: 14px; }
.ending-card .title.is-outcome { margin-top: 2px; }
.is-debrief .subtitle.is-lead, .is-debrief .title.is-outcome, .is-debrief .subtitle.is-tail { text-align: center; }
.ending-card p { line-height: 1.85; margin: 0 0 14px; max-width: none; color: var(--ink); }
.ending-card p:first-child { color: var(--ink-bright); font-size: 15px; }

/* A dark console blanks its panels along with the scope, and the controls
   under them go dead (the input handlers hold the other brace). The topbar
   stays — the room still exists, and so do the clock and the handbook. */
.is-console-dark #panel-tracks,
.is-console-dark #panel-batteries,
.is-console-dark #command-net,
.is-console-dark #scope-side,
.is-console-dark #scope-legend,
.is-console-dark #event-log {
  visibility: hidden;
  pointer-events: none;
}

/* The teaching watch's walk-through: one instruction at a time, cleared by
   the player actually doing it. Sits over the scope's top-left, out of the
   track list's way, and dies with the console in a blackout. */
.tutorial-card {
  position: absolute;
  /* Clear of the tube's own two readouts, which the canvas draws at y=16 and
     y=30: the card used to be laid straight over RANGE 140 KM. */
  top: 42px;
  left: 14px;
  max-width: 320px;
  padding: 10px 34px 10px 12px;
  background: var(--bg);
  border: 1px solid var(--accent);
  box-shadow: 0 0 0 1px var(--engrave), 0 4px 18px rgba(0, 0, 0, .45);
  z-index: var(--z-tutorial);
}
.tutorial-card b { display: block; font-size: 12.5px; line-height: 1.45; color: var(--ink); letter-spacing: .02em; }
.tutorial-card i { display: block; margin-top: 3px; font-size: 10px; font-style: normal; opacity: .68; letter-spacing: .14em; }
.tutorial-card .tut-step { display: block; font-size: 9.5px; letter-spacing: .2em; opacity: .6; margin-bottom: 4px; }
.tutorial-card .tut-skip {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  background: none;
  border: 1px solid var(--engrave);
  color: var(--ink);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
}
.tutorial-card .tut-skip:hover { background: var(--bg-raised); color: var(--ink-bright); }
.tutorial-card .tut-skip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.is-console-dark #tutorial-card { visibility: hidden; }

/*
 * The one question the console asks before it does something irreversible.
 *
 * Leaving the post ends the watch and is scored as abandonment, so the cap
 * asks first and the card says what it will cost. It shares the command net's
 * layer and comes after it in the markup, so it sits over everything on the
 * stage; the simulation underneath keeps running, which the line at the top
 * says out loud, because a confirmation that quietly stops the raid would be
 * a free pause with a question mark on it.
 */
.ask[hidden] { display: none; }
.ask {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, .62);
  z-index: var(--z-command);
}
.ask-card {
  width: min(520px, 82vw);
  padding: 16px 18px 14px;
  background: var(--bg-panel);
  border: 1px solid var(--edge);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .6), 0 18px 46px rgba(0, 0, 0, .62);
}
.ask-card h2 { margin: 0 0 10px; font-size: 13px; }
.ask-card h2 .lg b { font-size: 14px; letter-spacing: .18em; color: var(--ink-bright); }
.ask-card h2 .lg i { font-size: 10px; letter-spacing: .16em; color: var(--ink-dim); }
.ask-line {
  margin: 0 0 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
}
.ask-costs {
  margin: 0 0 14px;
  padding: 9px 12px 9px 26px;
  list-style: square;
  border-left: 2px solid var(--warn);
  background: var(--bg-raised);
  font-size: 11.5px;
  line-height: 1.7;
  color: var(--ink-dim);
}
.ask-costs b { color: var(--ink); }
.ask-actions { display: flex; gap: 10px; justify-content: flex-end; }
/*
 * The card is a form with the three costs numbered on it and the word the
 * file will carry stamped beside them; the button that costs you the watch
 * takes the alarm colour at full strength behind a guarded outline, and the
 * one that costs nothing goes quiet, because the two used to be the same
 * weight side by side.
 */
.ask-card { position: relative; }
.ask-costs { counter-reset: cost; list-style: none; padding-left: 0; }
.ask-costs li { counter-increment: cost; position: relative; padding-left: 26px; }
.ask-costs li::before {
  content: counter(cost); position: absolute; left: 0; top: 2px;
  width: 16px; height: 16px; line-height: 16px; text-align: center;
  font-size: 9px; color: var(--ink-dim); border: 1px solid var(--edge);
}
.ask-stamp {
  position: absolute; right: 26px; top: 18px;
  border: 2px double var(--hostile); color: var(--hostile);
  padding: 4px 10px; font-size: 10px; font-weight: 700; letter-spacing: .24em;
  transform: rotate(-8deg); opacity: .55; pointer-events: none;
  mask-image: repeating-linear-gradient(64deg, #000 0 9px, transparent 9px 10px);
}
.ask-actions .pb-refuse {
  background: color-mix(in srgb, var(--hostile) 26%, transparent);
  border-color: var(--hostile);
  box-shadow: inset 0 0 0 2px var(--bg-panel), inset 0 0 0 3px color-mix(in srgb, var(--hostile) 70%, transparent);
  background-image: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--hostile) 22%, transparent) 0 7px, transparent 7px 14px);
}
.ask-actions .pb-refuse:hover { background-color: color-mix(in srgb, var(--hostile) 38%, transparent); }
.ask-actions .pb-accept {
  background: transparent; border-color: color-mix(in srgb, var(--edge) 80%, transparent);
}
.ask-actions .pb-accept:hover { border-color: var(--accent); }
/* The two answers are the same size: a question does not lean on one of them
   by being wider. */
.ask-actions .pb { flex: 0 0 180px; width: 180px; text-align: center; }
.ask-actions .pb .lg { align-items: center; width: 100%; }

/* The one thing a browser with scripting off is shown. It is outside the
   console entirely, so it names the tokens it needs rather than inheriting. */
.noscript { color: var(--ink); padding: 2rem; font: 14px var(--font-mono); }

/*
 * The scenes between watches.
 *
 * The player: "The little displays and summaries between watches is cluttered
 * and overwhelming. It should be replaced with slick, 16-bit style animated
 * cut scenes." The host covers the window in black with the picture centred
 * in it at a whole-pixel scale; the words sit over the picture as ordinary
 * type in a pixel frame, or straight on the paper when the scene is a sheet
 * in the operator's hands. --scene-scale is the picture's scale, so every
 * measurement here is in scene pixels.
 */
/* The scene host: the whole window, black, with the picture centred in it. */
.scene { position: fixed; inset: 0; z-index: 60; background: #000; overflow: hidden; font-family: var(--font-mono); }
/*
 * On a phone the picture cannot fill a screen twice as tall as it is wide, so
 * what is left over is the dark of the room the player is sitting in rather
 * than a black void with a bordered empty box in it: the same ink, with the
 * same grain the scenes are dithered in, and the picture and the words set as
 * one group in the middle of it.
 */
.scene.is-stacked {
  background-color: #0a0d0a;
  background-image:
    repeating-linear-gradient(0deg, rgba(74, 79, 72, .16) 0 2px, transparent 2px 4px),
    repeating-linear-gradient(90deg, rgba(74, 79, 72, .16) 0 2px, transparent 2px 4px);
  background-size: 4px 4px, 4px 4px;
}
.scene[hidden] { display: none; }
/* A wordless beat has no words: the box is hidden, and it has to stay hidden
   against its own `display: flex`. */
.scene-text[hidden] { display: none; }
.scene canvas { position: absolute; image-rendering: pixelated; image-rendering: crisp-edges; }
/*
 * The words, over the picture. A pixel frame, ordinary type inside it.
 *
 * The box is a column: a speaker, a body that SCROLLS, and a row reserved at
 * the bottom for the prompt. Both of those are repairs. The body used to be
 * `overflow: hidden` inside a fixed box, so the last line of three scene kinds
 * — the end of the tape, the last line of every letter, the sentence the whole
 * campaign resolves onto — was drawn below the bottom edge and could not be
 * reached by any means; and the prompt used to print on top of whatever was on
 * the last line.
 */
.scene-text {
  position: absolute; box-sizing: border-box; overflow: hidden;
  display: flex; flex-direction: column;
  padding: calc(4px * var(--scene-scale, 3)) calc(5px * var(--scene-scale, 3))
           calc(7px * var(--scene-scale, 3));
  color: #e9efe4; font-size: calc(4.6px * var(--scene-scale, 3)); line-height: 1.35;
  /* Opaque. It was 86% black, so the desk drawers and their verticals printed
     through the box and crossed the type on every office shot. */
  background: #070a08;
  box-shadow: 0 0 0 calc(1px * var(--scene-scale, 3)) #cfd8cc, 0 0 0 calc(2px * var(--scene-scale, 3)) #0a0d0a;
}
.scene-speaker { display: block; flex: 0 0 auto; font-size: calc(3.4px * var(--scene-scale, 3)); letter-spacing: .12em; color: #9fb3a0; margin-bottom: calc(2px * var(--scene-scale, 3)); text-transform: uppercase; }
/*
 * The section's slip.
 *
 * The player: the letter's disposition should be "a slip, a stamp or a line in
 * the speaker plate", never a sentence a narrator reads out. The plate above
 * carries the short form; this is the docket itself, a scrap of buff paper
 * clipped to the sheet at a slight angle. First cut — the artist owns how it
 * finally looks.
 */
.scene-slip {
  position: relative; display: block; flex: 0 0 auto; align-self: flex-start;
  max-width: 72%; margin: calc(5px * var(--scene-scale, 3)) 0 calc(1px * var(--scene-scale, 3)) calc(3px * var(--scene-scale, 3));
  /* the top edge is left clear for the wire clip: the loop used to land on
     the first line of the docket and eat letters out of it */
  padding: calc(3.4px * var(--scene-scale, 3)) calc(2.4px * var(--scene-scale, 3)) calc(1.2px * var(--scene-scale, 3));
  font-size: calc(2.9px * var(--scene-scale, 3)); line-height: 1.3; letter-spacing: .03em;
  font-style: normal; color: #3a3226; background: #cdbf9c;
  border: 1px solid #6f6552;
  /* the torn lower edge of a docket, in the paper's own two inks */
  border-bottom: 0;
  box-shadow: calc(1px * var(--scene-scale, 3)) calc(1.4px * var(--scene-scale, 3)) 0 rgba(10, 13, 10, .5);
  transform: rotate(-1.2deg);
}
/* the tear along the bottom */
.scene-slip::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: calc(-1.4px * var(--scene-scale, 3));
  height: calc(1.6px * var(--scene-scale, 3));
  background: repeating-linear-gradient(90deg, #cdbf9c 0 3px, #6f6552 3px 4px, #cdbf9c 4px 6px, transparent 6px 7px);
}
/* the wire clip that holds it to the sheet */
.scene-slip::before {
  content: ''; position: absolute; left: 12%; top: calc(-2.2px * var(--scene-scale, 3));
  width: calc(6px * var(--scene-scale, 3)); height: calc(5px * var(--scene-scale, 3));
  border: calc(.9px * var(--scene-scale, 3)) solid #6b7168; border-bottom: 0; border-radius: 40% 40% 0 0;
}
.scene-slip[hidden] { display: none; }
.scene.is-stacked .scene-slip { font-size: 11px; padding: 8px 9px 5px; margin: 18px 0 12px 6px; }
.scene-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: none; }
.scene-body::-webkit-scrollbar { width: 0; height: 0; }
.scene-body p { margin: 0 0 calc(1.5px * var(--scene-scale, 3)); white-space: pre-wrap; }
.scene-body .cursor { display: inline-block; width: .55em; height: 1em; vertical-align: -.15em; background: currentColor; margin-left: 1px; animation: scene-blink .9s steps(1) infinite; }
@keyframes scene-blink { 50% { opacity: 0; } }
.scene-prompt { position: absolute; right: calc(5px * var(--scene-scale, 3)); bottom: calc(1.5px * var(--scene-scale, 3)); font-size: calc(3.6px * var(--scene-scale, 3)); color: #cfe3d0; animation: scene-blink 1.2s steps(1) infinite; }
/* The paper: dark ink on the sheet, no frame of its own — the sheet is the frame. */
.scene[data-kind="printout"] .scene-text, .scene[data-kind="quarters"] .scene-text, .scene[data-kind="folder"] .scene-text {
  background: transparent; box-shadow: none; color: #23261f;
}
/* The tape is upright and near-black, the letter is a hand in a cold ink, the
   document is a typed file. Three documents, three hands, one rule. */
/* The tape's type is locked to the paper: two printed lines to one bar of the
   continuous stationery, so the words sit ON the sheet instead of floating
   above a ruling that disagrees with them. */
/*
 * The tape is set at the pitch the paper feeds at: TAPE_LINE in scenes.js is
 * 4.5 scene pixels and so is this, so a line of type and a line of paper are
 * the same distance. The box is anchored at the print head and grows upward,
 * which is why it needs no padding under the last line and why the prompt is
 * outside it, on the machine's own front face.
 */
.scene[data-kind="printout"] .scene-text {
  font-size: calc(3.9px * var(--scene-scale, 3)); line-height: calc(4.5px * var(--scene-scale, 3));
  letter-spacing: .04em; overflow: visible; padding-bottom: 0;
}
.scene[data-kind="printout"] .scene-body { overflow: visible; }
.scene[data-kind="printout"] .scene-body p { margin-bottom: 0; }
.scene[data-kind="printout"] .scene-prompt {
  bottom: calc(-9px * var(--scene-scale, 3)); right: calc(2px * var(--scene-scale, 3)); color: #d9dcd4;
}
/* The file and the letter are locked to their own ruling the same way: two
   lines of type to one rule, so the words sit on the paper. */
.scene[data-kind="folder"] .scene-text { font-size: calc(4.1px * var(--scene-scale, 3)); line-height: calc(4.5px * var(--scene-scale, 3)); color: #2b2f28; }
.scene[data-kind="folder"] .scene-body p { margin-bottom: calc(4.5px * var(--scene-scale, 3)); }
/* Half a ruled line between paragraphs, so a whole letter — salutation,
   body and signature — is on the sheet at once at every width. The sheet's
   ruling has to be laid on these baselines rather than the other way round. */
.scene[data-kind="quarters"] .scene-body p { margin-bottom: calc(2.25px * var(--scene-scale, 3)); }
.scene[data-kind="printout"] .scene-speaker, .scene[data-kind="quarters"] .scene-speaker { color: #6f6552; }
/* the file's own heading, in the ink the file is typed in — it used to be
   the same grey as the page ruling, with a rule through it. */
.scene[data-kind="folder"] .scene-speaker { color: #43392a; font-weight: 700; }
.scene[data-kind="quarters"] .scene-text { font-style: italic; font-size: calc(3.9px * var(--scene-scale, 3)); line-height: calc(4.5px * var(--scene-scale, 3)); color: #2b2f4a; }
/*
 * On the three paper scenes the prompt is DRAWN, as a chevron on the picture —
 * on the printer's front lip, on the folder's board, on the floor of the
 * quarters — because printed over the sheet it is type on the paper, which is
 * what the art judge measured: "the NEXT prompt is printed on the paper. On
 * the tape it lands on the bottom-right corner across the perforation strip
 * and the last banded row; on the transfer it lands on the document's ruled
 * area beside TRANS 719." The span stays in the DOM, carrying the same text,
 * because it is what assistive tech and the harnesses read to know a line has
 * finished; it simply does not print over the picture.
 */
.scene[data-kind="printout"] .scene-prompt, .scene[data-kind="quarters"] .scene-prompt, .scene[data-kind="folder"] .scene-prompt { color: transparent; }
/* The order of appointment is read across the desk front, in the spoken box —
   named here so a kind can never fall through to the default by accident. */
.scene[data-kind="appointment"] .scene-text, .scene[data-kind="office"] .scene-text,
.scene[data-kind="finding"] .scene-text, .scene[data-kind="ending"] .scene-text { background: #070a08; }
/* A phone held upright: the words under the picture, on their own sheet. */
.scene.is-stacked .scene-text { font-size: 15px; line-height: 1.5; padding: 13px 15px 26px; justify-content: flex-start; }
.scene.is-stacked .scene-body { flex: 0 1 auto; }
.scene.is-stacked .scene-speaker { font-size: 10px; margin-bottom: 6px; }
.scene.is-stacked .scene-prompt { font-size: 11px; right: 14px; bottom: 8px; }
/* On a phone the letter and the file page run off the bottom of the picture
   and carry on down the screen: the panel is the same sheet, so it takes the
   paper's own ink and no frame of its own. */
.scene.is-stacked[data-kind="quarters"] .scene-text,
.scene.is-stacked[data-kind="folder"] .scene-text {
  background: #f4efe2; box-shadow: none; color: #23261f;
  border-left: 1px solid #b2a482; border-right: 1px solid #b2a482;
  padding: 4px 14px 22px;
  /* The sheet runs to the foot of the screen. It used to be sized to its own
     words, so a phone showed a strip of paper floating in a black field with
     the drawn hands holding a blank sheet above it. */
  font-size: 13px;
}
.scene.is-stacked[data-kind="quarters"] .scene-body,
.scene.is-stacked[data-kind="folder"] .scene-body { flex: 0 1 auto; }
.scene.is-stacked[data-kind="folder"] .scene-text { background: #ded3b8; }
/*
 * On a phone the tape's panel IS the paper: the machine is at the bottom of
 * the picture, the sheet rises out of it, and this is the same sheet carrying
 * on up the screen with the words at the bottom of it, climbing as they print.
 */
.scene.is-stacked[data-kind="printout"] .scene-text {
  font-size: 13px; justify-content: flex-end; overflow: hidden;
  background: #f4efe2; box-shadow: none; color: #23261f;
  /* the sprocket margins of the drawn sheet, carried up the panel at the same
     pitch, so the paper in the picture and the paper under the words are one
     piece of paper */
  border-left: 11px solid #ebe4d2; border-right: 11px solid #ebe4d2;
  border-top: 3px dotted #b2a482;
  padding: 10px 10px 4px;
  background-image:
    repeating-linear-gradient(180deg, transparent 0 5px, #b2a482 5px 8px, transparent 8px 13px),
    repeating-linear-gradient(180deg, transparent 0 5px, #b2a482 5px 8px, transparent 8px 13px);
  background-size: 3px 100%, 3px 100%;
  background-position: -7px 0, calc(100% + 7px) 0;
  background-repeat: repeat-y, repeat-y;
}
.scene.is-stacked[data-kind="printout"] .scene-body { flex: 0 0 auto; overflow: visible; }
/* the drawn chevron carries the prompt on the paper scenes at every width */
.scene.is-stacked[data-kind="printout"] .scene-prompt { bottom: 4px; right: 12px; color: transparent; }
.scene.is-stacked[data-kind="printout"] .scene-skip { bottom: auto; top: 12px; right: 14px; left: auto; }
.scene.is-stacked[data-kind="folder"] .scene-text, .scene.is-stacked[data-kind="quarters"] .scene-text { line-height: 1.5; }
.scene.is-stacked[data-kind="folder"] .scene-body p, .scene.is-stacked[data-kind="quarters"] .scene-body p { margin-bottom: 10px; }
/* On a phone the picture goes to the top of the window, so SKIP cannot sit
   over it: it goes under the words, where the screen is empty anyway. */
.scene.is-stacked .scene-skip { top: auto; right: auto; bottom: 16px; left: 14px; }
.scene-skip {
  position: absolute; top: 14px; right: 16px; z-index: 2; cursor: pointer;
  background: rgba(8, 12, 9, .8); color: #cfe3d0; border: 1px solid #6b7a6a; border-radius: 2px;
  font: 700 11px var(--font-mono); letter-spacing: .12em; padding: 6px 10px;
}
.scene-skip:hover, .scene-skip:focus-visible { border-color: #cfe3d0; outline: none; }

/*
 * The contact's menu.
 *
 * The player: "there should be a right click context menu that is available
 * for each target which shows a list of SAMs to which the target can be
 * assigned (including stats about range-to-target, missiles available,
 * etc.)." One row per battery: name and plate, when the contact is inside
 * its ring, the range and the reach, the rounds, the crew's estimate, and
 * whose net it is on. A live row is a button; a dead one says why.
 */
.ctx-menu {
  position: fixed; z-index: var(--z-command);
  min-width: 460px; max-width: min(600px, calc(100vw - 16px));
  overflow-y: auto; overscroll-behavior: contain;
  padding: 6px; font-size: 11px;
  background: var(--bg-panel); border: 1px solid var(--edge);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .6), 0 18px 46px rgba(0, 0, 0, .62);
}
.ctx-menu[hidden] { display: none; }
.ctx-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 4px 8px 6px; margin-bottom: 4px; border-bottom: 1px solid var(--edge);
  color: var(--ink); letter-spacing: .06em;
}
.ctx-head b { color: var(--ink-bright); }
.ctx-hint { color: var(--ink-dim); font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; }
.ctx-row {
  display: grid; grid-template-columns: 1.3fr 1fr .8fr 1.5fr .6fr 1fr; gap: 8px; align-items: center;
  width: 100%; margin: 0; padding: 6px 8px; text-align: left;
  background: transparent; border: 1px solid transparent; color: var(--ink);
  font: inherit; cursor: pointer;
}
.ctx-row.is-live:hover, .ctx-row.is-live:focus-visible { background: var(--bg-raised); border-color: var(--edge); outline: none; }
.ctx-row.is-dead { color: var(--ink-dim); cursor: not-allowed; }
.ctx-row.is-on { border-color: var(--accent); }
.ctx-name { display: flex; flex-direction: column; }
.ctx-name b { color: var(--ink-bright); letter-spacing: .08em; }
.ctx-row.is-dead .ctx-name b { color: var(--ink-dim); }
.ctx-name i, .ctx-range i, .ctx-pk i { display: block; font-style: normal; font-size: 9px; color: var(--ink-dim); }
.ctx-when { font-weight: 700; letter-spacing: .04em; }
.ctx-cmd.is-not { color: var(--warn); }
.ctx-why { grid-column: 1 / -1; font-size: 10px; color: var(--warn); }
.ctx-empty { margin: 8px; color: var(--ink-dim); }
@media (max-width: 900px) {
  .ctx-menu { min-width: 0; width: calc(100vw - 16px); }
  .ctx-row { grid-template-columns: 1.2fr 1fr 1fr; }
}

/*
 * The card the campaign leaves you sitting on.
 *
 * A title card: the last shot of the evening carried through behind it as a
 * dimmed ground, the ending's Cyrillic title small over its English title
 * large, one rule under them, the first line of the ending as prose, and the
 * figures as a single centred row. It used to be the same deck of panels as
 * every other watch with an operational headline at the top of it.
 */
.is-endcard { position: relative; }
.is-endcard.is-title-card { min-height: 78vh; display: flex; flex-direction: column; justify-content: center; }
.endcard-sky {
  position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1;
  object-fit: cover; image-rendering: pixelated; opacity: .34;
  filter: saturate(.7);
  pointer-events: none;
}
.is-title-card .endcard-block {
  position: relative; text-align: center; padding: 56px 60px 34px;
  /* The pool has to reach nothing INSIDE its own box, or the box itself is
     what you see: a rectangle of shade across the valley. */
  background: radial-gradient(54% 54% at 50% 50%, rgba(6, 8, 6, .93), rgba(6, 8, 6, .76) 55%, rgba(6, 8, 6, 0) 100%);
}
.endcard-tm {
  margin: 0 0 6px; font-size: 12px; letter-spacing: .34em; color: var(--ink-dim);
}
/* The file's own print, at the end of the file. */
.endcard-photo { display: inline-block; margin: 0 0 18px; }
.is-title-card .title { letter-spacing: .2em; }
.endcard-rule {
  width: 190px; margin: 14px auto 16px; border: 0; border-top: 1px solid var(--edge);
}
/* The watch's own name, under the title, where a subtitle goes. */
.endcard-watch { margin: 0 0 2px; letter-spacing: .2em; }
/*
 * The ending's first line reads as a paragraph, so it is set as one. Centred,
 * it came out as eight ragged lines on a phone.
 */
.is-endcard .ending-lede {
  max-width: 62ch; margin: 0 auto 18px; text-align: left;
  color: var(--ink); font-size: 15px; line-height: 1.62;
}
.is-title-card .subtitle.grave { margin: 0 auto 16px; }
.endcard-facts {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 28px;
  margin-top: 4px;
}
.endcard-facts span { display: flex; flex-direction: column; gap: 3px; }
.endcard-facts label { font-size: 9px; letter-spacing: .18em; color: var(--ink-dim); }
.endcard-facts b { font-size: 15px; color: var(--ink-bright); letter-spacing: .06em; }
.is-endcard-actions { justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 26px; }
.action-pair { display: flex; gap: 8px; }
.action-pair.is-quiet .btn { color: var(--ink-dim); border-color: color-mix(in srgb, var(--edge) 70%, transparent); }
.action-pair.is-quiet .btn:hover { color: var(--ink); border-color: var(--edge); }

/*
 * A rank board belongs on its own, not inline in a sentence.
 *
 * At twenty-one device pixels a recruit's board is indistinguishable from a
 * missing-glyph box, and it appeared that way three times in a new player's
 * first minute. It gets its own cell beside the name instead.
 */
/* the menu's file head: the print, the board and the name, on one line */
.ident-row { display: flex; align-items: center; gap: 14px; margin: 2px 0 12px; }
.ident-row .file-photo { width: 72px; height: 90px; border-width: 2px; }
.ident-row .file-ident-photo::after { width: 17px; height: 17px; right: 3px; bottom: 3px; }
.ident-board { display: block; flex: 0 0 auto; line-height: 0; }
/* a board with its rank written under it, so it is captioned rather than
   sitting on the form as an unexplained sprite */
.ident-board.is-captioned { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.ident-board.is-captioned small {
  line-height: 1.2; font-size: 8px; letter-spacing: .14em; color: var(--ink-dim);
  text-transform: uppercase; max-width: 68px; text-align: center;
}
.ident-name { display: flex; flex-direction: column; gap: 3px; }
.ident-name b { font-size: 15px; color: var(--ink-bright); letter-spacing: .06em; }
.ident-name small { font-size: 10px; letter-spacing: .14em; color: var(--ink-dim); text-transform: uppercase; }


/* the key under the map: the battery letters and what they are */
.map-key { margin: 8px 0 0; padding: 0; }
.map-key ul {
  display: flex; flex-wrap: wrap; gap: 4px 22px; margin: 0; padding: 0; list-style: none;
  font-size: 10px; letter-spacing: .1em; color: var(--ink-dim); text-transform: uppercase;
}
.map-key li { display: flex; align-items: baseline; gap: 6px; }
.map-key b { color: var(--ink-bright); font-size: 11px; }
.map-key li.is-own b, .map-key li.is-own { color: var(--good, #7fd6a2); }
.map-key small { font-size: 9px; opacity: .75; letter-spacing: .08em; }
.map-key-mark {
  font-size: 12px; font-weight: 700; letter-spacing: 0;
  fill: #dff0e2; stroke: #06120b; stroke-width: 2.8px; paint-order: stroke fill;
}

/*
 * The briefing's sector map.
 *
 * A drawn document, not a chart: a ruled ground, the frontier dashed across
 * the north, the batteries at their real reach, the defended places where they
 * really are and each raid axis on its real bearing, numbered to match the
 * paragraphs beside it.
 */
.card.is-situation { display: grid; grid-template-columns: minmax(320px, 480px) 1fr; gap: 22px; align-items: start; }
.card.is-situation h3 { grid-column: 1 / -1; }
.card.is-situation p { grid-column: 2; margin-bottom: 10px; }
.sector-map { grid-column: 1; grid-row: 2 / span 99; margin: 0; }
.sector-map svg { width: 100%; height: auto; display: block; border: 1px solid var(--edge); }
.map-ground { fill: color-mix(in srgb, var(--accent) 4%, #0b0f0c); }
.map-grid line { stroke: color-mix(in srgb, var(--edge) 55%, transparent); stroke-width: .5; }
.map-frontier line { stroke: var(--hostile); stroke-width: 1.2; stroke-dasharray: 7 5; }
.map-frontier text { fill: var(--hostile); font: 8px var(--font-mono); letter-spacing: .18em; }
.map-reach { fill: color-mix(in srgb, var(--good) 5%, transparent); stroke: color-mix(in srgb, var(--good) 45%, transparent); stroke-width: .8; }
.map-reach.is-own { fill: color-mix(in srgb, var(--accent) 8%, transparent); stroke: var(--accent); stroke-dasharray: 4 3; }
.map-site rect { fill: var(--good); }
.map-site.is-own rect { fill: var(--accent); }
/* Every name on the map is knocked out of the ground it sits on, so a label
   over a reach ring or a grid line is still a word and not a tangle. */
.sector-map text { paint-order: stroke fill; stroke: #050806; stroke-width: 3px; stroke-linejoin: round; }
.map-site text { fill: var(--ink-dim); font: 8.5px var(--font-mono); letter-spacing: .06em; text-anchor: middle; }
.map-site.is-own text { fill: var(--accent); }
.map-place rect, .map-place circle { fill: none; stroke: var(--ink); stroke-width: 1.2; }
/* a hair line from the mark to its name, for the names that had to move */
.map-leader { stroke: color-mix(in srgb, var(--ink-dim) 60%, transparent); stroke-width: .6; }
.map-place text { fill: var(--ink); font: 9px var(--font-mono); letter-spacing: .06em; }
.map-place.is-home circle { fill: color-mix(in srgb, var(--ink-bright) 35%, transparent); stroke: var(--ink-bright); stroke-width: 2; }
.map-place.is-home text { fill: var(--ink-bright); font-weight: 700; }
.map-axis line { stroke: var(--hostile); stroke-width: 1.6; }
.map-axis circle { fill: #0b0f0c; stroke: var(--hostile); stroke-width: 1; }
.map-axis text { fill: var(--hostile); font: 9px var(--font-mono); font-weight: 700; text-anchor: middle; }
.map-axis path, #map-arrow path { fill: var(--hostile); }
.map-scale line, .map-north line { stroke: var(--ink-dim); stroke-width: 1; }
.map-scale text, .map-north text { fill: var(--ink-dim); font: 8px var(--font-mono); letter-spacing: .1em; }
@media (max-width: 900px) {
  .card.is-situation { grid-template-columns: 1fr; }
  .card.is-situation p { grid-column: 1; }
  .sector-map { grid-column: 1; grid-row: auto; }
}

/* The four classes: a silhouette, the text, and the reach on one scale. */
.classes { display: flex; flex-direction: column; gap: 2px; }
.class-row {
  display: grid; grid-template-columns: 72px minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 16px; align-items: center;
  padding: 10px 0; border-bottom: 1px solid color-mix(in srgb, var(--edge) 45%, transparent);
}
.class-row:last-child { border-bottom: 0; }
.class-art svg { width: 64px; height: 32px; display: block; }
.class-art rect, .class-art circle { fill: var(--ink-dim); }
.class-art line { stroke: var(--edge); stroke-width: 1; }
.class-text { display: flex; flex-direction: column; gap: 3px; }
.class-text b { color: var(--ink-bright); letter-spacing: .08em; }
.class-reach { display: flex; flex-direction: column; gap: 5px; }
.reach-bar { position: relative; height: 7px; background: var(--bg-raised); border: 1px solid var(--edge); }
.reach-bar i { position: absolute; top: 0; bottom: 0; background: var(--accent); opacity: .75; }
@media (max-width: 700px) { .class-row { grid-template-columns: 64px 1fr; } .class-reach { grid-column: 1 / -1; } }
