/*
 * The chrome. Four layers, in this order:
 *
 *   1. tokens        the palette and the type scale everything is built from
 *   2. the UI kit    ui.ts's components -- panel, button, plate, readout,
 *                    meter -- styled once and reused by every screen
 *   3. the screens   sidebar, overlay, mission select, Boot Hill
 *   4. the layouts   the same screens rearranged for a phone
 *
 * Nothing below layer 2 invents its own button or its own border. That is the
 * whole point of having the kit: the mission select and the end-of-mission
 * panel cannot drift apart, because they are made of the same parts.
 *
 * Layer 4 is the reason layer 1 has a type scale rather than literal pixel
 * sizes. Every readable thing sizes from a token, and the tokens are redefined
 * per layout mode, so making the game legible on a phone is one block of
 * declarations rather than a hunt through six hundred lines. `data-layout` is
 * the only thing any of it branches on; `layout.ts` is the only thing that sets
 * it, and it decides from CSS pixels and orientation rather than from a
 * user-agent string, because a narrow desktop window has the same problem a
 * phone does and deserves the same answer.
 */

:root {
  /* Half again wider than it shipped, with a third more type -- the owner's
     numbers (200-qa 024). */
  --sidebar-w: 282px;
  /* Reserved bottom bar in portrait, before the home-indicator inset. */
  --bar-h: 104px;

  /*
   * The touch-target floor. 44px is the smallest thing a finger hits reliably,
   * and every control in the game is held to it -- buttons, difficulty tabs,
   * mission cards, roster plates once they became tappable.
   */
  --tap: 44px;

  /*
   * The type scale.
   *
   * The old sheet ran from 9px to 14px with up to 0.22em of letter-spacing on
   * the smallest of it, which is unreadable on a phone at arm's length and was
   * being "solved" by a media query that set `display: none`. Hiding the thing
   * the player needed was never the answer; sizing it was.
   */
  /*
   * Every size is a multiple of ten.
   *
   * ui/pixelface.ts builds the chrome face on a grid where one glyph pixel is
   * exactly one CSS pixel at font-size 10px -- so 10, 20, 30 and 40 land every
   * glyph edge on a whole device pixel and anything between them does not,
   * which is the softness this face exists to remove. There is no 24 here and
   * asking for one gets a blurred 20.
   *
   * The ladder doubled on 1 September: everything was set for a system font
   * and never revisited when the chrome became a bitmap face, which left the
   * result panel telling you a hostage had died in ten-pixel type. Body is 20
   * now, headings 30, titles 40 -- and every box that holds text had to grow
   * with it, which is the same instruction stated twice.
   */
  --face: "Boots & Bullets Pixel", ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  /*
   * The display face, for dialog titles and nothing else (700-qa 017).
   *
   * The owner, of the reference: "the font, 'The Armoury' we need this font,
   * it's thick and big." `--face` is five by seven on one-pixel strokes and no
   * font-size makes a one-pixel stroke thick -- scaled up it is a thin letter
   * drawn large, which is what these titles were. `--face-title` is eleven rows
   * on three-pixel stems (`src/title-glyphs.ts`), built by the same generator
   * from the same kind of table, so it is one type system and not two.
   *
   * It falls back to `--face` rather than to a system font: a heading in the
   * game's own smaller letterforms is a smaller heading, and a heading in
   * Consolas is the fault the whole file exists to remove.
   */
  --face-title: "Boots & Bullets Title", "Boots & Bullets Pixel", ui-monospace, monospace;
  /*
   * Every size here is a multiple of ten, and that is a hard constraint, not
   * a preference: `PIXEL` in ui/pixelface.ts is 100 units against an em of
   * 1000, so one glyph pixel is one CSS pixel at 10px, two at 20px, three at
   * 30px. Any other size is fractional and the letters go soft.
   *
   * There is no `--fs-xs`. It was referenced 24 times and defined nowhere
   * (600-infra 007), so every one of those rules silently dropped its
   * font-size and inherited. Those uses now read `--fs-sm`, which is what
   * they were resolving to on every layout anyway -- and the scale has no
   * legal step between 10px and 20px to give a real `xs`, so inventing one
   * would be a design decision rather than a repair.
   */
  --fs-micro: 20px;   /* keycaps, theatre notes -- decoration, not reading */
  --fs-sm: 20px;      /* hints, card meta */
  --fs-body: 20px;    /* roster plates, the objective line */
  --fs-lg: 30px;      /* button labels, the mission name */
  --fs-xl: 30px;      /* readout values */
  --fs-display: 40px; /* briefing and result titles */
  --fs-title: 40px;
  --fs-brand: 60px;

  /*
   * Tracking is a legibility cost that rises as type shrinks. Uppercase at 9px
   * with 0.22em between the letters reads as a texture rather than a word, so
   * the small tokens carry less of it and the large ones keep the look.
   */
  --track-tight: 0.06em;
  --track: 0.12em;
  --track-wide: 0.2em;

  /*
   * Olive, not emerald. The game's ground palette is a sunlit khaki green and
   * the chrome has to sit in the same family or it reads as a different game.
   *
   * **Three readable steps and one that is not** (310-qa 016). The owner
   * reported the pause screen's key hints as too faint to read, and measuring
   * the ramp against the plate they sit on found the bottom two thirds of it
   * below the 4.5:1 that small text needs:
   *
   *     on --plate #2a3117     before        after
   *       --ink                 9.49          9.49
   *       --ink-dim             4.49          6.74
   *       --ink-faint           2.33          5.00
   *
   * 2.33 is not a quiet label, it is an unreadable one, and it was carrying
   * nineteen pieces of real text -- every heading in the sidebar, the control
   * strip on two screens, the stat labels on the win panel, most of Boot Hill.
   * Somebody had already hit this and worked around it locally: #front-foot
   * hard-codes its own lighter olive with the comment "--ink-faint ... vanishes"
   * over the battlefield. Raising the token is that workaround generalised.
   *
   * Raising `--ink-faint` alone would have inverted the ramp -- it would have
   * come out brighter than `--ink-dim`. So both moved, and the order is now
   * monotone by measurement rather than by eye.
   */
  --ink: #d6dcae;
  --ink-dim: #b0bd83;
  --ink-faint: #96a36a;
  /*
   * Deliberately below reading contrast, at 2.33:1 -- the value `--ink-faint`
   * used to have. For the two things that genuinely want to recede: a rule
   * between sections, and the label on a disabled plate, where being hard to
   * read *is* the message. Never use it for text somebody is meant to read.
   */
  --ink-mute: #5f6a3d;
  --gold: #f0d878;
  --blood: #b4402a;
  /*
   * The two lettering shadows (700-qa 017).
   *
   * A drop shadow under a letter is the letter's own colour shaded, not black
   * -- three tones make a form, and a black shadow under gold reads as a hole
   * rather than as depth. `--gold-shadow` was already written as `#5c5210` in
   * three places before it had a name; `--ink-shadow` is the chrome ramp's own
   * shaded tone, so this adds a name and not a colour.
   */
  --gold-shadow: #5c5210;
  --ink-shadow: #2e3419;

  --plate: #2a3117;
  --plate-hi: #3d4623;
  --plate-lo: #161b0b;
  --well: #10140a;
  --edge: #0a0d05;

  /* Notch, home indicator, rounded corners. Zero on a desktop. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  /*
   * `dvh`, not `100%`. A percentage height collapses the moment a mobile
   * browser shows or hides its own chrome, which is every time the page is
   * scrolled -- and the fallback matters, because a browser without `dvh`
   * still has to get a full-height game.
   */
  height: 100%;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--edge);
  color: var(--ink);
  font: var(--fs-body)/1 var(--face);

  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* --- button */
/*
 * The chrome button, and it wears the plotted plate by default (310-qa 005).
 *
 * It used to be four CSS borders, with `.sheet-actions .ui-btn` and
 * `.result-actions .ui-btn` each re-applying the baked `--sk-btn` skin in an
 * identical five-line block. So the *skin* was the rule and the plain plate was
 * the exception, stated twice -- and a third screen building a `.ui-btn` got
 * the exception without knowing it. Which is the drift the owner was pointing
 * at: "make sure they are all using the same components".
 *
 * `.ui-btn` and `.fx-btn` still differ, and legitimately: this is dense and
 * left-aligned and carries hints and arrows, `.fx-btn` is the 280px hero button
 * on the front screen. Same skin, two sizes.
 */
.ui-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  min-height: var(--tap);
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.1s, transform 0.06s, border-color 0.1s;
}
/*
 * Hover lights the plate, so the label has to stop being light.
 *
 * `--ink` (#d6dcae) over a lit face measures **2.41:1** on olive, 2.39 on the
 * green and 2.41 on the rust -- against the 4.5:1 this game holds small
 * uppercase to. Every button in the game was doing it, and the tinted plates
 * from 330-qa 001 inherited it rather than introduced it.
 *
 * The plate goes bright; the ink goes dark. `--edge` over the same three faces
 * is 5.70 / 5.74 / 5.69, and dark type on a lit key is what a lit key looks
 * like -- so this reads as the control coming alive rather than as a colour
 * correction. Measured, not judged, per rules.md.
 *
 * The three numbers being the same number is not a coincidence and is the
 * thing to protect: green and rust are the olive plate re-hued at held
 * luminance (dispatch 01), so a tone differs from its neighbour in hue alone
 * and one measurement covers the family. Re-measure with
 * `node tools/probe-button-contrast.mjs` rather than editing these by hand --
 * they were stale for one release because the plates moved and the comment
 * did not.
 */
.ui-btn:hover, .ui-btn:focus-visible {
  border-image-source: var(--sk-btn-hot);
  color: var(--edge);
  outline: none;
}
/* The hint rides the same plate and needs the same treatment; `--ink-dim` over
   a lit face is worse than `--ink`, not better. `--well` keeps it a step back
   from the label without going pale again. */
.ui-btn:hover .ui-btn-hint,
.ui-btn:focus-visible .ui-btn-hint { color: var(--well); }
.ui-btn:hover .ui-btn-arrow,
.ui-btn:focus-visible .ui-btn-arrow { color: var(--well); }
/* Pressing pushes the plate into the bevel rather than moving the whole thing. */
.ui-btn:active { border-image-source: var(--sk-btn-down); }
/*
 * The two coloured buttons, and why they are plates rather than backgrounds.
 *
 * These rules used to be `background: #2f5222` plus a `border-top-color`, and
 * **neither could ever have worked.** `.ui-btn` above sets
 * `border-image: ... fill`: `fill` paints the source's middle slice over the
 * content box, on top of the background, and `border-image` replaces border
 * rendering so a border colour is inert. The tone was set by `hud.ts`, styled
 * here, and invisible on every button in the game (330-qa 001).
 *
 * `.ui-action` -- the touch pad's plates -- is the proof the tone names were
 * fine: it has no border-image, sets the same two colours as a plain
 * background, and shows them. One of the two worked and nobody noticed which.
 *
 * So a tone is a different bake now. `skin.ts` publishes six more properties;
 * these swap the *source*, which is the one thing that reaches the screen.
 */
.ui-btn.tone-good { border-image-source: var(--sk-btn-good); }
.ui-btn.tone-good:hover,
.ui-btn.tone-good:focus-visible { border-image-source: var(--sk-btn-good-hot); }
.ui-btn.tone-good:active { border-image-source: var(--sk-btn-good-down); }
.ui-btn.tone-warn { border-image-source: var(--sk-btn-warn); }
.ui-btn.tone-warn:hover,
.ui-btn.tone-warn:focus-visible { border-image-source: var(--sk-btn-warn-hot); }
.ui-btn.tone-warn:active { border-image-source: var(--sk-btn-warn-down); }

.ui-btn-body { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.ui-btn-label {
  font-size: var(--fs-body);
  font-weight: 700;
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.ui-btn-hint { color: var(--ink-dim); font-size: var(--fs-micro); letter-spacing: var(--track-tight); }

/* (The .ui-key keycap styling lived here until the owner had the shortcut
   caps stripped from every button; button() no longer renders one.) */

/* --- plate: chevrons instead of a border, which is how the original does it */
.ui-plate {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 7px;
  /*
   * Flat, not a ramp (310-qa 023).
   *
   * `docs/original-images/elements/buttons-and-troop-chevron-status.jpg` is one
   * flat mid-green -- measured, #243022 and a spread of JPEG noise either side
   * of it -- with a hard light edge along the top and black between the rows.
   * Ours was a vertical `linear-gradient`, which is a blend, and blends are why
   * the owner read the roster as "too clean": every plate had a soft sheen no
   * 1993 machine could have drawn.
   *
   * The bevel survives, because the bevel is real: a lit top edge and a dark
   * bottom one are two hard lines, not a gradient.
   */
  background: #2e3719;
  border-top: 1px solid #47522a;
  border-bottom: 1px solid var(--plate-lo);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.ui-plate-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ui-plate-value { color: var(--gold); }

/*
 * The wire's lamp on the rail, in a match (qa-multiplayer-sep-07 007).
 *
 * The owner, once Q2 kept the twenty-five-second grace: *"we need to have a
 * network status on the left side somewhere, something simple, perhaps some
 * coloured circles and one bit of text."* A pause you cannot see the reason
 * for is indistinguishable from a game that has frozen.
 *
 * Round, which the chrome already allows itself -- the corner tool, the
 * cooldown ring and the thumbstick are all circles. The pixel law is about
 * what is *plotted*, and nothing here is a sprite.
 *
 * The three colours are the ones already in the palette: the seat green the
 * lobby writes nicknames in, `--gold` for a warning the player can wait out,
 * and `--blood` for the one he cannot. The ring is the plate's own light edge,
 * so a lamp reads as set into the plate rather than laid on it.
 */
.hud-lamp {
  width: 8px;
  height: 8px;
  flex: none;
  margin-left: auto;
  border-radius: 50%;
  background: var(--ink-mute);
  box-shadow: 0 0 0 1px var(--plate-lo);
}
.hud-lamp.ok { background: #9bd06a; }
.hud-lamp.away { background: var(--gold); }
.hud-lamp.down { background: var(--blood); }
/* The lamp takes the right-hand gap, so the text sits next to it rather than
   at the far end of the plate with the lamp stranded in between. */
.hud-lamp + .ui-plate-value { margin-left: 7px; }

/* A dead soldier keeps his plate. That is the point of the plate. */
.ui-plate.dead {
  background: #241210;
  border-top-color: #4a2018;
  color: #7a5148;
  text-decoration: line-through;
  text-decoration-color: var(--blood);
}

/* --- readout */
.ui-readout { display: flex; align-items: baseline; gap: 7px; padding: 2px 7px; }
.ui-readout-value { color: var(--gold); font-size: var(--fs-xl); font-weight: 700; }
.ui-readout-label {
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.ui-readout-value.pulse { animation: ui-pulse 0.32s ease-out; }
@keyframes ui-pulse {
  0% { color: #fff6d0; transform: scale(1.35); }
  100% { color: var(--gold); transform: scale(1); }
}

/* --- meter */
.ui-meter { padding: 4px 7px; }
.ui-meter-head {
  display: flex;
  justify-content: space-between;
  margin-bottom: 4px;
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.ui-meter-text { color: var(--gold); }
/*
 * A match's clock, bigger (502 009).
 *
 * The owner asked for the countdown on its own line and larger. The line is
 * the sidebar's timer, which it now has; the size is this. It sits on
 * `.big` rather than on a multiplayer selector because the meter is a
 * component and does not know what mode it is in -- `sidebar.ts` does.
 *
 * The head stops being a row: at this size the label and the number side by
 * side leave the number nowhere to grow, so the label goes above it.
 */
.ui-meter.big .ui-meter-head {
  display: block;
  margin-bottom: 2px;
}
.ui-meter.big .ui-meter-text {
  display: block;
  font-size: var(--fs-lg);
  line-height: 1.05;
  letter-spacing: var(--track-tight);
}
/* In the MATCH group the track sits over a plate (STANDING) that runs the
   full width of the column; the meter's own 7px padding left the track
   inset from it by a hand's width for no reason a reader could see
   (750-qa-multiplayer 009, measured blind at 9px). The label keeps the
   padding, so it stays on the text grid with the headings. */
.ui-meter.big .ui-meter-track { margin: 0 -7px; }
.ui-meter-track { height: 6px; background: var(--well); box-shadow: inset 0 0 0 1px var(--plate-lo); }
/* Two hard bands, not a ramp: a lit top half over a shaded bottom is what a
   bevelled bar looks like when every pixel has to be one tone or the other. */
.ui-meter-fill {
  display: block;
  height: 100%;
  background: linear-gradient(#d8b64c 0 50%, #9a7a20 50% 100%);
}

/* --- heading */
.ui-heading {
  /*
   * Twenty-two above, so the three sidebar groups read as three things rather
   * than one list -- the owner's ask. `.hud-squad` overrides it back down,
   * because the first heading sits under the mission plate and does not need
   * separating from anything above it (310-qa 023).
   */
  margin: 22px 0 5px;
  padding: 0 7px;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}
/*
 * The rule under a heading: three hard bands, not a fade.
 *
 * It was `linear-gradient(90deg, var(--plate-hi), transparent)` -- a smooth
 * ramp to nothing, on screen for the whole of every mission, three times over.
 * The stops are hard now (`0 42%`, `42% 74%`, ...), so each band is one flat
 * tone and nothing is interpolated: the same stepped bargain the dither makes
 * in the world. It still reads as trailing off, which is what it is for.
 */
.hud-squad .ui-heading { margin-top: 8px; }
.ui-heading::after {
  content: "";
  display: block;
  height: 1px;
  margin-top: 4px;
  background: linear-gradient(90deg,
    var(--plate-hi) 0 42%,
    #333c1c 42% 74%,
    #232a12 74% 100%);
}

/* =============================================================== the stage */
/*
 * A grid rather than a flex row, because the three layouts are three different
 * arrangements of the same two children and a grid can express all of them
 * without the markup changing. `wide` puts the HUD in a column beside the
 * viewport, `stacked` puts it in a row beneath, and `compact` takes it out of
 * flow entirely and floats it in the corners -- see the layouts section.
 */

#stage {
  display: grid;
  height: 100%;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}

/*
 * The mission clock, top left of the battlefield (740-qa 010). A small plate
 * in the dim ink: the owner asked for "nothing too distracting". Same
 * rivetless plate the toast and the cards wear, so it is chrome and not a
 * label floating on the grass.
 */
.hud-clock {
  position: absolute;
  top: calc(8px + var(--safe-t));
  left: 8px;
  z-index: 3;
  padding: 3px 8px;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  line-height: 1;
  pointer-events: none;
}
.hud-clock[hidden] { display: none; }

/* The canvas's positioning context, and what the action bar is anchored to. */
#viewport {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

#screen {
  display: block;
  width: 100%;
  height: 100%;
  cursor: crosshair;
  /*
   * Without this the browser claims every drag for its own scrolling and pinch
   * for its own zoom, and the game never sees either. `user-scalable=no` has
   * been ignored by Safari since iOS 10; this is what actually works.
   */
  touch-action: none;
  -webkit-touch-callout: none;
}

/*
 * The mark on the black between missions (320-qa 003).
 *
 * Above `#blackout` and below the overlay, centred, and nothing else on the
 * screen. It is deliberately not the boot screen: no wordmark, no name, one
 * line of type. The lamps themselves are plotted into the canvas -- see
 * `ui/loading.ts` -- so there is nothing here but placement.
 */
#loading {
  position: fixed;
  inset: 0;
  z-index: 11;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  pointer-events: none;
}
#loading[hidden] { display: none; }
#loading canvas { image-rendering: pixelated; }
.loading-word {
  margin: 0;
  min-height: 1em;
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-wide);
}

/* --------------------------------------------------------------- sidebar */
/*
 * Down the left edge, as in the original. The column exists mainly to hold the
 * roster: a casualty should read as a name leaving a list, not as a counter
 * going down.
 */
/*
 * The sidebar keeps the small ladder.
 *
 * When the chrome doubled to 20/30/40 this column doubled with it, and a
 * 188px rail set for ten-pixel type burst at twenty -- every hint wrapped,
 * ORDERS ran to two lines, the roster barely fit. The owner asked for half
 * again, and half of this ladder is exactly the one the sidebar was designed
 * around. Scoped here rather than changed globally, so a dialog and the rail
 * can disagree about size while reading from one set of names -- and 15 is
 * not on the bitmap grid, so 30 halves to 20, not to a blur.
 */
#hud {
  --fs-micro: 13px;
  --fs-sm: 13px;
  --fs-body: 13px;
  --fs-lg: 26px;
  --fs-xl: 26px;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  /* The flat rail, which is what `compact` and `stacked` keep. The wide
     layout's plotted frame is the block below. */
  padding: calc(10px + var(--safe-t)) 6px 8px calc(6px + var(--safe-l));
  background: #1d2410;
  border-right: 2px solid var(--edge);
  box-shadow: inset -1px 0 0 var(--plate-lo);
  overflow: hidden;
}

/*
 * The rail wears the plotted frame, like every other panel in the game -- and
 * only in the layout that has a column to put it in.
 *
 * It was a flat fill with a hairline down one edge, against a result panel, a
 * briefing card and a settings sheet that all carry real chrome. The owner:
 * "it should be in a sprite frame, currently it looks too flat" (320-qa 008).
 *
 * Exactly the form the briefing box uses, and the note there is the one that
 * matters: `--sk-slice-frame` is a bare number, so the shorthand cannot use it
 * as the border-width and `fill / 18px` is what actually draws it.
 *
 * **The border replaces the padding rather than adding to it.** The rail is
 * 282px and was measured around ten-pixel type that has since doubled; it has
 * burst once already (see the ladder note above). 18px of frame either side
 * takes 36px out of it, so the old 6px padding goes and the frame's own ledge
 * is the inset -- 246px of content against 270px, with the roster, ORDERS and
 * the hint text looked at again at that width rather than assumed.
 *
 * Scoped rather than applied to `#hud` outright, because the other two layouts
 * are not a rail. `compact` floats the HUD over the canvas and already resets
 * the border to nothing; `stacked` is a bar along the bottom of a phone and
 * resets only `border-right`, so an unscoped frame would leave 18px of chrome
 * down the left and bottom of a strip and paint a picture frame round it.
 * A frame that is right on a desktop and wrong on a phone is not a frame to
 * ship everywhere. `layout.ts` always writes `data-layout`, so there is no
 * state in which this is neither scoped nor overridden.
 */
[data-layout="wide"] #hud {
  padding: calc(var(--safe-t)) 0 0 calc(var(--safe-l));
  background: none;
  box-shadow: none;
  image-rendering: pixelated;
}

.hud-mission { padding: 0 7px 2px; }
.hud-mission b {
  display: block;
  color: var(--gold);
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  line-height: 1.15;
}
.hud-diff {
  display: inline-block;
  margin-top: 6px;
  padding: 2px 6px;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.diff-rookie { background: #2f4a24; color: #bfe39a; }
.diff-veteran { background: #5a4118; color: #f0c878; }
.diff-elite { background: #5c2018; color: #f0a08c; }

.hud-body { flex: 1; min-height: 0; overflow: hidden; }
.hud-roster { display: flex; flex-direction: column; gap: 2px; }
.hud-objective {
  padding: 0 7px;
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.45;
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}
/* A hold whose clock is paused: the whole line goes amber, stepped, no fade —
   the same tone the difficulty chip already uses for veteran. */
.hud-objective.stalled { color: #f0c878; }

.hud-hint {
  padding: 8px 7px 0;
  border-top: 1px solid var(--plate-lo);
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  line-height: 1.8;
}
.hud-hint b { color: var(--ink-dim); font-weight: 700; }

/* --------------------------------------------------------------- overlay */
/*
 * The sidebar offset used to be baked in here as `inset: 0 0 0 var(--sidebar-w)`,
 * which quietly hard-coded one of the three layouts into the overlay geometry.
 * It is now a plain full-bleed overlay, and only `wide` shifts it.
 */
/* --- the blackout: one fade for the whole screen, canvas and chrome alike.
   Above the sidebar (3), the action bar (4) and the thumbstick (7), and below
   the overlay -- which is raised past it for exactly this reason, so the
   results panel and the briefing land *on* the black rather than under it.
   Not a canvas fill: the sidebar is DOM outside the canvas and nothing painted
   in there can ever reach it, which is the whole reason this element exists. */
#blackout {
  position: fixed;
  inset: 0;
  background: #000;
  opacity: 0;
  z-index: 8;
  pointer-events: none;
}

#overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(12px, var(--safe-t)) max(12px, var(--safe-r)) max(12px, var(--safe-b)) max(12px, var(--safe-l));
  background: rgba(8, 11, 5, 0.78);
  pointer-events: none;
  /* Above #blackout, so an end-of-mission panel is readable on the black
     rather than buried under it. */
  z-index: 12;
}
#overlay[hidden] { display: none; }
/* The briefing is a caption; the result panel is a thing you click. */
#overlay.interactive { pointer-events: auto; }
/*
 * The card holder is a hand for `fill()` and nothing else, so it gets out of
 * the way (qa-polish 001): `display: contents` makes whichever panel it holds
 * the direct flex item of `#overlay`, which is what centres it and what its
 * `max-height` has to measure against. As a box in between, it sized itself to
 * its content and a panel taller than the screen overflowed a `position:
 * fixed` parent that cannot scroll -- unreachable rather than merely long.
 */
#overlay-card { display: contents; }
.briefing-num {
  color: var(--ink);
  font-size: var(--fs-lg);
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.briefing-theatre {
  margin-top: 2px;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: 0.28em;
  text-transform: uppercase;
}
/* The mission's name, centre of the head row. The rules above and below it
   went with the stack: it sits between two other columns now, which separates
   it from them without a line (700-qa 005). */
.briefing-title {
  color: var(--gold);
  font-family: var(--face-title);
  font-size: var(--fs-display);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: uppercase;
  text-shadow: 4px 4px 0 var(--gold-shadow);
}

.briefing-box {
  margin-top: 12px;
  padding: 14px 20px 16px;
  /* Exactly the form the result panel uses. `--sk-slice-frame` is a bare
     number (18, not 18px), so it cannot be used as the border-width -- the
     shorthand goes invalid, the border collapses to nothing, and border-image
     has no box to paint into. `fill / 18px` is what actually draws it. */
  background: none;
  image-rendering: pixelated;
}
.briefing-box-head {
  color: var(--ink);
  font-size: var(--fs-lg);
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.briefing-obj {
  margin-top: 14px;
  color: var(--ink);
  font-size: var(--fs-body);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.briefing-line { margin-top: 10px; color: var(--ink-dim); font-size: var(--fs-body); }
/* The modifiers and the par, inherited from the dialog 004 deleted. `.mi-chip`
   and `.mi-best` keep their names: it is the same information wearing the same
   clothes, on a different screen (700-qa 004). */
.briefing-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
}

/* The controls strip and the squad roll came off the briefing (700-qa 005):
   the owner asked for the troops out and the controls elsewhere, and both are
   already elsewhere -- the roll on the sidebar for the whole mission, the
   controls on the pause sheet from the same `controlLines()`. `.briefing-ctl*`
   is kept because the sheet's strip is built by the same function. */

/* The way out, under the controls. Gold, because it is the one instruction on
   this screen that is about the screen itself. */
/*
 * The rung, on the card where a mission is set up.
 *
 * Chips rather than buttons: `hud-diff` and its per-rung colours are the
 * sidebar's, and one place should decide what a difficulty looks like. The
 * unselected ones are held back with a filter rather than a second palette,
 * so adding a fourth rung needs no CSS here (320-qa 007).
 */
.briefing-diff {
  /*
   * A grid rather than a flex row, and `1fr` rather than a width (330-qa 004).
   *
   * The owner asked for them "the same width". ROOKIE, VETERAN and ELITE are
   * three different lengths, so equal width has to come from the container --
   * and `repeat(auto-fit, minmax(0, 1fr))` gets it without anybody writing the
   * number three down, which is the same argument that keeps `diff-<id>` as
   * the colour source: a rung added or dropped needs nothing here.
   */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: stretch;
  gap: 8px;
  margin: 14px 0 2px;
  /*
   * The one thing on the briefing that is pressed rather than read.
   *
   * `#overlay` is `pointer-events: none` while a briefing is up -- deliberately,
   * because the card is a caption and the press that dismisses it belongs to
   * whatever is behind. So the row has to opt back in, the same way
   * `[data-layout="compact"] #hud > *` does, or the press lands on the canvas
   * and `main.ts` never sees a `.briefing-diff` to spare. Measured: without
   * this, pressing ELITE dismissed the briefing and changed nothing.
   */
  pointer-events: auto;
}
/*
 * The one-off word about the rung above (900-onboarding 003).
 *
 * A caption under the buttons, in the Major's voice but not in his panel, so
 * it reads at the weight of a note rather than of an instruction. `--ink-dim`
 * is 6.7:1 against the plate and safe for text; `--ink-mute` at 2.3 is not,
 * and this is a sentence somebody is meant to read rather than a rule.
 *
 * `pointer-events` is deliberately left alone. `.briefing-diff` opts back in
 * because it is pressed; this is read, and a press on it should dismiss the
 * briefing like a press on any other part of the card.
 */
.briefing-rung-note {
  margin: 6px 2px 0;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.45;
}
/*
 * The rungs, as buttons.
 *
 * `hud-diff` is sized for the sidebar, where it is a status readout: micro
 * type, 2px of padding, `inline-block`. Here it is the one thing on the screen
 * you press, so it gets a tap target, the body size, and a bevel -- while the
 * `diff-<id>` background underneath stays exactly where the colour comes from.
 *
 * The bevel is CSS borders rather than the plotted `--sk-btn`, and deliberately:
 * that plate is opaque and would cover the rung colour, which is the whole
 * lesson of 330-qa 001. Two hard 1px edges, light from the top left, is the
 * same construction `paintBevel` makes -- no gradient, no blur, no alpha.
 */
/* A column now: stars over the name, as the reference draws them. A mission
   that fixes its rung (701-chaos 003) shows the same plate as a fact rather
   than a button: same colour, lit, and nothing to press. */
.briefing-diff-btn,
.briefing-diff-fixed {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: var(--tap);
  margin-top: 0;
  padding: 8px 6px;
  border: 1px solid var(--diff-lo, var(--plate-lo));
  border-top-color: var(--diff-hi, var(--plate-hi));
  border-left-color: var(--diff-hi, var(--plate-hi));
  font-size: var(--fs-sm);
  cursor: pointer;
  font-family: inherit;
  filter: saturate(0.35) brightness(0.72);
}
/*
 * Solid, and each rung's own hue -- no alpha anywhere.
 *
 * A translucent white edge would have been one line and it is the thing this
 * renderer does not do: `rules.md` bans alpha outright. (This once claimed the
 * only rgba here was a full-screen scrim; there are seventeen, which is its own
 * clean-up and not this rule's excuse.) So the bevel is two measured tones
 * per rung, derived off the background it sits on -- lightness +15 points for
 * the lit edge, -8 for the shaded one, hue and saturation untouched. The same
 * construction `paintBevel` makes, in CSS.
 */
.briefing-diff-btn.diff-rookie, .briefing-diff-fixed.diff-rookie { --diff-hi: #507d3d; --diff-lo: #1e2f17; }
.briefing-diff-btn.diff-veteran, .briefing-diff-fixed.diff-veteran { --diff-hi: #966d28; --diff-lo: #3a2a0f; }
.briefing-diff-btn.diff-elite, .briefing-diff-fixed.diff-elite { --diff-hi: #993528; --diff-lo: #3c1510; }
.briefing-diff-btn:hover,
.briefing-diff-btn:focus-visible { filter: saturate(0.7) brightness(0.9); }
/* Pressing sinks the plate into its own bevel, the way `.ui-btn:active` does. */
.briefing-diff-btn:active {
  border-top-color: var(--diff-lo, var(--plate-lo));
  border-left-color: var(--diff-lo, var(--plate-lo));
  border-right-color: var(--diff-hi, var(--plate-hi));
  border-bottom-color: var(--diff-hi, var(--plate-hi));
}
.briefing-diff-btn.on {
  filter: none;
  box-shadow: inset 0 0 0 2px var(--gold);
}
/* Lit like a chosen rung, because it is the rung; not a pointer, because it
   is not a choice. The plate sits alone in its row and takes the full width. */
.briefing-diff-fixed { filter: none; cursor: default; }
/* One, two, three -- the same plotted star the result panel and the level
   select wear, at the 34px the card uses. It says the rung in the shape the
   rest of the game says it in. */
.briefing-diff-stars { display: flex; gap: 4px; }
.briefing-diff-name { font-size: var(--fs-sm); letter-spacing: var(--track-tight); }

/*
 * DEPLOY. It was the line CLICK TO CONTINUE, which was right for a card that
 * only had to be read; this screen has controls on it now, and a line of text
 * does not say which of them starts the mission (700-qa 005).
 */
.briefing-go {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  margin: 14px auto 0;
  padding: 8px 40px;
  border: 10px solid transparent;
  /* The lit plate, not the gold one: the reference sets DEPLOY in gold, and
     gold on gold is not a word. Gold is the ink here, so the plate under it
     has to be the olive one. */
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  background: none;
  color: var(--gold);
  font: inherit;
  font-size: var(--fs-lg);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  cursor: pointer;
  /* Like the rungs: `#overlay` is `pointer-events: none` while a briefing is
     up, so anything meant to be pressed has to opt back in. */
  pointer-events: auto;
}
.briefing-go:hover, .briefing-go:focus-visible {
  border-image-source: var(--sk-btn-hot);
  color: #2a1c02;
  outline: none;
}
.briefing-go .fx-star { width: 17px; height: 17px; }

/* --- the loadout row ------------------------------------------------------
 *
 * Three columns of equal width, whatever their names are -- the same argument
 * `.briefing-diff` makes, and for the same reason: a fourth column one day
 * should need nothing here. */
.briefing-kit {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: start;
  gap: 14px;
  /* Pressed, not read. See `.briefing-diff`. */
  pointer-events: auto;
}
.bl-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bl-label {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-wide);
}
.bl-box {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  width: 100%;
  padding: 8px 6px;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
}
/* A fixed height, so cycling onto NONE -- which has no portrait -- does not
   collapse the column and jump everything under it. */
.bl-art { height: 48px; display: grid; place-items: center; }
.bl-art canvas { display: block; image-rendering: pixelated; }
.bl-arrow {
  background: none;
  border: 0;
  padding: 4px 10px;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-display);
  line-height: 1;
  cursor: pointer;
}
.bl-arrow:hover, .bl-arrow:focus-visible { color: var(--ink); outline: none; }
/* Nothing to cycle to. Dimmed, but still live: it answers with a toast rather
   than swallowing the press (700-qa 005). */
.bl-arrow[aria-disabled="true"] { color: var(--ink-mute); cursor: default; }
.bl-name { color: var(--ink); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }
/* How many there are and which this is. Squares, not circles: a two-pixel
   circle is a curve, and this game does not draw those. */
/* The dots and the rocker on one line (qa-polish 007): "which one" and "how
   many" are one question, and stacked they cost the tallest column a row that
   the whole card then had to be tall enough for. */
.bl-meta { display: flex; align-items: center; justify-content: center; gap: 10px; }
.bl-dots { display: flex; gap: 5px; }
.bl-dot { width: 5px; height: 5px; background: var(--ink-mute); }
.bl-dot.on { background: var(--gold); }
/* --- the rocker: how many of that kind to take --------------------------
 *
 * Three cells, as the reference draws it. The number is boxed rather than
 * bare so the row reads as one control rather than as two buttons that
 * happen to have a digit between them. */
.bl-rock { display: flex; align-items: stretch; gap: 6px; }
.bl-step {
  min-width: 38px;
  padding: 2px 0;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
}
.bl-step:hover, .bl-step:focus-visible { border-image-source: var(--sk-btn-hot); outline: none; }
/* At either end. Dimmed, still live: it says which end it is at rather than
   swallowing the press -- the rule the arrows and the Armoury's BUY keep.
   The quiet plate, not the off one: it is not dead, it is just not the way
   out of here (dispatch 02). */
.bl-step[aria-disabled="true"] {
  border-image-source: var(--sk-btn-quiet);
  color: var(--ink-mute);
  cursor: default;
}
.bl-count {
  min-width: 48px;
  display: grid;
  place-items: center;
  padding: 2px 8px;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
  color: var(--gold);
  font-size: var(--fs-lg);
  line-height: 1;
}
.bl-blurb {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  line-height: 1.25;
  /* Wide enough that the longest blurb in the table takes two lines rather
     than three -- the column is 320px and this is what fits it. */
  max-width: 34ch;
}
.briefing-ctl { display: contents; }
.briefing-ctl-a {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}
.briefing-ctl-k {
  color: var(--ink);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-align: right;
}

/* The pause sheet's copy of the controls strip: the same two columns, sized
   to the sheet rather than to the briefing box. */
.sheet-controls {
  margin: 18px auto 0;
  padding-top: 14px;
  border-top: 2px solid var(--ink-mute);
  display: grid;
  grid-template-columns: auto auto;
  justify-content: space-between;
  column-gap: 18px;
  row-gap: 5px;
  text-align: left;
  width: 100%;
}

/* --- the comms panel (201-qa 007)
 *
 * Anchored to #viewport, not to the page, for the same reason the action bar
 * is: it belongs inside the battlefield on a phone and inside the play area
 * beside the sidebar on a desktop.
 *
 * It used to clear the action bar's bottom-right cluster, which is why the
 * touch breakpoint below lifted it by a whole tap target. **That cluster is
 * gone** with the Centre plate it held (310-qa 006), and the clearance went
 * with it -- a phone was otherwise reserving 58px of map for a button that no
 * longer exists. The pause plate is the only plate left and it is top-left, so
 * nothing down here needs dodging.
 *
 * Wears the plotted banner. Nothing new is baked for it.
 */
#comms {
  /*
   * One cell of the portrait strip on screen, and the bezel that rings it.
   *
   * 64px source at 2x. Twice the portrait this panel carried before, which is
   * what the owner asked for -- and a 96px cut at 2x, which was the first
   * answer, made the panel about two and a half times its old height and ate
   * the bottom of the map. 64 is also the size that lets the phone breakpoint
   * drop to 1x: every scale here is a whole number, because half a pixel of a
   * pixel sprite is the one thing that would give it a soft edge.
   */
  --comms-face-px: 128px;
  --comms-bezel-px: 144px;
  --comms-inset: calc((var(--comms-bezel-px) - var(--comms-face-px)) / 2);

  position: absolute;
  /*
   * Centred by auto margins between left:0 and right:0, not by `left: 50%`
   * with a -50% translate.
   *
   * The translate version shrink-wrapped into the *right half* of the play
   * area -- an absolutely positioned box sizes itself against the space from
   * its `left` to the container edge -- so a long line wrapped to five rows in
   * a 495px panel while 760 was available and three would have done. Five rows
   * of panel over the bottom of the map is a real cost on a map you are trying
   * to read.
   *
   * `width` rather than the `fit-content` + `max-width` this carried before:
   * with a 208px portrait beside it, fit-content still squeezed the line into
   * six short rows, because the widest thing the text *has* to be is one word.
   */
  left: 0;
  right: 0;
  margin: 0 auto;
  width: min(780px, calc(100% - 32px));
  bottom: calc(10px + var(--safe-b));
  z-index: 5;
  padding: 2px 8px;
  background: none;
  /* Its own plate, not the shared banner. A banner is a ribbon -- pointed at
     both ends, meant to carry a mission name -- and this is a slab someone is
     talking out of. The slice is four numbers because the bottom notches are
     anchored to the bottom corners and must land inside the side slices; see
     skin.ts. */
  border-style: solid;
  border-width: 12px 30px 18px 30px;
  border-image: var(--sk-comms) var(--sk-slice-comms) fill / 12px 30px 18px 30px round;
  image-rendering: pixelated;
  text-align: center;
  pointer-events: none;
  /* Off the bottom of the play area until it is asked for. */
  transform: translateY(calc(100% + 24px));
  opacity: 0;
  /* The transform's duration is CONFIG.timing.commsExit, published by
     installSkin -- `hideComms` waits exactly this long (600-infra 041). */
  transition: transform var(--t-commsExit) cubic-bezier(0.18, 1.5, 0.4, 1), opacity 0.2s linear;
}
#comms[hidden] { display: none; }

/* With a face, the strip becomes two columns: the portrait on the left, as the
   brief asked, and what he is saying beside it. Aligned to the *top* rather
   than centred -- a portrait this tall centred against three lines of text
   floats, where lining its head up with his name reads as a person speaking. */
#comms.with-face {
  display: flex;
  align-items: flex-start;
  gap: 18px;
  text-align: left;
}

/*
 * The portrait: a bezel-sized box, a cell-sized child, and the bezel on top.
 *
 * The strip is the child's background, moved by `--comms-face-f` -- a frame
 * *number* that ui/comms.ts writes on the parent on a timer and the child
 * inherits. One decoded image and an offset, rather than a different data URL
 * per frame: swapping the image made the first play of each frame a fresh
 * decode, and a talk loop reaches every frame inside the first second of a
 * line, so the first cycle flashed.
 *
 * The child is exactly one cell (740-qa 007). The strip used to be a second
 * background on this bezel-sized element, which is 16px wider than a cell, so
 * eight pixels of the next frame were inside the box and showed through the
 * bezel's chamfered corners. The bezel is the `::after`, so it still paints
 * over the portrait and he is set into the panel rather than stuck onto it.
 */
.comms-face {
  --comms-face-f: 0;
  position: relative;
  flex: 0 0 auto;
  width: var(--comms-bezel-px);
  height: var(--comms-bezel-px);
  image-rendering: pixelated;
}
.comms-portrait {
  position: absolute;
  left: var(--comms-inset);
  top: var(--comms-inset);
  width: var(--comms-face-px);
  height: var(--comms-face-px);
  background-repeat: no-repeat;
  background-position: calc(-1 * var(--comms-face-f) * var(--comms-face-px)) 0;
  background-size: calc(var(--comms-face-n) * var(--comms-face-px)) var(--comms-face-px);
  image-rendering: pixelated;
}
.comms-face::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--sk-bezel) 0 0 / var(--comms-bezel-px) var(--comms-bezel-px) no-repeat;
  image-rendering: pixelated;
  pointer-events: none;
}
.comms-said { flex: 1 1 auto; min-width: 0; padding-top: 6px; }
#comms.in {
  transform: translateY(0);
  opacity: 1;
}
/* The overshoot is the whole of the "bounces in" the brief asked for, so under
   prefers-reduced-motion it is the whole of what goes. */
#comms.still { transition: opacity 0.2s linear; }

.comms-who {
  display: block;
  color: var(--gold);
  font-size: var(--fs-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
.comms-line {
  display: block;
  color: var(--ink);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  line-height: 1.45;
  /* Reserve the space the finished line will need, so the plate does not grow
     a row at a time while type() appends. Invisible on a one-line banner; very
     visible on a plate this tall with a portrait holding its height. */
  min-height: calc(3 * 1.45em);
}

/*
 * Above the action bar's cluster rather than beside it, so the strip does not
 * move when a player switches hands. The cluster is roughly a tap target tall
 * plus its own inset.
 *
 * The portrait drops to 1x here, not to something in between: at 360px wide a
 * 192px portrait leaves about 130px for the text and turns three lines into
 * eight. Also keyed on height, because a landscape phone has the width and not
 * the room.
 */
@media (max-width: 900px), (max-height: 560px) {
  #comms {
    --comms-face-px: 64px;
    --comms-bezel-px: 72px;
    /* 20px, not 20px + --tap: the plate that clearance was for is gone. */
    bottom: calc(20px + var(--safe-b));
    border-width: 10px 22px 14px 22px;
    border-image-width: 10px 22px 14px 22px;
    width: calc(100% - 20px);
  }
  #comms.with-face { gap: 10px; }
  .comms-line { min-height: calc(4 * 1.45em); }
}
.result-diffline { margin-top: 8px; }
.result-title {
  font-size: var(--fs-title);
  font-weight: 700;
  letter-spacing: var(--track);
  text-transform: uppercase;
}
.result.win .result-title { color: var(--gold); text-shadow: 0 3px 0 var(--gold-shadow); }
.result.lose .result-title { color: #e07050; text-shadow: 0 3px 0 #52180d; }
.result-sub { margin-top: 10px; color: var(--ink-dim); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }

/* The two rolls share a row: stacked, they were the whole of the win panel's
   overflow at 1366x768 (310-qa 026). `wrap` so a narrow window stacks them
   again rather than crushing both. */
.result-rolls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 0 24px;
}
.result-roll { margin: 20px auto 0; max-width: 420px; flex: 1 1 240px; }
.result-roll-head {
  margin-bottom: 7px;
  color: var(--blood);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}
.result-roll .ui-plate {
  background: #241210;
  border-top-color: #4a2018;
  color: #a8756a;
  margin-bottom: 2px;
}
/* --- the three numbers, under the sub-line.
 *
 * A key/value list rather than a row of chips: the values are different widths
 * and a chip row wraps into a ragged block the moment one of them is long. The
 * keys sit faint and the values bright, so the eye lands on the numbers. */
.result-stats {
  margin: 16px auto 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 480px;
  width: 100%;
}
.result-stat {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
.result-stat-k { color: var(--ink-faint); text-transform: uppercase; }
.result-stat-v { color: var(--ink); text-align: right; }

.result-perfect { margin-top: 18px; color: #a8c07a; font-size: var(--fs-sm); }

/* --- the challenge score (qa-polish 012) ---------------------------------
 *
 * The owner: "highlight and make bigger the stats LASTED and KILLS, center
 * align, number under it." A challenge is one number, not a debrief, so its
 * stats are cells rather than the mission panel's key-left/value-right rows --
 * label small above, figure large beneath, side by side and centred.
 *
 * Its own classes rather than a reshaping of `.result-stat`, which the mission
 * panel still uses in its two-column form and should keep.
 *
 * Built from the map's `challenge.score`, so two is the case to lay out for
 * and three has to survive: `flex` with `1 1 0` cells rather than a fixed
 * two-column grid.
 */
.score-stats {
  display: flex;
  justify-content: center;
  gap: clamp(20px, 6vw, 56px);
  margin-top: 22px;
}
.score-stat { flex: 0 1 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.score-stat-k {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}
.score-stat-v {
  color: var(--gold);
  font-family: var(--face-title);
  font-size: var(--fs-xl);
  font-weight: 700;
  line-height: 1;
  text-shadow: 3px 3px 0 var(--gold-shadow);
}
/* The mark, under the pair and quieter: it is the reason to go again. */
.score-best {
  margin-top: 14px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
/* Overrun is not a failure, so the title is not the loss red (`.result.lose`).
   Gold, as the mission panel's win title is -- what happened is a score. */
.result.score .result-title { color: var(--gold); text-shadow: 0 3px 0 var(--gold-shadow); }

/* The War Bonds tally: same table voice as the stats, with a ruled total. */
.result-bonds {
  margin: 14px auto 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 480px;
  width: 100%;
}
.result-bond {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  /* Hidden until its turn in the count-up (700-qa 027). `visibility` rather
     than `display`, so the block does not reflow line by line as it runs. */
  visibility: hidden;
}
.result-bond-k { color: var(--ink-faint); text-transform: uppercase; }
.result-bond-v { color: var(--ink); text-align: right; }
.result-bond-total {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid var(--ink-mute);
}
.result-bond-total .result-bond-k,
.result-bond-total .result-bond-v { color: var(--ink); }
/* The total says its name with the note rather than the words "war bonds
   earned", which was the widest label on the panel (qa-polish 006). The
   canvas is the same 16x11 note the Armoury puts beside a price. */
.result-bond-total .result-bond-k { display: flex; align-items: center; }
.result-bond-total .result-bond-k canvas {
  display: block;
  image-rendering: pixelated;
}

/* --- the tally counting up (700-qa 027) ----------------------------------
 *
 * The owner: "we'll show their coins, and then it'll visually top up, like
 * +1, +1, +1 ... the same type of animation we have when you pick up a
 * grenade from the game."
 *
 * A row is hidden until its turn and then simply *is* there -- no slide, no
 * ease. What moves is the `+N`, and it moves the way the battlefield popup
 * moves, because the player has seen that rise a hundred times and this is
 * the same event reported late. `visibility` rather than `display`, so the
 * block does not reflow line by line as the tally runs. */
.result-bond.in { visibility: visible; }
/* The sum, and the purse under it, wait for the lines to finish arriving --
   showing the total while they are still coming gives away the ending. */
.result-bonds .result-bond-total,
.result-purse { visibility: hidden; }
.result-bonds.tallied .result-bond-total,
.result-bonds.tallied .result-purse { visibility: visible; }
/*
 * The rise, on a copy of the value drawn over itself.
 *
 * A pseudo-element rather than a second node, so the panel's markup does not
 * grow a throwaway span per line -- and `attr()` means the value is said once
 * in the DOM and echoed here.
 */
.result-bond.pop .result-bond-v { position: relative; }
.result-bond.pop .result-bond-v::after {
  content: attr(data-pop);
  position: absolute;
  right: 0;
  top: 0;
  color: var(--gold);
  pointer-events: none;
  animation: bond-pop 0.9s linear forwards;
}
/*
 * Up and out. The one place alpha is spent in this game is a fade, and this
 * is that -- 15 pixels of rise, which is `CONFIG.fx.popupRise`, so the DOM
 * copy travels exactly as far as the canvas original.
 */
@keyframes bond-pop {
  0% { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(-15px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .result-bond.pop .result-bond-v::after { animation: none; opacity: 0; }
}

/* The purse: the note, and the balance climbing beside it. */
.result-purse {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--ink-mute);
}
.result-purse canvas { display: block; image-rendering: pixelated; }
.result-purse-n {
  color: var(--gold);
  font-size: var(--fs-display);
  line-height: 1;
  font-weight: 400;
}

.result-actions { display: flex; flex-direction: column; gap: 7px; margin-top: 22px; }
/* Back and onward, side by side and equal weight.
   `align-items: stretch` is what makes them the same height whatever each one
   carries (qa-polish 006): measured at 68px against 58px, because one button
   held an arrow and the other did not. Equal width was already handled by the
   `flex: 1 1 0` below; equal height was not, and the owner asked for both. */
.result-choice { display: flex; gap: 7px; align-items: stretch; }
.result-choice .ui-btn { flex: 1 1 0; }
/* Two buttons, an arrow and a keycap into half a panel: the label gives way. */
.result-choice .ui-btn-label {
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
  white-space: nowrap;
}
.ui-btn-arrow { flex: none; font-size: 0.85em; line-height: 1; color: var(--ink-dim); }
/* The gap on .ui-btn already spaces them; these only pull them to the edges. */
.ui-btn-arrow.back { margin-right: -2px; }
.ui-btn-arrow.fwd { margin-left: -2px; }

/* The layouts section at the foot of this file replaces what used to be a
   single `max-width: 720px` query whose whole strategy was `display: none`. */

/* ============================================================ the meta-game */
/*
 * Rank, the roster and Boot Hill.
 *
 * Nothing here invents a new button or a new plate -- it decorates the kit's
 * pieces. A grave and a living soldier are the same `plate` with different
 * classes on it, deliberately, because they are the same man at different times.
 */

/* --- rank marks, shared by the sidebar and the result panel */
.ui-plate.vet .ui-plate-value { color: #f0d878; font-weight: 700; }
/* The one soldier the player named. Marked everywhere he appears. */
.ui-plate.own .ui-plate-label::before {
  content: '\2726';
  margin-right: 5px;
  color: var(--gold);
  font-size: var(--fs-micro);
}

/* --- the par line in the sidebar */
/* --- the loadout row: what the squad is carrying (700-qa 010) -------------
 *
 * Three plates, and the third is a button. It replaced YOUR BEST and the bare
 * grenade counter: the owner asked for the record out and the loadout in, and
 * the three decisions made on the mission screen were invisible the moment
 * the mission started. */
.hud-loadout-row { display: flex; flex-direction: column; gap: 4px; }
.hud-kit {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 4px 8px;
  background: var(--well);
  border: 0;
  font: inherit;
  text-align: left;
  color: var(--ink);
}
/*
 * One cell for every kit portrait, whatever the portrait is (qa-polish 004).
 *
 * 36x24 because the widest plotted kit sprite at scale 1 is 34 (the supply
 * drop and the reinforcements) and the row's line box is 24. Both measured off
 * `render/sprites/kit.ts`, not guessed -- and if a taller sprite is added one
 * day this clips rather than re-flowing the row, which is the failure you see
 * immediately rather than the one you do not.
 */
.hud-kit-icon {
  flex: none;
  width: 36px;
  height: 24px;
  display: grid;
  place-items: center;
}
.hud-kit canvas { display: block; image-rendering: pixelated; flex: none; }
.hud-kit-text { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.hud-kit-label {
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hud-kit-value { color: var(--gold); font-size: var(--fs-micro); }
/* The one you press. Lit while the next click on the map is its mark. */
.hud-kit-callin { cursor: pointer; box-shadow: inset 0 0 0 1px var(--plate-hi); }
.hud-kit-callin:hover, .hud-kit-callin:focus-visible {
  box-shadow: inset 0 0 0 1px var(--ink-dim);
  outline: none;
}
.hud-kit-callin.on { box-shadow: inset 0 0 0 2px var(--gold); }
/* Spent. Still takes the press -- it answers with a toast (700-qa 016). */
.hud-kit-callin.off { color: var(--ink-mute); cursor: default; }
.hud-kit-callin.off .hud-kit-value { color: var(--ink-mute); }
/* The pickup pulse the grenade readout used to do for itself. */
.hud-kit.flash { animation: hud-kit-flash 0.4s step-end; }
@keyframes hud-kit-flash {
  0%, 50% { background: var(--plate-hi); }
  100% { background: var(--well); }
}

/* --- the squad roll on the briefing card */
.briefing-squad {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
  margin-top: 18px;
}
.briefing-man {
  padding: 3px 8px;
  background: var(--plate);
  box-shadow: inset 0 1px 0 var(--plate-hi);
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  font-style: normal;
  letter-spacing: var(--track-tight);
}
.briefing-man.vet { color: var(--gold); }
.briefing-man.fresh { color: #8fb0d4; }
.briefing-man.own { box-shadow: inset 0 0 0 1px var(--gold); }

/* --- records and promotions on the end panel */
.result-records {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
  margin-top: 16px;
}
/*
 * The rating row, drawn by `stars()` in the kit and worn by two screens.
 *
 * `.ui-stars` is the row itself and `.fx-star` is the sprite in it -- the same
 * one the level select wears, which is the point (700-qa 020). The card puts
 * its row inside `.m-ribbons`, which has its own flex, so `.ui-stars` only has
 * to lay out where it stands alone.
 */
.ui-stars { display: flex; gap: 3px; align-items: center; }

/*
 * The end-of-mission rating: the stars, and one line saying what the next costs.
 *
 * Bigger than the card's 34px, because this is a thing to look at rather than
 * a badge on a list, and the panel has the room for it. **51px because the cut
 * is 17 and 51 is exactly 3x** -- any other number resamples a pixel sprite,
 * which is the one thing this game does not do (700-qa 020).
 */
.result-rating {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  margin-top: 16px;
}
.result-rating .ui-stars { gap: 8px; }
.result-rating .fx-star { width: 51px; height: 51px; }
/*
 * The one that just lit, and it is a ring rather than an animation.
 *
 * A star that pulses would be the only moving thing on a still panel, and it
 * would still be moving while the player reads the casualty roll underneath it.
 *
 * The ring was gold, which stopped working the moment the stars themselves
 * became gold (700-qa 020) -- gold on gold is not a cue. It is the chrome's
 * brightest ink now, which reads against the star it surrounds and is not a
 * new colour.
 */
.result-rating .fx-star.just { box-shadow: 0 0 0 2px #fffbe8; }
.result-rating-note {
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}

.result-ribbon {
  padding: 3px 9px;
  background: #4a3d12;
  color: var(--gold);
  font-size: var(--fs-micro);
  font-style: normal;
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}
/*
 * Promotion is good news and has to look like it. These plates inherited the
 * casualty styling -- dark red on a red gradient, struck through in spirit if
 * not in fact -- so the one screen that congratulates you did it in the colour
 * of a death notice.
 */
.result-roll.promotions .ui-plate {
  background: #263619;
  border-top-color: #4d6a2c;
  color: #d8ecb0;
}
/*
 * On a promotion plate the *rank* gives way, not the name.
 *
 * `.ui-plate-label` truncates by default, which is right in the sidebar where
 * the value is three letters. Here the value is the full rank -- "Lance
 * Corporal" -- and with the two rolls sharing a row (310-qa 026) it pushed
 * names out: "G... LANCE CORPORAL". The name is the identity and the reason
 * the panel exists; the rank is already implied by the heading above it.
 */
.result-roll.promotions .ui-plate-label { flex: none; overflow: visible; }
.result-roll.promotions .ui-plate-value {
  /* Merged up from the duplicate declaration that used to sit at line 1226 (600-infra 019). */ color: #b6e06a; font-weight: 700;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.result-roll.promotions .result-roll-head { color: #8fae5e; }
.result-note {
  margin-top: 10px;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  line-height: 1.5;
}

/* --- the music switch
 *
 * Pinned to the corner, not in the flow: it belongs to the screen rather than to
 * the list of missions, and it must not move when the list scrolls -- hence
 * `fixed` inside a container that is itself the scroller. Round, because every
 * other control in this game is a bevelled rectangle and this one is not a
 * choice about the war.
 */
/*
 * The two corner controls, which are now one thing (qa-polish 011).
 *
 * The owner: "clean up the setting button, make it similar looking to the
 * audio button." They were two different button systems standing 8px apart --
 * the speaker a `var(--tap)` circle on a flat plate with three 1px CSS
 * borders, the gear a 44x40 rectangle wearing the plotted `--sk-btn`
 * border-image, with different hover behaviour. Side by side that reads as one
 * of them having gone wrong.
 *
 * The speaker's own note explains its shape and still stands: "Round, because
 * every other control in this game is a bevelled rectangle and this one is not
 * a choice about the war." Neither is Settings. So the gear joins it rather
 * than the other way round.
 *
 * **The box is shared; the glyph is not.** The speaker draws an SVG cone, the
 * gear keeps its baked `--sk-ic-gear` sprite. Matching the box was the ask,
 * and it costs the front screen no second vector.
 */
.corner-tool {
  position: fixed;
  top: calc(var(--safe-t) + 14px);
  z-index: 11;
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  background: var(--plate);
  border: 1px solid var(--plate-hi);
  border-bottom-color: var(--plate-lo);
  border-right-color: var(--plate-lo);
  border-radius: 50%;
  color: var(--ink-faint);
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.corner-tool:hover, .corner-tool:focus-visible { background: var(--plate-hi); outline: none; }
.corner-tool:active { transform: translateY(1px); }
#music-toggle { left: calc(var(--safe-l) + 14px); }
/* One tap square over, with the same 8px gap it has always had. */
.front-gear { left: calc(var(--safe-l) + 14px + var(--tap) + 8px); }
/*
 * The gear's glyph, on the shared round box. It is no longer a `.hud-tool`, so
 * it brings its own `::after` -- `.hud-tool`'s is what used to draw it, along
 * with the 44x40 plate this issue removes.
 */
.front-gear::after {
  content: '';
  width: 22px;
  height: 22px;
  background-image: var(--sk-ic-gear);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  image-rendering: pixelated;
}
.front-gear:hover::after, .front-gear:focus-visible::after { background-image: var(--sk-ic-gear-hot); }
.front-gear:active::after { background-image: var(--sk-ic-gear-down); }
/* No `.hud-tool.t-gear` rules: the gear was the only thing wearing that class
   and it is a `.corner-tool` now (qa-polish 011). The in-mission chrome has no
   gear -- Settings is reached from the pause sheet there. */
#music-toggle svg { width: 22px; height: 22px; overflow: visible; }
/* One rule each: the cone is a solid shape, the waves and the strike are lines,
   and both take their colour from the button so the states need only set that. */
#music-toggle .cone { fill: currentColor; }
#music-toggle .wave, #music-toggle .slash {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
}
/* Playing: lit gold, the one warm thing in the corner. */
#music-toggle.on { color: var(--gold); border-top-color: #4a7a34; border-left-color: #4a7a34; }
/* Held back by the browser's autoplay rules -- on, but not yet audible. The
   pulse is the only honest way to say "this is waiting for you", and it stops
   the moment the first click lets the track through. */
#music-toggle.blocked { color: var(--ink-dim); animation: music-wait 1.6s ease-in-out infinite; }
@keyframes music-wait {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #music-toggle.blocked { animation: none; opacity: 0.7; }
}
/* The SVG star and its three rung colours are gone (700-qa 020): the game has
   one star, it is plotted, and it has one colour. */

/* ================================================================ layer 4 */
/* ============================================================ the layouts */
/*
 * Three arrangements of the same markup, chosen by `layout.ts` and published
 * as `data-layout` on the root element.
 *
 *   wide     >= 900px and landscape. The sidebar, as the original had it.
 *   compact  landscape, narrower. The HUD leaves the flow and floats in the
 *            corners, because a 188px column out of 640px is a third of the
 *            battlefield and a phone has none to spare.
 *   stacked  portrait. Thumbs cannot reach the top of a tall screen, so the
 *            HUD is a reserved bar at the bottom where they already are.
 *
 * The rule the whole section follows: on a small screen the game gets the
 * pixels and the chrome gets out of the way, except where a thumb has to be
 * able to land on something.
 */

/* --------------------------------------------------------------- wide */

/*
 * No layout ever insets the overlay now. The rule that did -- briefing only,
 * sidebar left readable -- guarded a scene that stopped existing: the briefing
 * is shown on a full blackout (main.ts raises it before showBriefing), so the
 * sidebar it was protecting is under black anyway, and all the inset produced
 * was a bar of that black standing beside the scrim, plus a briefing centred
 * on the play area instead of the screen. Every overlay owns the whole screen.
 */

/* ------------------------------------------------------------ compact */

[data-layout="compact"] {
  --fs-micro: 12px;
  --fs-sm: 13px;
  --fs-body: 14px;
  --fs-lg: 17px;
  --fs-xl: 22px;
  --fs-display: clamp(24px, 6vw, 40px);
  --fs-title: clamp(20px, 5vw, 27px);
  --fs-brand: 30px;   /* was overridden at the foot of the file (600-infra 019) */
  --tap: 48px;
  /* Tracking that reads at 11px is a smear at 14px on a phone held at arm's
     length, so the touch layouts tighten it as the type grows. */
  --track-wide: 0.14em;
  --track: 0.08em;
  --track-tight: 0.04em;
}

[data-layout="compact"] #stage {
  grid-template-columns: minmax(0, 1fr);
}
/*
 * Out of the grid entirely and over the canvas. `pointer-events: none` on the
 * shell with it restored per child is what stops an invisible HUD rectangle
 * eating orders meant for the ground behind it -- the single most common way a
 * floating game HUD goes wrong.
 */
[data-layout="compact"] #hud {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr);
  align-content: start;
  gap: 6px;
  padding: calc(6px + var(--safe-t)) calc(6px + var(--safe-r)) calc(6px + var(--safe-b)) calc(6px + var(--safe-l));
  background: none;
  border: 0;
  box-shadow: none;
  pointer-events: none;
  z-index: 3;
}
[data-layout="compact"] #hud > * { pointer-events: auto; }

[data-layout="compact"] .hud-body {
  display: contents;
}
/* Anything floating over the battlefield needs its own ground to be read
   against; the sidebar had the column for that and these have nothing. */
[data-layout="compact"] .hud-mission,
[data-layout="compact"] .hud-squad,
[data-layout="compact"] .hud-loadout,
[data-layout="compact"] .hud-orders {
  padding: 5px 8px;
  background: rgba(12, 16, 8, 0.82);
  box-shadow: inset 0 0 0 1px var(--plate-lo);
}
[data-layout="compact"] .hud-mission { grid-area: 1 / 1; justify-self: start; }
[data-layout="compact"] .hud-loadout { grid-area: 1 / 2; justify-self: end; }
[data-layout="compact"] .hud-orders {
  grid-area: 2 / 1 / auto / span 2;
  align-self: start;
  justify-self: center;
  max-width: min(90%, 460px);
  text-align: center;
}
[data-layout="compact"] .hud-squad { grid-area: 1 / 1; justify-self: start; margin-top: 34px; }

/* The section headings are a sidebar device. Floating over the world they are
   four more words between the player and the battlefield. */
[data-layout="compact"] .ui-heading,
[data-layout="stacked"] .ui-heading { display: none; }
[data-layout="compact"] .hud-hint,
[data-layout="stacked"] .hud-hint { display: none; }
/*
 * The loadout stays on every layout (700-qa 010).
 *
 * YOUR BEST was hidden here on both, which is what made it a desktop-only
 * readout and part of why it went. The call-in plate is a *control* and the
 * touch player has no other route to it since the wing came off the corner --
 * so hiding this row on a phone would take the airstrike away from exactly
 * the player the wing existed for.
 */

/* ------------------------------------------------------------ stacked */

[data-layout="stacked"] {
  --fs-micro: 12px;
  --fs-sm: 13px;
  --fs-body: 15px;
  --fs-lg: 18px;
  --fs-xl: 24px;
  --fs-display: clamp(26px, 8vw, 44px);
  --fs-title: clamp(22px, 6.5vw, 27px);
  --fs-brand: 20px;   /* was overridden at the foot of the file (600-infra 019) */
  --tap: 48px;
  --track-wide: 0.14em;
  --track: 0.08em;
  --track-tight: 0.04em;
}

[data-layout="stacked"] #stage {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
}
[data-layout="stacked"] #viewport { grid-row: 1; }
[data-layout="stacked"] #hud {
  /* The enlarged sidebar ladder (200-qa 024) is a desktop measure; in the
     phone bar it wrapped the goal one word per line. Back to the bar's own
     sizes here. */
  --fs-micro: 10px;
  --fs-sm: 10px;
  --fs-body: 10px;
  --fs-lg: 20px;
  --fs-xl: 20px;
  grid-row: 2;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  min-height: var(--bar-h);
  padding: 8px calc(10px + var(--safe-r)) calc(8px + var(--safe-b)) calc(10px + var(--safe-l));
  border-right: 0;
  border-top: 2px solid var(--edge);
  box-shadow: inset 0 1px 0 var(--plate-lo);
}
[data-layout="stacked"] .hud-body {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-width: 0;
}
[data-layout="stacked"] .hud-mission { flex: none; max-width: 34%; }
[data-layout="stacked"] .hud-orders { flex: 1; min-width: 0; }
[data-layout="stacked"] .hud-loadout { flex: none; }
/* Side by side in the bottom bar: three plates stacked would be taller than
   the strip. */
[data-layout="stacked"] .hud-loadout-row { flex-direction: row; gap: 6px; }
/* One scrollable row rather than a column: the names stay, because a name
   leaving a list is the whole point of the roster. */
[data-layout="stacked"] .hud-roster {
  flex-direction: row;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
}
[data-layout="stacked"] .hud-roster::-webkit-scrollbar { display: none; }
[data-layout="stacked"] .hud-roster .ui-plate { flex: none; }

/* ------------------------------------------------- touch, at any layout */
/*
 * Keyed off the input the player is actually using rather than the viewport,
 * because these are statements about fingers, not about width.
 */

/* A crosshair cursor on a device with no cursor. */
[data-input="touch"] #screen { cursor: default; }
[data-input="touch"] .ui-plate { min-height: var(--tap); }

/* Hover states are a lie on a touchscreen: they latch on after a tap and stay
   lit until something else is touched, which reads as a stuck selection. */
@media (hover: none) {
  /*
   * These three said `background`, and a `.ui-btn`'s background has never been
   * visible -- `border-image: ... fill` covers it. So this block has never
   * suppressed anything: on a touchscreen the hot *plate* latched on after a
   * tap exactly as described, because the rule undoing it was aimed at a
   * property nothing was reading (330-qa 001).
   *
   * The plate is what hover changes, so the plate is what has to be put back.
   */
  .ui-btn:hover { border-image-source: var(--sk-btn); color: var(--ink); }
  .ui-btn.tone-good:hover { border-image-source: var(--sk-btn-good); }
  .ui-btn.tone-warn:hover { border-image-source: var(--sk-btn-warn); }
  /* The dark hover ink goes back with the plate it was for: on a resting plate
     it is unreadable, which is the same fault the other way round. */
  .ui-btn:hover .ui-btn-hint { color: var(--ink-dim); }
  .ui-btn:hover .ui-btn-arrow { color: var(--ink-dim); }
  .diff:hover { background: var(--plate); color: var(--ink-dim); }
  .diff.on:hover { background: var(--plate-hi); color: var(--gold); }
  .mission:hover { background: linear-gradient(#232b14, #1a2010); transform: none; }
  .mission:hover .m-num { color: #56703a; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ========================================================== the action bar */
/*
 * Anchored inside #viewport, so it lands in the battlefield on a phone and
 * inside the play area beside the sidebar on a desktop.
 *
 * The shell does not receive pointer events; only the buttons do. An invisible
 * full-size rectangle over the canvas swallowing move orders is the single most
 * common way a floating game HUD goes wrong, and it is silent when it happens.
 */
#controls {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 4;
}
#controls[hidden] { display: none; }

.controls-top {
  position: absolute;
  top: calc(8px + var(--safe-t));
  left: calc(8px + var(--safe-l));
  display: flex;
  gap: 8px;
  pointer-events: auto;
}

/* The bottom-right cluster is gone with the Centre plate it held (310-qa 006).
   `.controls-top` is the only thing in the bar now; if a second plate ever
   comes back, so does a cluster, and the left-handed mirror with it. */
/* With a mouse, pause lives in the sidebar tools; the floating plate is the
   touch route only, since a phone has no Esc and no sidebar (200-qa 024). */
#controls[data-mode="pointer"] .controls-top { display: none; }
/* The call-in wing is gone (700-qa 010): the owner asked for it off the
   top-right corner and onto the sidebar, where it is a plate in the LOADOUT
   row that doubles as the button. See `.hud-kit-callin`. */
#controls[data-hand="left"] .controls-top {
  left: auto;
  right: calc(8px + var(--safe-r));
}

.ui-action {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 58px;
  height: 58px;
  padding: 0;
  border: 0;
  /*
   * A plate, not a bubble.
   *
   * These were 58px circles with a translucent fill and a soft drop shadow,
   * floating over the battlefield. A critic shown a frame of the game and asked
   * for the single most un-1993 thing in it picked this without hesitation: "a
   * smooth anti-aliased circle with a soft translucent dark fill ... it reads as
   * a modern mobile-game touch control pasted onto the frame." Three separate
   * breaches of the house rules in one widget -- an anti-aliased curve, alpha,
   * and a blurred shadow -- and it is on screen in every single frame.
   *
   * So: hard corners, an opaque fill, a solid offset shadow, and the same 2px
   * inset border the name plates use, which is the idiom the reference actually
   * shows (`elements/buttons-and-troop-chevron-status.jpg`). The tap target is
   * the same size it was; only the drawing changed.
   */
  border-radius: 0;
  background: var(--plate);
  box-shadow: inset 0 0 0 2px var(--plate-hi), 3px 3px 0 var(--edge);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  touch-action: none;
  transition: background 0.1s, transform 0.06s;
}
/* The one the thumb should find without looking. */
.ui-action.primary { width: 76px; height: 76px; }
/* Pressed: the plate moves onto its own shadow, the way a physical key does. */
.ui-action:active {
  transform: translate(2px, 2px);
  box-shadow: inset 0 0 0 2px var(--plate-hi), 1px 1px 0 var(--edge);
}
.ui-action.on { background: #4c6024; box-shadow: inset 0 0 0 2px var(--gold), 3px 3px 0 var(--edge); }
/* Unavailable is a dimmer plate, not a faded one: `opacity` is a blend, and
   there is no blending anywhere else in this game. */
.ui-action.off { background: var(--plate-lo); color: var(--ink-mute); cursor: default; }
.ui-action.tone-good { background: #2f5222; }
.ui-action.tone-warn { background: #4a2718; }
.ui-action:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.ui-action[hidden] { display: none; }

.ui-action-glyph { font-size: 20px; line-height: 1; }
.ui-action.primary .ui-action-glyph { font-size: 30px; }
.ui-action-label {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}
.ui-action-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 20px;
  padding: 2px 4px;
  border-radius: 10px;
  background: var(--gold);
  color: var(--plate-lo);
  font-size: var(--fs-micro);
  font-weight: 700;
  font-style: normal;
  text-align: center;
}
.ui-action-badge[hidden] { display: none; }

/* The cooldown, drained as a wedge over the button. A dead press is explained
   rather than ignored, which is the difference between a control that feels
   broken and one that feels busy. */
.ui-action-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
}

/* The thumbstick nub. Touch only -- on a mouse the aim already follows the
   cursor and a stick drawn under a pointer is clutter. */
.ui-stick {
  position: fixed;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  background: rgba(240, 216, 120, 0.5);
  box-shadow: 0 0 0 2px rgba(240, 216, 120, 0.8);
  pointer-events: none;
  z-index: 7;
}
.ui-stick[hidden] { display: none; }

/* An armed grenade is a mode, so it needs a visible way out. */
.ui-cancel {
  position: absolute;
  left: 50%;
  top: calc(10px + var(--safe-t));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: 9px 14px;
  background: rgba(74, 39, 24, 0.92);
  box-shadow: inset 0 0 0 1px #74412a;
  color: var(--ink);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  pointer-events: auto;
  cursor: pointer;
}
.ui-cancel[hidden] { display: none; }

/* ============================================================== the sheets */

#sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding:
    max(12px, var(--safe-t)) max(12px, var(--safe-r))
    max(12px, var(--safe-b)) max(12px, var(--safe-l));
  background: rgba(6, 9, 3, 0.82);
}
#sheet[hidden] { display: none; }
.sheet-sub {
  margin-top: 6px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-align: center;
  text-transform: uppercase;
}
.sheet-note {
  margin: 14px 0 0;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.sheet-row:last-child { border-bottom: 0; }
.sheet-row-text + .ui-segmented:not(:only-child) { min-width: 0; }
.sheet-row-text b {
  display: block;
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}
.sheet-row-text span {
  display: block;
  margin-top: 3px;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  line-height: 1.4;
}
/* On a phone the label and its control stop fitting side by side long before
   the text itself becomes the problem, so the row turns into two. */
@media (max-width: 460px) {
  /* `flex: 1 1 190px` grows along the *main* axis, and turning the row into a
     column turns that axis vertical -- so the label stretched to fill the row
     and every setting became 263px tall with a canyon of empty green between
     the words and the control. The basis has to be reset with the direction. */
  .sheet-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .sheet-row-text { flex: 0 0 auto; min-width: 0; }
  /* The danger row stacks with the rest of them: it is the same shape and it
     is the widest label on the sheet, so it stops fitting first, not last. */
  .sheet-danger { flex-direction: column; align-items: stretch; gap: 10px; }
  /* Five segments do not fit 390px, and the fifth was simply off the edge.
     Scrolling is already what this control does when it overflows; it just
     needs to be allowed to wrap instead when there is nowhere to scroll to. */
  .ui-segmented { flex-wrap: wrap; }
}
.ui-segmented::-webkit-scrollbar { display: none; }
.ui-seg:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

/* --- level bar: a track, a fill, and a square bevelled handle. Hard edges
   everywhere -- it is a piece of the plate, not a browser range input. */
.ui-slider {
  flex: 1 1 180px;
  min-width: 150px;
  padding: 8px 0;
  cursor: pointer;
  /* Dragging the handle is the gesture; the page must not scroll under it. */
  touch-action: none;
}
.ui-slider-track {
  position: relative;
  height: 10px;
  background: #141a0b;
  border-top: 1px solid var(--plate-lo);
  border-left: 1px solid var(--plate-lo);
  border-bottom: 1px solid var(--plate-hi);
  border-right: 1px solid var(--plate-hi);
}
.ui-slider-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background: #7a6028;
}
.ui-slider-handle {
  position: absolute;
  top: 50%;
  width: 16px;
  height: 24px;
  margin: -12px 0 0 -8px;
  background: #a8843c;
  border-top: 2px solid #d9c27a;
  border-left: 2px solid #d9c27a;
  border-right: 2px solid #4a3a16;
  border-bottom: 2px solid #4a3a16;
}

/*
 * The mission screen (700-qa 005).
 *
 * 1100, not 760. The owner: "this screen is really squashed right now -- make
 * it wider". The number is what three loadout columns need: each holds a
 * portrait at 2x between two arrows and a two-line blurb, which is about 320,
 * and three of those plus the gaps is the width. The squad roll and the
 * controls strip came off to pay for their height.
 */
.briefing {
  text-align: center;
  width: min(1100px, 94vw);
  margin: 0 auto;
  padding: 0 clamp(16px, 5vw, 30px);
}
/* The head: what mission this is, what it is called, what you have to spend.
   Three columns, so the name is centred on the *screen* rather than on
   whatever the ident happens to be wide. */
.briefing-top {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  margin-bottom: 8px;
}
.briefing-ident { text-align: left; }
.briefing-bonds { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
.briefing-bonds canvas { display: block; image-rendering: pixelated; }
.briefing-bonds-text { display: flex; flex-direction: column; align-items: flex-end; }
.briefing-bonds-k {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
.briefing-bonds-n { color: var(--gold); font-size: var(--fs-lg); line-height: 1; }

/* A named rule between the sections, as the reference draws them: the word
   sits on the line rather than above it. */
.briefing-rule {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  /* Tight on purpose: the card has to hold a briefing, three rungs, three
     loadout columns and a button inside 800px, which is the shortest screen
     the harness measures (700-qa 005). */
  margin: 12px 0 8px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}
/* The one rule that gets air above it (qa-polish 007): the owner asked for
   "more space between difficulty and loadout", which is the LOADOUT rule's own
   top margin. Widening every rule would take the room back from the card. */
.briefing-rule.wide { margin-top: 22px; }

/*
 * A short screen gets the same briefing with less air in it (qa-polish 007).
 *
 * The owner: "try to make sure everything fits in vertically, i don't want
 * scrollbars." Measured on Behind the Wire, the card's sections came to
 * 52 + 218 + 20 + 78 + 20 + 211 = 599px of content, which overflowed by 71px
 * at 1366x768 and 219px at 1280x600 -- so it was scrolling on the commonest
 * laptop there is.
 *
 * The same bargain `.result` struck at 310-qa 026 and for the same reason:
 * **the spacing gives way and the content does not.** Nothing is dropped and
 * nothing is truncated -- the margins and the padding tighten, and the
 * DIFFICULTY/LOADOUT separation the owner asked for survives at a smaller
 * number rather than being the first thing spent.
 *
 * Keyed on height, because height is the axis that runs out: a wide 768-tall
 * laptop is exactly the case.
 */
@media (max-height: 820px) {
  .briefing-box { margin-top: 8px; padding: 10px 20px 12px; }
  .briefing-obj { margin-top: 8px; }
  .briefing-line { margin-top: 6px; }
  .briefing-chips { margin-top: 8px; }
  .briefing-rule { margin: 8px 0 6px; }
  .briefing-rule.wide { margin-top: 14px; }
  .briefing-top { margin-bottom: 4px; }
  .bl-art { height: 44px; }
  .bl-col { gap: 2px; }
  .briefing-go { padding: 10px 26px; }
  /*
   * The two that actually cost the height, measured rather than assumed:
   * `.bl-box` came to 84px around a 44px portrait, because the arrows are set
   * at `--fs-display` with 4px of padding and they, not the picture, decide
   * the row. And the blurb ran to three lines at 75px where its own comment
   * says it is sized for two -- widening it is what makes that true again,
   * rather than clipping a line off.
   */
  .bl-box { padding: 2px 6px; }
  .bl-arrow { padding: 0 10px; font-size: var(--fs-xl); }
  .bl-blurb { max-width: 42ch; }
  /* DEPLOY is outside the scrolling body, so its height comes straight off
     what the body has. Its own margin is the last 11px. */
  .briefing-go { margin-top: 8px; padding: 6px 40px; }
}
.briefing-rule::before, .briefing-rule::after {
  content: '';
  height: 2px;
  background: var(--plate-hi);
}
.result {
  /* Merged up from the duplicate declaration that used to sit at line 990 (600-infra 019). */
  width: min(680px, 92vw);
  background: none;
  image-rendering: pixelated;
  box-shadow: none;
  text-align: center;
  padding: clamp(20px, 4vw, 34px);
}

/* --- a panel that keeps its buttons on screen (qa-polish 001) -------------
 *
 * Four things in this game are "a panel with buttons at the bottom" -- the
 * result, the two sheets, a confirmation and the briefing -- and they used to
 * disagree about what happens when the content is taller than the screen.
 * `.result` and `.sheet-card` scrolled *themselves*, so their buttons scrolled
 * away with everything else; `.briefing` and `.confirm-card` had no answer at
 * all, so a tall one simply grew past `#overlay`, which is `position: fixed`
 * and cannot scroll -- its DEPLOY was not below the fold, it was unreachable.
 * The owner: "we need the buttons to be in a static position at the bottom of
 * the frame/panel ... which doesn't feel like a game."
 *
 * So the panel is a column: everything is fixed height except the one child
 * marked `.panel-body`, which takes the slack and scrolls inside it. There is
 * deliberately no `.panel-foot` class -- *not* being the body is what makes a
 * child fixed, so a head, a foot and anything added later are all right by
 * default, and the failure mode of forgetting a class is a section that does
 * not scroll rather than one that walks off the screen.
 *
 * `min-height: 0` is load-bearing. A flex child defaults to `min-height: auto`
 * and refuses to shrink below its content, so without it the body never
 * scrolls and the buttons are pushed off the bottom exactly as before.
 *
 * The privacy dialog already did this locally -- see `.privacy`, which caps
 * and scrolls its own body inside `.confirm-card` -- and this is that made
 * general. It keeps its own cap, which is tighter, and is unaffected.
 */
.panel-scroll {
  display: flex;
  flex-direction: column;
  max-height: 88vh;
  max-height: 88dvh;
}
.panel-scroll > * { flex: 0 0 auto; }
.panel-scroll > .panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/*
 * A short window gets the same panel with less air in it (310-qa 026).
 *
 * Measured at 1366x768: a loss with six dead wanted 670px against 640px of
 * room -- thirty over, and no promotions roll to pair the casualty roll with,
 * which is what fixes the win panel. So on a short screen the *spacing* gives
 * way and the content does not: nothing is dropped, nothing is truncated, the
 * margins and the padding simply tighten. Keyed on height, because that is the
 * axis that runs out; a wide 768px-tall laptop is the case.
 *
 * **It has to sit after the .result sizing rule above, not up beside the other
 * .result rules.** That block is top level and appears late in the file, so an
 * earlier media query loses to it on source order and the padding here was
 * silently ignored -- measured, 8px of a promised 30.
 */
@media (max-height: 820px) {
  .result { padding: 20px 30px 18px; }
  .result-stats,
  .result-records,
  .result-rating,
  .result-rolls .result-roll { margin-top: 12px; }
  .result-rating { gap: 4px; }
  .result-actions { margin-top: 14px; }
}

.result-actions .ui-btn { justify-content: center; }

/*
 * The card metadata used to be hidden below 720px. The shape of a map and the
 * doctrine of its garrison are *why you pick one mission over another*, so
 * hiding them was a layout dodge rather than an editorial decision. Below the
 * width where a right-hand column fits, it becomes a row under the title.
 */
@media (max-width: 560px) {
  .mission { flex-wrap: wrap; }
  .m-meta {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin-top: 4px;
    padding-left: 44px;
  }
}

/* ------------------------------------------------- the touch type floor */
/*
 * Keyed off the input rather than the layout, and separate from the layout
 * blocks above on purpose.
 *
 * A tablet in landscape at 1024px is `wide` -- it has room for the sidebar and
 * should have it -- but it is still a device operated with a thumb, and the
 * audit caught the whole mission-select running at 10 and 11px there. The floor
 * belongs to the finger, not to the width.
 */
[data-input="touch"] {
  --fs-micro: 12px;
  --fs-sm: 13px;
  --track-wide: 0.14em;
  --track: 0.08em;
}

/*
 * `Boots & Bullets` is held on one line by non-breaking spaces -- a game title
 * that wraps reads as a sentence -- so it is 15 unbreakable cells, and monospace
 * type with 0.18em of tracking needs 11.7x its own size in width to set them.
 * The old title was 13 cells at 10.1x, and an earlier 11vw put it at 401px in a
 * 332px hole on a 360px phone: a document-level overflow, the one thing this
 * game cannot scroll its way out of, since the body does not scroll at all.
 * Two more cells buy that margin back down, hence 7vw rather than 7.5 -- 25px
 * and 294px on the same phone, and still inside the hole on a 320px one.
 */
/*
 * The brand, on the grid like everything else.
 *
 * These were clamps -- 22px at one width, 27 at another -- which is exactly
 * what a bitmap face cannot do: every size between the multiples of ten puts
 * a glyph edge between two pixels. Three fixed steps instead, and the smallest
 * is 20 because fifteen unbreakable cells of it come to 233px, which fits a
 * 390px phone beside the music button and a 320px one without it.
 */

/* --- the dev panel
 *
 * Bottom right, deliberately ugly: it is scaffolding, and it should never be
 * mistaken for part of the game. Only ever in the page under `npm run dev` --
 * a production build does not contain the code that creates it.
 */
.debug-panel {
  position: fixed;
  right: 6px;
  bottom: 6px;
  z-index: 90;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 3px;
  /* Wide enough for the longest row -- three call-ins -- since 700-qa 012
     grouped the eleven switches into four lines. */
  max-width: 250px;
  padding: 4px;
  background: rgba(10, 13, 5, 0.85);
  border: 1px solid #4a5a2a;
}
.debug-title {
  width: 100%;
  color: #8fae5e;
  font-size: 10px;
  letter-spacing: 2px;
  text-align: right;
}
.debug-btn {
  padding: 2px 5px;
  background: #2a3418;
  border: 1px solid #4a5a2a;
  color: #c9d4b8;
  font: inherit;
  font-size: 10px;
  cursor: pointer;
}
/* A stated wrap, so the four groups are groups rather than an accident of
   how long the labels happen to be. */
.debug-break { width: 100%; height: 0; }
.debug-btn:hover { background: #3a4a20; }
.debug-btn.on { background: #6d9a4a; color: #10140a; }

/* ===========================================================================
 * The front end -- intro and level select, one screen
 *
 * Nothing in here draws its own bevel. Every plate, button, frame, banner and
 * star is the plotted sprite arriving as a sliced border-image from ui/skin.ts,
 * which is the whole reason those sprites exist: a control has to look the same
 * on canvas and in the DOM, and it only does that if there is one drawing.
 *
 * border-image rather than background, because none of these are one size --
 * a button is as wide as its label and a frame as big as the screen. Slicing
 * stretches only the middles, so a corner lands untouched at any size.
 * ======================================================================== */

#front {
  position: fixed;
  inset: 0;
  z-index: 14;
  overflow-y: auto;
  /* The ground the front end sits on. When the attract world lands it goes
     behind this and only the vignette stays. */
  background:
    radial-gradient(ellipse at 50% 38%, #1e2a12 0%, #0d1207 62%, #070a04 100%);
  opacity: 0;
  /* Published by installSkin from CONFIG.timing (600-infra 041). No
     fallback on purpose: a literal here is the drift coming back. */
  transition: opacity var(--t-frontFade) ease;
}
#front.in { opacity: 1; }
#front.leaving { opacity: 0; }
#front[hidden] { display: none; }

#front-inner {
  min-height: 100%;
  max-width: 1100px;
  margin: 0 auto;
  /* The bottom is deep enough to clear #front-foot, which is pinned there. */
  padding: 40px 24px 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
}
/* The intro is a title screen: logo and buttons sit centred in the viewport
   rather than hung from the top. Only there -- the panes below it fill the
   screen instead, and centring a filled box does nothing. */
#front:not(.on-select) #front-inner {
  justify-content: center;
  padding-top: 24px;
}

/* --- the screen template (501-multiplayer 000) --------------------------
 *
 * Every pane below the logo is the same shape: a plotted frame holding a
 * head (title left, status right), a body that fills, and a foot. **The
 * page never scrolls -- the boxes inside the body do**, which is the
 * owner's rule from screens.md and the reason this exists at all: the
 * lobby and the room are two more panes in here, and a container that
 * grows past the viewport would have them each solving it differently.
 *
 * `min-height: 0` on every box between here and the scrolling list is not
 * decoration. A flex or grid child refuses to shrink below its content, so
 * without it the list keeps its full 2972px, the frame grows to match, and
 * the whole thing hangs out of the bottom of the screen exactly as it did
 * before the change -- which reads as "the edit did nothing".
 */
#front.on-select #front-inner {
  height: 100%;
  min-height: 0;
  gap: 18px;
}
/* The lobby's boxes want the height the art gives them: a shorter breath
   under the logo, and the credit's clearance trimmed to what it needs. */
#front.on-select:has(#lobby:not([hidden])) #front-inner { gap: 10px; padding-top: 24px; padding-bottom: 64px; }
#front.on-select .fx-pane {
  flex: 1;
  min-height: 0;
  width: 100%;
}
#front.on-select #select-frame,
#front.on-select #armoury-frame,
#front.on-select #lobby-frame {
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-height: 100%;
}
/* The select fills what is left; the shorter panes keep their own height
   and only borrow the ceiling above. */
#front.on-select #select-frame { flex: 1; }
/* The lobby fills its pane the same way: sized to content it stopped ~70px
   short of the frame while its own column clipped, which is the worst of
   both -- dead space below, a hidden button above. And an empty note must
   not spend a paragraph's margins. */
#front.on-select #lobby-frame { flex: 1; }
#lobby-note:empty { display: none; }

/* A scrolling box is chrome, and this game plots its chrome -- a default
 * bar is a browser widget sitting in a pixel drawing. Colours only: no
 * sprite, no image, and nothing rounded. `scrollbar-*` covers Firefox,
 * the `::-webkit-` block covers the rest, and a browser honouring neither
 * simply gets its own bar, which is a worse look and not a broken one. */
#select-list, #select-rail, #armoury-list, .lb-feed,
#lobby-actions, .lb-games-list {
  scrollbar-width: thin;
  scrollbar-color: var(--ink-mute) var(--well);
}
#select-list::-webkit-scrollbar,
#select-rail::-webkit-scrollbar,
#armoury-list::-webkit-scrollbar,
#lobby-actions::-webkit-scrollbar,
.lb-games-list::-webkit-scrollbar,
.lb-feed::-webkit-scrollbar { width: 10px; }
#select-list::-webkit-scrollbar-track,
#select-rail::-webkit-scrollbar-track,
#armoury-list::-webkit-scrollbar-track,
.lb-feed::-webkit-scrollbar-track { background: var(--well); }
#select-list::-webkit-scrollbar-thumb,
#select-rail::-webkit-scrollbar-thumb,
#armoury-list::-webkit-scrollbar-thumb,
.lb-feed::-webkit-scrollbar-thumb {
  background: var(--ink-mute);
  border: 2px solid var(--well);
}

/* --- the credit at the foot of the front end
 *
 * Not a flex child of #front-inner: the intro pane centres its contents in the
 * viewport, so a flowed footer would land in the middle of the screen, and an
 * auto margin to push it down would take the centring with it.
 *
 * And `fixed`, not `absolute`. #front is itself the scroll container, so an
 * absolutely positioned child scrolls with the mission list and disappears off
 * the top the moment you reach the bottom of the desert -- which is exactly
 * what the first version did. Fixed pins it to the viewport on both panes.
 * (#front's opacity transition makes a stacking context but not a containing
 * block, so fixed still means fixed in here.)
 *
 * Faint on purpose: it is a credit, not an action. #front-inner carries extra
 * bottom padding so the last mission card cannot end up underneath it, and
 * pointer-events are off everywhere except the link itself, so the strip never
 * eats a click meant for the card behind it.
 */
#front-foot {
  position: fixed;
  z-index: 1;
  left: 0;
  right: 0;
  bottom: calc(var(--safe-b) + 12px);
  margin: 0;
  text-align: center;
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  color: var(--ink-faint);
  pointer-events: none;
}
#front-foot a {
  pointer-events: auto;
  color: inherit;
  text-decoration: none;
}
#front-foot a:hover,
#front-foot a:focus-visible {
  color: var(--ink-dim);
  text-decoration: underline;
  outline: none;
}
#front-foot a:focus-visible { color: var(--gold); }

/* --- the small-screen gate ----------------------------------------------
 * Below this width the game is not offered: the logo, a line of marching
 * orders, and nothing to click. Driven entirely by the width query so
 * resizing a window walks in and out of it with no script involved. */
#front-mobile { display: none; }
@media (max-width: 720px) {
  #intro, #select, #armoury { display: none !important; }
  #front-mobile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 0 12px;
    text-align: center;
  }
  /* Whatever pane state the front was in, the logo stands back up, centred,
     still bobbing -- capped to the screen, because a 2x wordmark is wider
     than the phones this gate exists for. */
  #front #front-logo,
  #front.on-select #front-logo {
    width: min(calc(var(--sk-logo-w) * 2px), 90vw);
    height: auto;
    aspect-ratio: var(--sk-logo-w) / var(--sk-logo-h);
    margin-bottom: 0;
    animation: fx-logo-idle 8s step-end infinite;
  }
  #front #front-inner,
  #front.on-select #front-inner { justify-content: center; }
  .fx-mobile-title {
    margin: 0;
    color: var(--gold);
    font-size: 30px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: var(--track-wide);
    text-transform: uppercase;
    max-width: 420px;
  }
  .fx-mobile-sub {
    margin: 0;
    color: var(--ink);
    font-size: 17px;
    line-height: 1.6;
    letter-spacing: var(--track-tight);
  }
}
/* Restated after the gate: its logo rule re-arms the animation, and this must
   still win for anyone who asked their machine for less motion. */
@media (prefers-reduced-motion: reduce) {
  #front #front-logo,
  #front.on-select #front-logo { animation: none; }
}

/* The idle. A sprite-style bob -- whole pixels, held pose to pose with
   `step-end` the way the game animates everything else, never a smooth
   sub-pixel glide over a pixelated image. Three slow breaths, then one
   small hop with a bounce on landing, and round again. */
@keyframes fx-logo-idle {
  0%   { transform: translateY(0); }
  6%   { transform: translateY(-2px); }
  12%  { transform: translateY(-4px); }
  18%  { transform: translateY(-2px); }
  24%  { transform: translateY(0); }
  30%  { transform: translateY(-2px); }
  36%  { transform: translateY(-4px); }
  42%  { transform: translateY(-2px); }
  48%  { transform: translateY(0); }
  54%  { transform: translateY(-2px); }
  60%  { transform: translateY(-4px); }
  66%  { transform: translateY(-2px); }
  72%  { transform: translateY(0); }
  78%  { transform: translateY(-8px); }
  82%  { transform: translateY(-3px); }
  86%, 100% { transform: translateY(0); }
}
#front-logo {
  /* Merged up from the duplicate declaration that used to sit at line 2218 (600-infra 019). */
  width: calc(var(--sk-logo-w) * 2px);
  height: calc(var(--sk-logo-h) * 2px);
  margin: 0;
  background-image: var(--sk-logo);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  flex: none;
  transition: width 0.34s ease, height 0.34s ease;
 animation: fx-logo-idle 8s step-end infinite;
}
/* Small and risen on the select pane, the logo is furniture: it holds still. */
#front.on-select #front-logo {
  /* Merged up from the duplicate declaration that used to sit at line 2238 (600-infra 019). */
  width: calc(var(--sk-logo-w) * 1px);
  height: calc(var(--sk-logo-h) * 1px);
  position: relative;
  z-index: 1;
  /* It is the way back to the intro now, and the hand should say so. */
  cursor: pointer;
 animation: none;
}
@media (prefers-reduced-motion: reduce) {
  #front-logo { animation: none; }
}
.fx-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.fx-pane { width: 100%; display: flex; flex-direction: column; align-items: center; }
.fx-pane[hidden] { display: none; }

/* --- buttons: the plotted plate, at any width */
.fx-btn {
  min-width: 280px;
  padding: 14px 26px;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  color: #f6efd8;
  font: var(--fs-lg)/1 var(--face);
  letter-spacing: var(--track);
  text-transform: uppercase;
  cursor: pointer;
  image-rendering: pixelated;
}
.fx-btn:hover, .fx-btn:focus-visible {
  border-image-source: var(--sk-btn-hot);
  color: #fffbe8;
  outline: none;
}
.fx-btn:active { border-image-source: var(--sk-btn-down); }
/* The front screen's three colours (740-qa 012/013): gold for PLAY NOW,
   the darker olive for MULTIPLAYER, olive for the rest. */
.fx-btn.primary { border-image-source: var(--sk-btn-gold); }
.fx-btn.primary:hover, .fx-btn.primary:focus-visible { border-image-source: var(--sk-btn-gold-hot); }
.fx-btn.primary:active { border-image-source: var(--sk-btn-gold-down); }
/*
 * The dark plate keeps its border and drops its middle (qa-polish 010).
 *
 * The owner: "the multiplayer button default has some bad artifacts on it,
 * clean it up." Measured off the three cuts rather than guessed at -- all
 * three carry two tones in the middle region, but only this one is a *dither*:
 *
 *              cell     middle   longest flat run across (of 52)
 *   dark      72x24     52x4      8    -- a checker
 *   olive     72x27     52x7     27
 *   gold      72x44     52x24    52    -- flat
 *
 * That 52x4 checker is what `fill` paints across the face, and MULTIPLAYER is
 * the widest button in the game: tiled about nine across and twelve down a
 * 481x82 plate, the whole face is fine noise while the two buttons above it
 * are smooth. Rendered, a column down the dark face changes colour every 2px
 * against 54px on the olive one.
 *
 * The cell is 24 tall against gold's 44, so its middle survived the cut as
 * noise where theirs kept a gradient. Rather than re-cut the sheet, the middle
 * is simply not painted from it: `border-image-slice` without `fill` leaves
 * the border art untouched and lets the background through, and the background
 * is the dominant tone of that very band -- `#554718`, which is already in the
 * cut's own palette. The plate keeps its bevel and loses the noise.
 */
.fx-btn.dark {
  border-image-source: var(--sk-btn-dark);
  border-image-slice: var(--sk-slice-btn);
  background: #554718;
}
.fx-btn.dark:hover, .fx-btn.dark:focus-visible { border-image-source: var(--sk-btn-dark-hot); }
.fx-btn.dark:active { border-image-source: var(--sk-btn-dark-down); }

/*
 * WANT MORE OF THIS?, top right of the intro (751-teasing 002).
 *
 * The brief: *"noticeable but should NOT compete with PLAY NOW"*. Three things
 * keep that true and each is deliberate.
 *
 * **It is in the corner, not in the menu.** A fourth plate in the intro's
 * three-row stack competes with PLAY NOW by standing in the same list as it,
 * whatever size it is.
 *
 * **It wears the olive resting plate**, LEVEL SELECT's and ARMOURY's, at the
 * caption size -- where PLAY NOW is `--fs-lg` gold on a 280px plate. That is
 * several stops below the gold and PLAY NOW is never in doubt.
 *
 * It wore MULTIPLAYER's *dark* olive first, on the reasoning that the quietest
 * plate in the game is the safest place for a secondary CTA. Read blind, that
 * was the wrong sentence: in this game's own colour grammar dark olive means
 * *not now* -- it is the tone the NOT YET and BACK TO THE WAR buttons wear --
 * so the offer was being made on the plate the game uses to decline things.
 * One step up puts it in the "a choice among choices" band, and gives it a
 * face to be seen by rather than only a border, which is what it lacked on a
 * wide screen where the corner is a long way from the eye.
 *
 * **It is on the intro only.** Every other pane keeps a close door in this
 * exact corner (`.lb-head-door`), so `.on-select` -- which `goto()` puts on
 * the root for any pane but the intro -- takes it off the screen. One rule,
 * rather than a mount and unmount per pane.
 */
.demand-cta {
  position: fixed;
  top: calc(var(--safe-t) + 14px);
  right: calc(var(--safe-r) + 14px);
  z-index: 11;                    /* the speaker's layer, the other corner */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  min-height: var(--tap);
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  color: #f6efd8;
  font: var(--fs-sm)/1 var(--face);
  letter-spacing: var(--track);
  text-transform: uppercase;
  cursor: pointer;
  image-rendering: pixelated;
  /*
   * The one label in the game small enough for the browser to try subpixel
   * antialiasing on, which a blind read caught at 8x: clear red and blue
   * fringes on the stems of WANT, MORE and THIS, where the bigger plates come
   * out clean cream. Colour fringing on type is anti-aliasing, and the first
   * of this game's visual laws says there is none of it.
   */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.demand-cta:hover, .demand-cta:focus-visible {
  border-image-source: var(--sk-btn-hot);
  /* The lit plate is bright, so the label stops being light -- the same
     4.5:1 rule `.ui-btn:hover` follows and for the same reason. */
  color: var(--edge);
  outline: none;
}
.demand-cta:active { border-image-source: var(--sk-btn-down); }
/* The mark for "there is more here", the same star MULTIPLAYER wears. */
.demand-cta-star {
  width: 17px; height: 17px;
  background-image: var(--sk-star-on);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
/* Every other pane keeps its close door here. */
#front.on-select .demand-cta { display: none; }
/*
 * Off entirely where the screen is short or narrow. The logo, the three menu
 * rows and the music prompt already have to fit above the fold there, and the
 * corner it sits in is the one the layout borrows first.
 */
@media (max-width: 900px), (max-height: 560px) {
  .demand-cta { display: none; }
}
/* Genuinely disabled: the plate goes dark and the label stays readable on it.
   It used to be the undimmed grey plate carrying a hand-picked #6d7254 at
   1.03:1 -- a button whose label could not be read at all, which is how the
   dispatch shipped a SEND nobody could see. `--ink-dim` on the dimmed plate is
   3.71:1, a hair off what the resting plate gives `--ink` (dispatch 02). */
.fx-btn:disabled {
  border-image-source: var(--sk-btn-off);
  color: var(--ink-dim);
  cursor: default;
}

#intro-actions { display: flex; flex-direction: column; gap: 14px; align-items: center; }
/* Three rows (740-qa 012): the pair sits side by side, the other two alone. */
.intro-row { display: flex; gap: 14px; justify-content: center; align-items: center; }
.intro-row-play .fx-btn.primary {
  min-width: 480px;
  padding: 22px 40px;
  font-size: var(--fs-xl);
}
.intro-row-pair .fx-btn { min-width: 300px; }
.intro-star { display: block; image-rendering: pixelated; }
/* The stars flank the label; the plate is a row, not a stack (740-qa 012). */
.intro-row-net .fx-btn { min-width: 480px; gap: 18px; display: flex; flex-direction: row; align-items: center; justify-content: center; }
@media (max-width: 900px) {
  .intro-row { flex-direction: column; }
  .intro-row-play .fx-btn.primary, .intro-row-net .fx-btn { min-width: 280px; padding: 14px 26px; }
}

/* --- the level select frame */
#select-frame {
  width: 100%;
  position: relative;
  image-rendering: pixelated;
  padding: 6px;
}

/* The mission dialog's loadout rows are gone with the dialog (700-qa 004).
   The briefing's three cyclers replaced them -- see `.bl-col`. */

/* --- the lobby, to the concept art (501-multiplayer 013/017) ---------- */
#lobby-frame {
  width: min(1180px, 100%);
  margin: 0 auto;
  position: relative;
  image-rendering: pixelated;
  padding: 6px 14px 14px;
}
#lobby-head .ar-bonds canvas { display: block; image-rendering: pixelated; }
#lobby-body {
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  gap: 14px;
  align-items: stretch;
  flex: 1;
  min-height: 0;
}
#lobby-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-y: auto;
  padding-right: 4px;
}
/*
 * The column stacks and stops (owner, 4 Sep 2026: "i don't want the buttons
 * or the join with code to be vertically stretched or trying to FIT the
 * space, this looks wrong"). A gap under the last box is fine; a button
 * whose height depends on how many boxes happen to be above it is not.
 * Only the first box is given a little more room, because a soldier
 * standing in it wants it.
 */
#lobby-actions > * { flex: none; }
/* ...but the room is one column of two rows, and its lower half is meant to
   take the rest -- that is what puts the chat line on the panel's floor. */
#lobby-actions > .lb-lower { flex: 1; }
#lobby-actions > .lb-box:first-child { padding-top: 14px; padding-bottom: 14px; }
#lobby-games { display: flex; flex-direction: column; gap: 8px; min-height: 0; }
/* And its tail takes the slack (owner, 4 Sep 2026), so the dark card runs
   to the bottom of the pane instead of leaving a column of nothing. */
.lb-games-list > .lb-end { flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px; }

/* Every control group is its own box, as drawn: the same dark well and thin
   edge whether it is a fact (PLAYING AS), a door (the create cards) or a
   form (JOIN WITH CODE). */
.lb-box {
  background: var(--well);
  border: 1px solid #2c3519;
  padding: 6px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: none;
}
.lb-box-label {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
.lb-box-title { color: var(--ink); font-size: var(--fs-body); letter-spacing: var(--track-tight); }
.lb-box-sub { color: var(--ink-faint); font-size: var(--fs-sm); }
.lb-box-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.lb-box-row .lb-who { flex: 1; }

/* A door: a whole box that presses. Icon on the left, title over a
   sub-line, as the art has it. The private door wears the gold. */
.lb-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  background: var(--well);
  border: 1px solid #2c3519;
  padding: 8px 12px;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--ink);
  flex: none;
}
.lb-card:hover, .lb-card:focus-visible { border-color: var(--ink-mute); outline: none; }
.lb-card.primary { background: #4a4218; border-color: #6e6224; }
.lb-card.primary .lb-box-title { color: var(--gold); }
.lb-card.primary .lb-box-sub { color: #b9a955; }
.lb-card-icon { display: block; image-rendering: pixelated; flex: none; }
.lb-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

/*
 * A create door: `.fx-btn`'s plate, laid out as mark + two lines. The
 * label rides `.fx-btn-label` so it inherits the button's own ink in all
 * four states; only the layout is new.
 */
.lb-door {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  text-align: left;
}
.lb-door .fx-btn-label { font-size: var(--fs-body); letter-spacing: var(--track-tight); }
/* One line, always: wrapped, the private door stood taller than the public
   one and the pair stopped reading as a pair. */
.lb-door .lb-box-sub {
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The compact plated buttons this pane uses beside boxes. */
#lobby-body .fx-btn { min-width: 0; padding: 6px 14px; font-size: var(--fs-body); }
#lobby-body .fx-btn-label { font-size: var(--fs-body); letter-spacing: var(--track-tight); }
/* Practice and the way out share a row: two plated buttons, the column's
   last line, so the doors the art has get the height. */
.lb-foot { display: flex; gap: 6px; flex: none; }
.lb-foot .fx-btn { flex: 1; }

/* PLAYING AS: the avatar, the name, the way to change it. */
.lb-avatar-plate { flex: none; image-rendering: pixelated; }
.lb-who {
  min-width: 0;
  color: var(--ink);
  font-size: var(--fs-body);
  letter-spacing: var(--track);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.lb-pencil { background: none; border: 0; padding: 2px; cursor: pointer; line-height: 0; flex: none; }
.lb-pencil canvas { display: block; image-rendering: pixelated; }
.lb-pencil:hover canvas { filter: brightness(1.3); }
.lb-badge {
  color: var(--ink);
  background: var(--plate-lo);
  border: 1px solid var(--ink-mute);
  font-size: var(--fs-sm);
  padding: 0 6px;
  margin-left: 6px;
}
.lb-join { display: flex; align-items: center; gap: 8px; }
.lb-join .lb-input { flex: 1; min-width: 0; }
.lb-join .fx-btn { flex: none; }
/*
 * One field for the whole game (owner, 4 Sep 2026: "this should be a global
 * thing"). It is built on `.fx-btn`'s box -- the same 10px border-image
 * geometry, the same padding, the same line -- wearing the *pressed* plate,
 * which is the sheet's own sunken surface. (The disabled one was the first
 * try and was both too dark and a lie: a field you can type in should not
 * wear the plate that means "not now".) That is what makes a field and
 * the button beside it exactly the same height without a number written
 * down anywhere: change the button's padding and the field follows.
 */
.fx-input, .lb-input, .confirm-input {
  min-width: 0;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn-down) var(--sk-slice-btn) fill / 10px round;
  color: var(--ink);
  font: var(--fs-lg)/1 var(--face);
  letter-spacing: var(--track);
  padding: 14px 20px;
  text-transform: uppercase;
  image-rendering: pixelated;
}
.fx-input::placeholder, .lb-input::placeholder, .confirm-input::placeholder { color: var(--ink-mute); }
/*
 * Focus does not change the plate.
 *
 * It used to swap to the lit button skin, which put cream text and a muted
 * placeholder on a bright gold ground -- the owner's words were that the
 * placeholder was impossible to see, and it was the *focused* state, which
 * is the one state a field is guaranteed to be in while it matters. The
 * ground stays sunken and the ink brightens instead; the caret does the
 * rest, which is what a caret is for.
 */
.fx-input:focus, .lb-input:focus, .confirm-input:focus {
  outline: none;
  color: #fffbe8;
}
.fx-input:focus::placeholder, .lb-input:focus::placeholder,
.confirm-input:focus::placeholder { color: var(--ink-faint); }
/* The pane's fields shrink with its buttons, one rule feeding both. */
#lobby-body .lb-input, .confirm-input { padding: 8px 14px; font-size: var(--fs-body); }
.lb-err { color: #ff8a6a; font-size: var(--fs-sm); letter-spacing: var(--track-tight); }

/* --- the public games list */
.lb-games-head {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-dim);
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
  flex: none;
}
/* Refresh is the small circular-arrows mark beside the heading, as drawn --
   not a button. It turns in quarter steps while the list reloads. */
.lb-refresh {
  background: none;
  border: 0;
  padding: 2px;
  cursor: pointer;
  display: inline-flex;
  image-rendering: pixelated;
}
.lb-refresh canvas { display: block; }
.lb-refresh:hover canvas { filter: brightness(1.25); }
.lb-refresh.busy canvas { animation: lb-turn 0.8s steps(4) infinite; }
@keyframes lb-turn { to { transform: rotate(360deg); } }
.lb-games-list {
  /* Merged up from the duplicate declaration that used to sit at line 2457 (600-infra 019). */ flex: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
  position: relative;
}
/* One game: the map on the left, three lines in the middle, the count over
   JOIN on the right. The map box is a placeholder until 018. */
.lb-game {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 8px 10px;
  background: var(--well);
  border: 1px solid #2c3519;
}
.lb-game-map {
  width: 116px;
  height: 78px;
  background: var(--plate-lo);
  border: 1px solid var(--ink-mute);
  flex: none;
}
.lb-game-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.lb-game-name {
  color: var(--ink);
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lb-game-meta {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lb-game-tail { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex: none; }
.lb-game-count { color: var(--ink); font-size: var(--fs-sm); letter-spacing: var(--track); }
.lb-game-tail .fx-btn { flex: none; }
/* IN PLAY: a status where a JOIN would be (qa-multiplayer-sep-07 003). It was
   a disabled plate, and a disabled plate is deliberately hard to read --
   `--ink-dim` at 3.71:1, inside a row `.lb-game.full` has already faded, which
   is why the owner could not see it. `--ink-faint` is 5.00:1 on this plate and
   is the quietest token that is still meant to be read. The padding matches
   `.fx-btn`'s so the row does not jump between a joinable game and a spent
   one. */
.lb-game-status {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track);
  padding: 8px 14px;
  flex: none;
}
.lb-end {
  color: var(--ink-faint);
  font-size: var(--fs-body);
  letter-spacing: var(--track);
  text-align: center;
  padding: 18px 0;
  background: var(--well);
  border: 1px solid #2c3519;
  flex: none;
}
.lb-end.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.lb-games-list.busy > .lb-game { opacity: 0.35; }
/* A game under way: present, and plainly not for you. Dimmed rather than
   hidden -- the whole point is that it is evidence of a game being played. */
.lb-game.full { border-color: #232a12; }
.lb-game.full .lb-game-name, .lb-game.full .lb-game-count { color: var(--ink-faint); }
.lb-game.full .lb-game-meta { color: var(--ink-mute); }
.lb-game.full .lb-game-map { opacity: 0.5; }

/* --- the room (501-multiplayer 015), to the concept art -------------- */
#lobby-body.in-room { grid-template-columns: minmax(0, 1fr); }
#lobby-head {
  /* Merged up from the duplicate declaration that used to sit at line 2417 (600-infra 019). */
  margin: 0 0 12px;
  padding: 4px 2px 8px;
  border-bottom: 2px solid var(--ink-mute);
  flex: none;
 display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
}
#lobby-head .ar-title { flex: none; }
#lobby-head .ar-bonds {
  /* Merged up from the duplicate declaration that used to sit at line 2423 (600-infra 019). */ display: flex; align-items: center; gap: 8px;
 margin-left: auto; flex: none;
}
.lb-sub {
  flex-basis: 100%;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}

/* Two slots. The empty one holds the invite -- the thing you do while you
   wait belongs in the space the person will fill. */
.lb-slots { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; flex: none; }
/*
 * A chair sits *on* the panel, not in a hole in it (owner, 4 Sep 2026:
 * "with the darkness it feels recessed ... lighter green like the original
 * image"). The panel's own ground measures #3f4618, and the card was
 * `--well` at #10140a -- a drop of that size reads as a cut-out however
 * thin the border is. #4a5424 is one step up from the panel and the edge is
 * lighter still, which is what makes a raised face rather than a sunken one.
 */
.lb-slot {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  /* Rows pack to the top: stretched, the trooper and the name drifted to
     the middle of a tall card and the two chairs stopped lining up.
     `align-content: start` is what does that, and it is also what left the
     READY button sitting directly under the trooper instead of on the floor
     of the card (owner, 5 Sep 2026) -- so the last row takes the slack
     instead, and everything above it still packs to the top. */
  align-content: start;
  align-items: start;
  /* Three rows in either chair -- the label, the body, the line it ends on.
     The middle one takes the slack, which is what puts the last on the floor
     without an empty fourth row's gap holding it 10px clear of it. */
  grid-template-rows: auto 1fr auto;
  gap: 10px;
  padding: 10px 12px;
  background: #4a5424;
  border: 1px solid var(--ink-mute);
  /* The two chairs are the same height whatever is in them, which is what
     lets the button and the other chair's status line agree on a floor. */
  min-height: 100%;
}
.lb-slot-label { grid-column: 1 / -1; }
.lb-slot.ready { background: #4f7a2c; border-color: #82ad5c; }
.lb-slot.away { opacity: 0.6; }
/* The soldier on the card: a baked sprite on a canvas at whole-number
   scale, and nothing else. He had a bordered well of his own, which put a
   second box inside a box and read as a picture frame (owner, 4 Sep 2026). */
.lb-avatar {
  display: block;
  image-rendering: pixelated;
  flex: none;
  align-self: start;
}
.lb-slot-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.lb-slot-name {
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--ink);
  /* Whose chair this is, is the card's subject -- so it is the card's
     largest thing (owner, 4 Sep 2026). */
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
}
.lb-host {
  color: var(--gold);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
.lb-slot-state { display: flex; gap: 10px; color: var(--ink-faint); font-size: var(--fs-sm); }
.lb-wins { color: var(--gold); }
.lb-slot.ready .lb-slot-state { color: #c7e3a2; }
/* The line both chairs end on, whether it is a control or a fact. `auto`
   top margin puts it on the floor of the card rather than under whatever
   happens to be above it -- and it is on `.lb-actline`, which both the
   button and the flat status wear, so the two chairs still end level. */
.lb-actline { width: 100%; grid-column: 1 / -1; align-self: end; }
/*
 * The other chair's state, built to the button's measurements: the same
 * 10px border box, the same padding and line, so the two cards end at the
 * same height. Flat, because it is a fact and not a control.
 */
.lb-status {
  box-sizing: border-box;
  border: 10px solid transparent;
  padding: 6px 14px;
  font-size: var(--fs-body);
  line-height: 1;
  letter-spacing: var(--track-tight);
  text-align: center;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.lb-slot.ready .lb-status { color: #dff0c8; }
/* The invite spans the slot under the silhouette: the link on its own
   line, broken where it must rather than cut to an ellipsis, then the
   copy button and the code. */
.lb-invite {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.lb-invite .lb-url {
  flex: 1;
  min-width: 0;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  /* One line. Wrapped it cost the panel a row it did not have, and the
     code has its own gold line below anyway -- COPY LINK is the way this
     is actually used, not reading it off the screen. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.3;
  background: var(--plate-lo);
  border: 1px solid #2c3519;
  padding: 5px 8px;
}
/* The code is the short way in, so it is the one gold thing in the box. */
.lb-orcode { display: flex; align-items: baseline; gap: 8px; }
.lb-code { color: var(--gold); font-size: var(--fs-body); letter-spacing: var(--track); }
.lb-invite-row { display: flex; align-items: center; gap: 10px; }
.lb-copy { flex: none; }

/* Settings beside the feed. */
.lb-lower {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 10px;
  flex: 1;
  min-height: 0;
  /* Clear of the chairs above (owner, 5 Sep 2026). It sits here rather than
     as a gap on `#lobby-actions`, because that column is also the lobby's
     and a gap there would open one between every box on the other pane. */
  margin-top: 14px;
}
/* The settings box takes the height its four rows need; the feed is the
   one that gives, because the feed scrolls and a clipped VISIBILITY row is
   just a missing setting. */
.lb-settings {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-self: start;
}
/* The feed takes every row the settings beside it do not, and the line you
   type sits on the floor of the panel under it (owner, 4 Sep 2026). */
.lb-feedbox {
  /* Merged up from the duplicate declaration that used to sit at line 2822 (600-infra 019). */
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
 height: 100%;
}
/* The way out sits under the settings, as the art's LEAVE ROOM does -- a
   modest button in the column, not a bar across the bottom eating a row. */
.lb-leave { margin-top: 6px; width: 100%; }
/* A settings row is a control: it opens its picker, so it looks pressable. */
.lb-set {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  /* Three pixels of padding made a row the height of its own text: hard to
     read and, since the row *is* the button, hard to press (owner, 5 Sep
     2026). The min-height is what keeps a row with a short value the same
     target as one with a long one. */
  padding: 8px 10px;
  /* 40, because `bakeLock` is two pixels taller than the three marks and a
     row that sizes to its icon gives one of the four a different target.
     Fixed here rather than by scaling the canvas: these are plotted sprites
     and a fractional scale is exactly what /pixel-check exists to stop. */
  min-height: 40px;
  background: var(--well);
  border: 1px solid var(--edge);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.lb-set:hover, .lb-set:focus-visible { border-color: var(--ink-mute); outline: none; }
.lb-set:focus-visible .lb-set-v { color: var(--gold); }
.lb-set-k { color: var(--ink-faint); font-size: var(--fs-sm); }
.lb-set canvas { display: block; image-rendering: pixelated; }
.lb-set-v { color: var(--ink); font-size: var(--fs-sm); text-align: right; }
/*
 * The feed fills its box on the first frame.
 *
 * `flex: 1` alone left the basis at `auto`, so an almost-empty feed was
 * sized by its three lines and the box grew under it as messages arrived --
 * the owner watched the chat window get taller as he typed. A zero basis
 * makes the height come from the box and never from the content.
 */
.lb-feedbox .lb-feed { flex: 1 1 0; height: auto; min-height: 0; }
/* Two kinds of line, told apart at a glance: the comms character is dim
   and italic-free prose, a player is <NICK> in his own colour. */
.lb-line { display: flex; gap: 6px; align-items: baseline; }
.lb-nick { flex: none; letter-spacing: var(--track-tight); }
.lb-nick.green { color: #9bd06a; }
.lb-nick.blue { color: #6aa8d0; }
.lb-said { min-width: 0; word-break: break-word; }
.lb-line.said .lb-said { color: var(--ink-faint); }
.lb-line.chat .lb-said { color: var(--ink); }
.lb-say { display: flex; gap: 8px; align-items: center; flex: none; }
.lb-say .lb-input { flex: 1; min-width: 0; text-transform: none; font-size: var(--fs-sm); }

/* The room: the seat plates, and the green that means ready. */
.lb-seats { display: flex; flex-direction: column; gap: 6px; }
.lb-seat {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  padding: 10px 12px;
  background: var(--well);
  border: 2px solid var(--edge);
  color: var(--ink);
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
}
.lb-seat.ready { background: #33591f; border-color: #1d3510; color: #e4f0ca; }
.lb-seat.away { color: var(--ink-mute); }
.lb-seat .lb-state { font-size: var(--fs-sm); color: var(--ink-faint); flex: none; }
.lb-seat.ready .lb-state { color: #c7e3a2; }
.lb-share {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-sm);
  color: var(--ink-faint);
}
.lb-url { color: var(--ink-dim); word-break: break-all; min-width: 0; flex: 1; }
.lb-copy .fx-btn-label { font-size: var(--fs-sm); }
.lb-feed {
  height: 140px;
  overflow-y: auto;
  background: var(--well);
  border: 2px solid var(--edge);
  padding: 6px 10px;
}
.lb-feed div { font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.5; }
@media (max-width: 720px) {
  #lobby { display: none !important; }
}

/* --- the armoury: the same plotted frame, a stock list inside it -------- */
#armoury-frame {
  /* Four cards in a row, as the owner drew them, and that is what sets this
     width -- 680px gave each card 160 and every blurb wrapped to four lines,
     which is the whole of "all the text is too tight" (700-qa 002). */
  /* ...and never wider than the pane it sits in (740-qa 027). It was
     min(1200px, 96vw) inside #front-inner's 1100px, so on a 1280 desktop it
     hung 148px out to the right and its centre sat 74px off the screen's --
     "the dialog is not center, it's a bit to the right". Measured, fixed,
     and measured again at 1280, 1366 and 1440. */
  width: min(1200px, 96vw, 100%);
  margin: 0 auto;
  image-rendering: pixelated;
  padding: 6px 14px 14px;
  display: flex;
  flex-direction: column;
  /* The stock list scrolls inside the plate; ten items outgrow a laptop,
     and a frame that spills lets the page footer bleed through it. */
  max-height: calc(100vh - 200px);
}
#armoury-list { overflow-y: auto; min-height: 0; }
#armoury-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin: 0 2px 12px;
  /* The corner cross is positioned against this, as it is on the other two
     panes -- see `.hud-tool.lb-head-door`. */
  position: relative;
}
/*
 * `.ar-title` and `.ar-bonds` are the *shared* head, borrowed by the lobby and
 * the level select -- so the Armoury's own, bigger treatment is scoped to it
 * rather than applied to the class. Widening the class instead put a 40px
 * title on three screens and a plate around the lobby's player count.
 */
/*
 * The four dialogs' titles are set in the display face (700-qa 017 sitting 2):
 * `.ar-title` here, `.sheet-title`, `.confirm-title` and `.briefing-title`.
 * Each says so in its own rule rather than in one grouped selector, because a
 * group counts as a second declaration of every selector in it and
 * `rules.test.mjs` refuses that -- the later one would win silently.
 *
 * Two things go with the face wherever it is used.
 *
 * **Line-height 1.2**, not the body's 1: a title glyph is eleven rows where
 * the chrome's is seven, so at 1 the caps are taller than the box holding them
 * and the line above clips them.
 *
 * **A shadow of one *glyph* pixel down and right** -- `style.md`'s `{1,1}` for
 * lettering -- which is three CSS pixels at 30px and four at 40, since one
 * glyph pixel is one CSS pixel at 10px. `.briefing-title` used to say
 * `0 3px`: a third offset, which existed because it was measured in CSS pixels
 * against a face that had no grid to measure against.
 */
.ar-title {
  color: var(--ink);
  font-family: var(--face-title);
  font-size: var(--fs-lg);
  line-height: 1.2;
  /*
   * No tracking, where the chrome face wanted `--track-wide` (700-qa 017).
   *
   * Tracking is a fix for a face whose letters are too close together, and it
   * was inherited from the text face along with the rule. The display face
   * carries a blank column of side bearing in every glyph, so 0.2em on top of
   * it is a fifth of an advance of air between letters -- a blind critique
   * shown this beside the reference called ours "wide, airy letterspacing"
   * against the reference's "letters nearly touching", and that is the whole
   * of the difference. The same reason applies to every title below.
   */
  letter-spacing: 0;
  text-shadow: 3px 3px 0 var(--ink-shadow);
}
.ar-bonds { color: var(--ink-dim); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }
.ar-titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
#armoury-head .ar-title { font-size: var(--fs-display); text-shadow: 4px 4px 0 var(--ink-shadow); }
.ar-sub { color: var(--ink-faint); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }

/* --- the balance ---------------------------------------------------------
 *
 * Its own plate, and the numeral at display size: it was 20px opposite a 30px
 * title, which is the owner's "it's hard to see". The note sprite sits beside
 * it so the number is labelled by a picture rather than by a second word. */
#armoury-head .ar-bonds {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 16px 6px 10px;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
}
#armoury-head .ar-bonds canvas { display: block; image-rendering: pixelated; }
.ar-bonds-text { display: flex; flex-direction: column; align-items: flex-end; gap: 0; }
.ar-bonds-k { color: var(--ink-dim); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }
/* Gold, because a balance is the one number every other decision on this
   screen is made against -- and because the owner drew it gold. */
.ar-bonds-n { color: var(--gold); font-size: var(--fs-display); line-height: 1; }

/* --- the cards -----------------------------------------------------------
 *
 * A row of items, each a plate. Four across at the frame's full width, and
 * they wrap rather than shrink: a card narrower than this cannot hold a gun
 * portrait and a two-line blurb, which is the pair that makes it a card
 * rather than a row. */
.ar-grid {
  display: grid;
  /* Five across now that the issued rifle heads the weapons (740-qa 027):
     four made it two rows and a scrollbar the owner asked not to have. */
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
  padding: 4px 2px 2px;
}
/* The issued rifle, named above the guns rather than sold beside them
   (740-qa 027): what you hold, and that the cards are upgrades. */
.ar-issued {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 2px 6px 6px;
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  color: var(--ink-dim);
}
.ar-issued-k { color: var(--gold); }
.ar-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 10px 8px;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
  text-align: center;
}
/* A fixed box, so a tall sniper rifle and a small grenade leave their names
   on the same line across the row. Sized to the tallest portrait at 2x
   rather than generously: the card has to hold a three-line blurb and a
   button as well, and the button is the one thing that must never need
   scrolling to reach. */
/* Tall enough for the tallest portrait at 2x (740-qa 027): a fixed 58px
   cut the smoke and the flashbang off into their own names. */
.ar-art { min-height: 58px; display: grid; place-items: center; }
.ar-art canvas { display: block; image-rendering: pixelated; }
.ar-card .ar-name {
  color: var(--ink);
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
}
.ar-card .ar-blurb {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.3;
}
/* Stocked, not arrived: visible the way a locked mission is visible. */
.ar-card.pending .ar-name,
.ar-card.pending .ar-blurb { color: var(--ink-mute); }
.ar-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding-top: 6px;
  border-top: 1px solid var(--plate-hi);
}
.ar-price { display: flex; align-items: center; gap: 8px; }
.ar-price canvas { display: block; image-rendering: pixelated; }
.ar-price-n { color: var(--gold); font-size: var(--fs-lg); }
/* How many are in the store. Above the price, because it is the number the
   player is deciding against (700-qa 002). */
.ar-have { color: var(--ink); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }
.ar-soon { color: var(--ink-mute); font-size: var(--fs-sm); letter-spacing: var(--track-tight); }
/* Inside the card, whatever the plate's minimum says (740-qa 027): `.fx-btn`
   wants 280px and a card is narrower, so the button stood 3px proud on each
   side. */
.ar-buy { width: 100%; min-width: 0; }
.ar-buy .fx-btn-label { font-size: var(--fs-lg); }
/*
 * OWNED is the same button in an outline skin, so nothing moves when you buy.
 * This is not disabled -- it is *done* -- so it wears the quiet plate rather
 * than the off one, and keeps a readable ink rather than the muted one. The
 * two were the same picture until dispatch 02, which is the only reason this
 * comment used to have to argue the point.
 */
.ar-buy.owned {
  border-image-source: var(--sk-btn-quiet);
  cursor: default;
}
.ar-buy.owned .fx-btn-label { color: var(--ink-dim); }
/* Unaffordable. Not `disabled`: it still takes the click, and answers with a
   toast saying why (700-qa 002) -- so the quiet plate, not the off one. */
.ar-buy.off { border-image-source: var(--sk-btn-quiet); cursor: default; }
.ar-buy.off .fx-btn-label { color: var(--ink-mute); }

/* --- the tab strip -------------------------------------------------------
 *
 * Gold on the one that is showing and an outline on the rest, which is the
 * reference exactly and also style.md's rule: gold means *this one*. */
.ui-tabs { display: flex; gap: 10px; margin: 0 2px 12px; }
.ui-tab {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font: inherit;
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  cursor: pointer;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn-quiet) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  color: var(--ink-dim);
  padding: 8px 14px;
}
.ui-tab canvas { display: block; image-rendering: pixelated; }
.ui-tab:hover, .ui-tab:focus-visible { color: var(--ink); outline: none; }
.ui-tab.on {
  border-image-source: var(--sk-btn-hot);
  color: #fffbe8;
}
.ui-tabs-body { min-height: 0; }

/*
 * The sheet's cross hangs off `.sheet-wrap`, not the card (740-qa 032).
 *
 * `.sheet-card` is its own scroll container -- it has to be, or a settings
 * list on a 390x844 phone loses its last two rows -- and an absolutely
 * positioned child that hangs outside a scroll container is clipped by it.
 * 700-qa 017 therefore tucked the cross *inside* the corner, and the owner
 * saw two placements across the dialogs. The wrap is the card's box and
 * does not scroll, so the cross can overhang it like every other frame's.
 * The wrap has no border of its own, so the offset is the overhang alone:
 * `.frame-door` pays the 18px frame and this subtracts it back.
 */
.sheet-wrap { position: relative; width: min(620px, 100%); }
.sheet-wrap > .sheet-card { width: 100%; }
.sheet-wrap > .hud-tool.frame-door {
  top: -10px;
  right: -10px;
}

/* --- the frame, once ------------------------------------------------------
 *
 * Eight surfaces each wrote these two lines out (700-qa 017): the sidebar in
 * the wide layout, the briefing box, the result panel, the level select, the
 * lobby, the Armoury, the settings sheet and a confirm. They are the same
 * panel wearing different widths, and eight copies is eight places for the
 * next change to the frame to be missed.
 *
 * `--sk-slice-frame` is a bare number (18, not 18px), so it cannot double as
 * the border-width -- the shorthand goes invalid, the border collapses to
 * nothing, and `border-image` has no box to paint into. `fill / 18px` is what
 * actually draws it, and that is why the width is stated twice.
 *
 * Each surface keeps its own width, padding and position; this says only what
 * they all agree on.
 */
[data-layout="wide"] #hud,
.briefing-box,
.result,
#select-frame,
#lobby-frame,
#armoury-frame,
.sheet-card,
.confirm-card {
  border: 18px solid transparent;
  border-image: var(--sk-frame) var(--sk-slice-frame) fill / 18px round;
  image-rendering: pixelated;
}

/* --- the toast -----------------------------------------------------------
 *
 * Fixed to the viewport, because both lists it has to speak over scroll, and
 * a message clipped by the overflow it is explaining would be worse than no
 * message. Chrome tones only: never red, which is damage, and never gold,
 * which means *this one*. */
.toast {
  position: fixed;
  z-index: 200;
  max-width: 320px;
  padding: 8px 14px;
  border: 10px solid transparent;
  /* Moss, not brass (740-qa 028): the plate it lands on is brass, and a label
     in the same green as its host is a label nobody sees. */
  border-image: var(--sk-plate-toast) var(--sk-slice-plate) fill / 10px round;
  image-rendering: pixelated;
  color: var(--ink);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-align: center;
  /* It is a label, not a target: a click meant for what is under it must not
     land here instead. */
  pointer-events: none;
}

/* The screen's name on the left, what it is worth on the right -- the same
 * two-part head the armoury already uses ("THE ARMOURY" / "WAR BONDS · 12")
 * and the shape the concept art asks for ("MULTIPLAYER LOBBY" / "3 PLAYERS
 * ONLINE"). It held a BACK button and a centred banner until the logo
 * became the way back; both were dead code by the time 501-000 arrived. */
#select-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  margin: 0 2px 12px;
  flex: none;
}
/* The door is out of flow, so the head has to leave the corner alone by
   hand -- without this the star count runs underneath it. */
#select-head, #lobby-head { padding-right: 44px; }

#select-body {
  display: grid;
  grid-template-columns: 340px minmax(0, 1fr);
  gap: 16px;
  align-items: stretch;
  /* Fills the frame, and may shrink -- see the template note above. */
  flex: 1;
  min-height: 0;
}

/* --- the rail */
#select-rail {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-y: auto;
}
/* One column since 700-qa 009 took the placeholder portrait out: the name
   starts at the row's own left edge rather than 48px into it. */
/*
 * Three columns: the picture, the words, and whatever the row ends with --
 * a padlock on a theatre that does not exist yet, nothing on the others.
 *
 * The 1fr is `minmax(0, 1fr)` so the words can be *narrower* than their
 * content wants; without the zero minimum a grid column refuses to go below
 * its longest word and the row overflows the rail instead of the text
 * ellipsing. That is what pays for the picture.
 */
.fx-group {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  min-width: 0;
  width: 100%;
  text-align: left;
  padding: 8px 12px;
}
/*
 * The placeholder portrait is gone (700-qa 009) and what is in its place is
 * not it (751-teasing 001).
 *
 * `.fx-group-art` was a 36px square carrying a `linear-gradient` per theatre
 * -- a blend, which the drawing rules forbid outright, standing in for six
 * paintings that were never commissioned. `.fx-group-scene` is 32px, plotted
 * through `rect()` out of the zone's own tile paints, and there is one for
 * every row on the rail.
 */
.fx-group-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/*
 * The name gets the small size, and it has to: a 32px picture and a 17px
 * padlock take 59 of the rail's 280 pixels before a letter is drawn, and
 * "THE FROZEN NORTH" at `--fs-body` in what is left wraps to two lines and
 * shoves the meta line out of the plate. Ellipsis rather than wrap, for the
 * same reason the meta line is `nowrap` -- a name broken across two lines is
 * a different name at a glance.
 */
.fx-group-name {
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* nowrap on both halves: "15 MISSIONS" breaking after the number reads as a
   different number, which is worse than the line being tight. */
.fx-group-meta { display: flex; justify-content: space-between; gap: 8px; white-space: nowrap; }
/* A step below the name, which is itself a step below what it was: the row
   now carries a picture, and the count is the least of the three things in
   it. `--fs-micro` is the caption size the mission cards' own tail uses. */
.fx-group-count { color: #9db97a; font-size: var(--fs-micro); }
.fx-group-done { color: var(--gold); font-size: var(--fs-micro); }
.fx-group.on { border-image-source: var(--sk-btn-hot); }
/* A theatre the stars have not bought yet: present, priced, and it opens to
   show its locked maps (740-qa 025) -- so it is a button like the others,
   on the iron plate a locked card wears, with its name still readable
   (740-qa 028: `--ink-mute` is 2.3:1 and is for rules and disabled states;
   a goal you are meant to want has to be legible). */
.fx-group.shut { border-image-source: var(--sk-btn-quiet); }
.fx-group.shut .fx-group-name { color: var(--ink-faint); }
.fx-group.shut .fx-group-count { color: var(--ink-faint); }
/*
 * A theatre that does not exist yet (751-teasing 001).
 *
 * The brief's two prohibitions are what shapes this: *"do not make all three
 * identical grey disabled buttons"* and *"the zones should look desirable and
 * clickable rather than simply disabled"*. So it is not `.shut`'s iron plate
 * and not dimmed -- it is the ordinary olive plate every open theatre wears,
 * with a picture of its ground on the left and a padlock at the end of the
 * row. The row is a button, it lights on hover, and pressing it opens
 * something.
 *
 * It carried the word `COMING` under the name for one afternoon and the owner
 * had it out. Nothing replaced it: the picture, the name and the lock say it
 * between them, and the row is the only one on the rail with a single line of
 * text in it -- which is itself the tell that there is nothing to count.
 */
.fx-group-scene {
  /* An exact 2x of the 16x16 bake. The `.fx-lock` lesson: a pixel sprite on a
     fractional scale is the one thing the drawing laws are absolute about. */
  width: 32px; height: 32px;
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
/*
 * The padlock, in the row's last column and centred in it.
 *
 * It sat inside the meta line first, sharing a `space-between` with the word
 * COMING; when the word went it slid to the left and sat against the name.
 * Its own grid cell is where a mission card puts its stars, so the locks on
 * three consecutive rows line up with each other and with the rail's right
 * edge -- which is the whole of what "aligned" means here.
 *
 * Half the mission card's padlock: 34x44 there is the 17x22 cut at 2x, so
 * this is the same cut at 1x. Not a resample -- the one thing the drawing
 * laws are absolute about.
 */
.fx-group-lock {
  width: 17px; height: 22px;
  background-image: var(--sk-lock);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
/* The dim meta tone is set for the resting plate; on the lit one it vanishes. */
.fx-group.on .fx-group-count { color: #2a2408; }
.fx-group.on .fx-group-done { color: #1c1804; }
.fx-group.on .fx-group-name { color: #fffbe8; }

/* --- the mission cards */
/* The one box on this screen that scrolls. THE JUNGLE alone is twenty cards
   -- roughly 2970px -- so the alternative is the page scrolling, which is
   the thing 501-000 exists to stop. The gutter keeps the bar off the cards'
   plotted right edge. */
#select-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
}
.fx-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 18px;
  width: 100%;
  padding: 12px 18px;
  text-align: left;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-plate) var(--sk-slice-plate) fill / 10px round;
  color: #f6efd8;
  font: var(--fs-body)/1.3 var(--face);
  cursor: pointer;
  image-rendering: pixelated;
}
.fx-card:hover, .fx-card:focus-visible { border-image-source: var(--sk-plate-hot); outline: none; }
.fx-card.locked { border-image-source: var(--sk-plate-off); color: #6d7254; cursor: default; }
/* A big gold numeral against the name, which is the reference's arrangement. */
.fx-card-num { color: var(--gold); font-size: var(--fs-title); line-height: 1; min-width: 66px; }
.fx-card.locked .fx-card-num { color: #4e5230; }
.fx-card-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.fx-card-name { font-size: var(--fs-lg); letter-spacing: var(--track-tight); }
/* Not italic. The brief is explicit and a bitmap face has no italic anyway. */
.fx-card-desc { color: #a3b47c; font-size: var(--fs-body); }
.fx-card.locked .fx-card-desc { color: #5c6142; }
.fx-card-tail { display: flex; align-items: center; gap: 6px; }

.fx-stars { display: flex; gap: 4px; }
.fx-star {
  width: 34px; height: 34px;
  background-image: var(--sk-star-off);
  background-size: 100% 100%;
  image-rendering: pixelated;
}
.fx-star.on { background-image: var(--sk-star-on); }
.fx-lock {
  /* An exact 2x of the 17x22 cut. It was 30x34 over a 15x16 bake, which is 2.0
     across and 2.125 down -- a resampled pixel sprite, in the one place the
     laws are absolute about. */
  width: 34px; height: 44px;
  background-image: var(--sk-lock);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}

/* --- the foot: difficulty, and the way back */
#select-foot {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  margin-top: 16px;
}
.fx-diffs { display: flex; gap: 8px; flex-wrap: wrap; }
.fx-diff, .fx-back { min-width: 0; padding: 10px 20px; }
.fx-diff.on { border-image-source: var(--sk-btn-hot); color: #fffbe8; }

/* The rail stacks under the list on anything narrow; a 280px column and a
   mission card cannot both have room on a phone. */
@media (max-width: 780px) {
  #select-body { grid-template-columns: minmax(0, 1fr); }
  #select-rail { flex-direction: row; overflow-x: auto; }
  .fx-group { width: auto; min-width: 220px; }
  .fx-btn { min-width: 0; width: 100%; }
}
.hud-tool {
  width: 44px;
  height: 40px;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  cursor: pointer;
  padding: 0;
}
.hud-tool:hover, .hud-tool:focus-visible { border-image-source: var(--sk-btn-hot); outline: none; }
.hud-tool:active { border-image-source: var(--sk-btn-down); }
.hud-tool:disabled { border-image-source: var(--sk-btn-off); cursor: default; }
/*
 * The glyph changes with the plate, which is the half that was missing.
 *
 * The border already swapped on hover and press; the icon stayed one flat cream
 * in all four states, so a pressed button was a bright frame round an unchanged
 * picture -- which reads as a rendering fault rather than as a press. Four
 * rules per icon, in specificity order: rest, hover, active, disabled.
 */
.hud-tool.t-back::after { background-image: var(--sk-ic-back); }
.hud-tool.t-close::after { background-image: var(--sk-ic-close); }
.hud-tool.t-close:hover::after, .hud-tool.t-close:focus-visible::after { background-image: var(--sk-ic-close-hot); }
.hud-tool.t-close:active::after { background-image: var(--sk-ic-close-down); }
.hud-tool.t-back:hover::after, .hud-tool.t-back:focus-visible::after { background-image: var(--sk-ic-back-hot); }
.hud-tool.t-back:active::after { background-image: var(--sk-ic-back-down); }
.hud-tool.t-exit::after { background-image: var(--sk-ic-door); }
.hud-tool.t-restart::after { background-image: var(--sk-ic-restart); }
.hud-tool.t-pause::after { background-image: var(--sk-ic-pause); }

.hud-tool.t-exit:hover::after, .hud-tool.t-exit:focus-visible::after { background-image: var(--sk-ic-door-hot); }
.hud-tool.t-restart:hover::after, .hud-tool.t-restart:focus-visible::after { background-image: var(--sk-ic-restart-hot); }
.hud-tool.t-pause:hover::after, .hud-tool.t-pause:focus-visible::after { background-image: var(--sk-ic-pause-hot); }

.hud-tool.t-exit:active::after { background-image: var(--sk-ic-door-down); }
.hud-tool.t-restart:active::after { background-image: var(--sk-ic-restart-down); }
.hud-tool.t-pause:active::after { background-image: var(--sk-ic-pause-down); }

.hud-tool.t-exit:disabled::after { background-image: var(--sk-ic-door-off); }
.hud-tool.t-restart:disabled::after { background-image: var(--sk-ic-restart-off); }
.hud-tool.t-pause:disabled::after { background-image: var(--sk-ic-pause-off); }

/* The standing orders, under the mission name. Always on screen. */
.hud-goal {
  margin-top: 4px;
  color: #a3b47c;
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  line-height: 1.4;
}

/* --- the confirmation ----------------------------------------------------
 *
 * Above the pause sheet (20) and everything of the game's, below the dev
 * panel (90): a question outranks whatever raised it.
 */
.confirm-layer {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(8, 11, 5, 0.78);
}
.confirm-title {
  color: var(--gold);
  font-family: var(--face-title);
  font-size: var(--fs-lg);
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: uppercase;
  text-shadow: 3px 3px 0 var(--gold-shadow);
}
/* The typed answer (501-multiplayer 016). One field, the game's own face,
   and no browser chrome: a rounded native box in a plotted dialog reads as
   a different application borrowed for a moment. */
.mp-stats {
  margin: 10px 0 0;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
.confirm-field { display: flex; flex-direction: column; gap: 8px; }
.confirm-field-label {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
}
/* The name field is the global one -- see `.fx-input` above. */

.confirm-body {
  margin-top: 14px;
  color: var(--ink);
  font-size: var(--fs-body);
  line-height: 1.5;
}
.confirm-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 22px;
}
/* The privacy policy (930-analytics 013): legal text in the one dialog the
   game has. Left-aligned and scrolling, because five short sections are
   still longer than a card, and centred prose does not read. */
.privacy {
  text-align: left;
  max-height: min(60vh, 520px);
  overflow-y: auto;
  padding-right: 8px;
  font-size: var(--fs-sm);
}
.privacy h3 {
  margin: 14px 0 4px;
  font-size: var(--fs-sm);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--gold);
}
.privacy p, .privacy ul { margin: 0 0 6px; color: var(--ink-dim); }
.privacy ul { padding-left: 18px; }
.privacy strong { color: var(--ink); font-weight: inherit; }
.privacy a { color: var(--ink); }
.privacy-updated { color: var(--ink-faint); margin: 0; }
#front-foot .foot-sep { margin: 0 6px; color: var(--ink-mute); }
/* The auto-close count on a card with nothing to press -- the interlude
   between two rounds (750-qa-multiplayer 008). Where a button would be, in
   the dim ink the meter heads use, so it reads as a clock and not a call. */
.confirm-count {
  padding: 8px 0;
  color: var(--ink-dim);
  font-size: var(--fs-micro);
  letter-spacing: var(--track);
  text-transform: uppercase;
}
/*
 * A dialog with one button gives it the width (owner, 4 Sep 2026). Green
 * because this one is an affirmative -- the row's only other exit is the
 * corner X, and a lone brass button beside a corner X reads as a pair of
 * equals rather than as "yes" and "not now".
 */
.confirm-actions:only-child .confirm-btn,
.confirm-actions .confirm-btn:only-child { min-width: 320px; }
.fx-btn.good { border-image-source: var(--sk-btn-good); }
.fx-btn.good:hover, .fx-btn.good:focus-visible { border-image-source: var(--sk-btn-good-hot); }
.fx-btn.good:active { border-image-source: var(--sk-btn-good-down); }

/* --- the sidebar tools fill their row, and stay square -------------------
 *
 * Scoped to the row, because `.hud-tool` is a component two screens borrow
 * now. Unscoped, `flex: 1 1 0` and `aspect-ratio: 1` made the lobby's head
 * button grow to 564x564 -- the whole width it was given, squared. */
.hud-tools {
  /* Merged up from the duplicate declaration that used to sit at line 3123 (600-infra 019).
   *
   * No rule above the row (700-qa 015). It was `border-top: 1px solid
   * var(--plate-lo)`, and at #161b0b on the sidebar's plate that is a hard
   * black hairline -- the owner read it as a fault rather than as a divider,
   * which is what it is. The gap and the three plates separate the row from
   * the groups above it perfectly well without a line. */
  display: flex;
 gap: 8px; padding: 8px 7px 4px;
}
.hud-tools .hud-tool {
  flex: 1 1 0;
  width: auto;
  height: auto;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 0;
  /* The size three of them come to. A mode that hides one or two (750-qa-
     multiplayer 006) must not hand the rest the whole row: a blind read of
     the match panel found two 101px squares under a 232px void, and one
     button alone would have been the width of the sidebar (009). */
  max-width: 60px;
}
/* A mode hides the tools it has no use for (750-qa-multiplayer 006). The
   `display: grid` above outranks the browser's own `[hidden]`, so this sits
   after it at the same specificity plus the attribute. A check that read
   `n.hidden` passed with the button on screen; the harnesses read
   visibility now. */
.hud-tools .hud-tool[hidden] { display: none; }
.hud-tool::after {
  /* Merged up from the duplicate declaration that used to sit at line 3145 (600-infra 019). */
  content: '';
  display: block;
  width: 30px;
  height: 30px;
  background: center / 100% 100% no-repeat;
  image-rendering: pixelated;
 margin: 0;
}

/*
 * The corner door: the way out of a screen (the lobby, the level select) or
 * of a dialog. It hangs off the top-right corner, a little outside it, so it
 * reads as fixed to the panel rather than as another item in the row of text
 * beside it.
 *
 * **It has to come after every plain `.hud-tool` rule, and it has to be more
 * specific than one.** It did not, and lost: `.hud-tool` at equal weight
 * further down the file put the button back to 44x40, which leaves a 24x20
 * content box holding a 30x30 glyph -- and without the grid centring (also
 * lost) the picture starts at the content box's top-left and spills out of
 * the bottom-right corner, which is exactly where the owner found it.
 *
 * 50px is arithmetic, not taste. The plate's border is 10px a side, so the
 * content box is 30px; the icon sprite is 15px square drawn at 2x, which is
 * 30px. Any other size either centres a 30px picture in a box that is not
 * 30px, or scales the sprite by a fraction, which this renderer does not do.
 * `margin: 0` for the same reason: the sidebar's `-3px` nudge is for a 44x40
 * tool sitting in a row.
 */
.hud-tool.fx-corner-door, .hud-tool.lb-head-door, .hud-tool.frame-door {
  position: absolute;
  /*
   * -28px, not -10px, and the difference is the frame.
   *
   * An absolute offset resolves against the containing block's *padding*
   * box, and every host here -- both panel frames and the dialog card --
   * wears an 18px border-image. So `-10px` spent the whole offset climbing
   * the border and still landed 8px inside the panel's outer edge, which is
   * what the owner kept seeing: a button tucked into the corner rather than
   * escaping it. The border has to be paid first, then the overhang.
   */
  top: calc(-1 * (18px + 10px));
  right: calc(-1 * (18px + 10px));
  z-index: 2;
  width: 50px;
  height: 50px;
  display: grid;
  place-items: center;
  padding: 0;
}
.hud-tool.fx-corner-door::after, .hud-tool.lb-head-door::after, .hud-tool.frame-door::after {
  width: 30px;
  height: 30px;
  margin: 0;
}

/* --- the sheet, in the plotted chrome ------------------------------------
 *
 * The settings looked like what they were: a 420px panel of CSS borders from
 * before the chrome existed, holding twenty-pixel type it was never sized for
 * -- the five-segment zoom row ran clean off its right edge. Wider, dressed in
 * the frame, and each row stacks its control under its label instead of
 * fighting it for one line.
 */
.sheet-card {
  /* Merged up from the duplicate declaration that used to sit at line 1740 (600-infra 019). */
  /* Scrolls inside the viewport rather than growing past it: the difference
     between a settings list that works on a 390x844 phone and one whose last
     two rows cannot be reached. The cap and the scrolling are `.panel-scroll`'s
     now (qa-polish 001) -- the card used to scroll as a whole, which took its
     own buttons with it. */
  border-top: 2px solid var(--plate-hi);
  border-left: 2px solid var(--plate-hi);
  border-right: 2px solid var(--edge);
  border-bottom: 2px solid var(--edge);
  width: min(620px, 100%);
  background: none;
  image-rendering: pixelated;
  padding: 24px 26px 20px;
  box-shadow: none;
}
.sheet-title {
  /* Merged up from the duplicate declaration that used to sit at line 1757 (600-infra 019). */
  font-weight: 700;
  color: var(--gold);
  font-family: var(--face-title);
  font-size: var(--fs-lg);
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: uppercase;
  text-align: center;
  text-shadow: 3px 3px 0 var(--gold-shadow);
}
.sheet-row {
  /* Merged up from the duplicate declaration that used to sit at line 1798 (600-infra 019). */
  flex-wrap: wrap;
  border-bottom: 1px solid var(--plate-lo);
 display: flex; flex-direction: column; align-items: stretch; gap: 10px; padding: 14px 0;
}
.sheet-row > .ui-segmented {
  /* Merged up from the duplicate declaration that used to sit at line 1810 (600-infra 019). */ flex: 0 1 auto;
 margin-left: 0;
}
.ui-segmented {
  /* Merged up from the duplicate declaration that used to sit at line 1841 (600-infra 019). */
  display: flex;
  flex: none;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
 flex-wrap: wrap; gap: 6px; overflow: visible;
}
.ui-seg {
  /* Merged up from the duplicate declaration that used to sit at line 1851 (600-infra 019). */
  flex: none;
  border-top: 1px solid var(--plate-hi);
  border-left: 1px solid var(--plate-hi);
  border-right: 1px solid var(--plate-lo);
  border-bottom: 2px solid var(--plate-lo);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 700;
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  cursor: pointer;
  scroll-snap-align: start;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn) var(--sk-slice-btn) fill / 10px round;
  image-rendering: pixelated;
  color: #d9d2ba;
  min-height: 0;
  padding: 6px 14px;
}
.ui-seg.on {
  /* Merged up from the duplicate declaration that used to sit at line 1869 (600-infra 019). */ background: var(--plate-hi);
  border-image-source: var(--sk-btn-hot);
  color: #fffbe8;
  box-shadow: none;
}

/* The pause sheet's shape: resume large and alone, the small print sharing
   one row beneath it (200-qa 024). */
.sheet-actions .sheet-primary {
  font-size: var(--fs-lg);
  padding: 18px 10px;
}
.sheet-split { display: flex; gap: 10px; }
.sheet-split .ui-btn { flex: 1 1 0; }

/* Casualties of turning the row into a column: the label kept its 190px
   flex-basis and the level bar its 180px one, which in a column are pixels of
   *height*. Both corrected against the axis they actually live on now. */
.sheet-row-text { flex: 0 0 auto; min-width: 0; }
.sheet-row > .ui-slider { flex: 0 0 auto; min-width: 0; }
.sheet-rows {
  /* Merged up from the duplicate declaration that used to sit at line 1790 (600-infra 019). */ margin-top: 14px;
 padding-bottom: 8px;
}
/*
 * "Clear my data", at the foot of Settings.
 *
 * Set apart from `.sheet-row` on purpose: those are choices that take effect
 * as you make them, and this one asks a question first. A heavier rule above
 * it and a gap say "this is not another setting" without needing a heading,
 * which is the same argument that took four section headings off this sheet.
 *
 * Laid out as a row where there is room and stacked where there is not, which
 * is what `.sheet-row` does at the same breakpoint -- the sheet is the screen
 * that grows on a phone.
 */
.sheet-danger {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 2px solid var(--plate-lo);
}
.sheet-danger .sheet-row-text { flex: 1 1 auto; min-width: 0; }
.sheet-danger .ui-btn { flex: 0 0 auto; }
.sheet-actions {
  /* Merged up from the duplicate declaration that used to sit at line 1777 (600-infra 019). */
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin-top: 18px;
 padding: 4px 4px 0;
}

/* --- the modifiers and the par -------------------------------------------
 *
 * These were the mission dialog's, and 004 deleted the dialog. They are on
 * the briefing now (`.briefing-chips`), which is the screen that carries
 * everything that dialog did -- the classes are kept because it is the same
 * information wearing the same clothes. `.mi`, `.mi-desc` and `.mi-meta`
 * went with the body that held them. */
.mi-chip {
  padding: 2px 8px;
  background: rgba(0, 0, 0, 0.28);
  box-shadow: inset 0 0 0 1px #3b4d26;
  color: #9db97a;
  font-size: var(--fs-body);
  letter-spacing: var(--track-tight);
}
.mi-chip.warn { color: #e0a050; box-shadow: inset 0 0 0 1px #6b4a20; }
.mi-best { color: var(--gold); font-size: var(--fs-body); letter-spacing: var(--track-tight); }

/* One line of difficulties, stars on a line of their own, no fourth button. */
/* 740, measured: the three difficulty buttons come to 629px with their gaps,
   and 680 left 584 inside the frame -- one line was the whole point. */
.confirm-card {
  /* Merged up from the duplicate declaration that used to sit at line 3212 (600-infra 019). */
  position: relative;
  padding: 30px 30px 26px;
  image-rendering: pixelated;
  text-align: center;
 width: min(740px, 92vw);
}
.confirm-btn { min-width: 130px; }
.mi-earned {
  /* Merged up from the duplicate declaration that used to sit at line 3405 (600-infra 019). */ display: flex; align-items: center;
 flex-direction: column; gap: 8px;
}

/* ---------------------------------------------------------- the arena
   A screen with nobody playing it: no squad, no orders, no aim. The crosshair
   is the squad's cursor, so without a squad it is a promise the page cannot
   keep -- the ordinary pointer goes back. Set from `main.ts` as one data
   attribute, alongside `input.mode = 'spectator'`. */
[data-mode="spectator"] #screen { cursor: default; }
/* The sidebar is a squad roster, a supply count and an orders list, and the
   action bar is FIRE, GRENADE and RECENTRE. A spectator has no squad, no
   supplies and no orders, so all of it is furniture describing a game that is
   not being played. Hidden here rather than by not building it, so the arena
   and a mission share one HUD. */
[data-mode="spectator"] #hud,
[data-mode="spectator"] #controls,
[data-mode="backdrop"] #hud,
[data-mode="backdrop"] #controls { display: none; }
/* ...and the column it occupied has to collapse, or the canvas keeps a
   sidebar's width of nothing beside it. `#stage` is a grid, so hiding the
   sidebar is not enough on its own -- the track is still there. */
[data-mode="spectator"] #stage,
[data-mode="backdrop"] #stage { grid-template-columns: minmax(0, 1fr); }

/* ------------------------------------------------- the attract world
   The battle running behind the front end. `#front` already sits over
   `#stage` and its background was the ground the whole screen stood on;
   with a battle down there it becomes a vignette instead -- dark at the
   edges so the plates and the mission list stay readable, thin in the
   middle so the fighting shows through.

   This is the one soft gradient in the game and it is allowed because it
   is *DOM chrome*, not drawing: it never touches the canvas and so cannot
   put a soft edge into a dithered image. The visual laws are about what
   is plotted. Do not "fix" this by dithering it -- a dithered vignette
   over live pixels crawls.

   It is deliberately heavy. A menu you cannot read is a worse outcome
   than no attract world, and the battlefield is a busy mid-green with
   muzzle flashes going off in it. */
[data-mode="backdrop"] #front {
  background:
    radial-gradient(ellipse at 50% 40%,
      rgba(9, 13, 6, 0.42) 0%,
      rgba(9, 13, 6, 0.74) 46%,
      rgba(7, 10, 4, 0.93) 100%);
}
/* The level select is a wall of small text where the intro is a logo and
   three buttons, so it takes another step of darkening. */
[data-mode="backdrop"] #front.on-select {
  background:
    radial-gradient(ellipse at 50% 40%,
      rgba(9, 13, 6, 0.66) 0%,
      rgba(9, 13, 6, 0.86) 46%,
      rgba(7, 10, 4, 0.96) 100%);
}

/* The spectator readout. Thin on purpose: the mission sidebar is all about a
   squad you own and a task you were set, and a watcher has neither. */
.arena-bar {
  position: absolute;
  left: 50%;
  top: 8px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 5px 14px;
  background: var(--plate-lo, #1a1c14);
  border: 2px solid var(--plate-hi, #3d4231);
  /* `--pixel-font` never existed; the fallback was doing all the work. The
     face is `--face`, and 11px is not a multiple of ten -- but this bar is
     the arena's debug overlay rather than chrome the player reads, so it
     keeps the size it was tuned at (600-infra 007). */
  font: 11px/1.2 var(--face, monospace);
  letter-spacing: 0.5px;
  color: var(--ink, #d8e0c0);
  pointer-events: none;
  white-space: nowrap;
  z-index: 5;
}
.ab-side { display: inline-flex; align-items: baseline; gap: 0.55em; }
.ab-side b { font-weight: normal; }
.ab-side i { font-style: normal; opacity: 0.55; font-size: 0.8em; }
.ab-green b { color: #8ad655; }
.ab-red b { color: #7f9ad8; }
/* The hens, where a map made them a third side (more-than-two-sides 08). A
   warm bird-brown that sits beside the green and the blue without competing
   with either -- they are the joke on the bar, not a third army. */
.ab-hen b { color: #d8a24a; }
.ab-vs { opacity: 0.4; font-size: 0.8em; }
/* The bar is the only place the two viewing keys are written down, and it
   goes away with them when the bar is hidden -- which is correct: somebody
   who has just pressed H knows what H does. */
.ab-keys { opacity: 0.4; font-size: 0.8em; margin-left: 4px; }
/* Over the intro it is the score of the wallpaper battle -- both sides on
   one row, each side's kills in that side's colour -- set in the front
   end's own small type so it reads as part of the menu rather than a debug
   readout left on. It belongs to the intro alone: the level select is
   already a wall of small text, and a scoreboard over it is one more. */
#front .arena-bar {
  top: 14px;
  gap: 1.6em;
  padding: 8px 20px;
  font: var(--fs-sm)/1 var(--face);
  letter-spacing: var(--track);
}
#front.on-select .arena-bar { display: none; }
.ab-score { display: inline-flex; gap: 0.6em; }
.ab-score b { font-weight: normal; }
.ab-score.ab-green { color: #8ad655; }
.ab-score.ab-red { color: #7f9ad8; }
.ab-score.ab-hen { color: #d8a24a; }

/* ------------------------------------------- "play some awesome music"
   The offer that turns the browser's autoplay rule into part of the game.
   See ui/musicprompt.ts for why it exists at all.

   Deliberately NOT a plate. PLAY NOW is what the intro is for, and a third
   plated button under it would compete with the two that matter -- so this
   is a line of text with a speaker on it, smaller and warmer, reading as a
   note under the menu rather than an item in it. */
.fx-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 2px;
  padding: 7px 16px;
  background: none;
  border: 0;
  /* Was a hard-coded #a9b47c, because `--ink-faint` used to vanish over a
     vignetted battlefield. The token reads at 5:1 now (310-qa 016), so the
     workaround is the token's job again. */
  color: var(--ink-faint);
  font: var(--fs-sm)/1 var(--face);
  letter-spacing: var(--track);
  text-transform: uppercase;
  cursor: pointer;
  image-rendering: pixelated;
  /* Two stepped frames, not a pulse: it should look plotted, not animated. */
  animation: cta-breathe 1.8s steps(2, end) infinite;
}
.fx-cta svg { width: 20px; height: 20px; flex: 0 0 auto; }
.fx-cta .cone { fill: currentColor; }
.fx-cta .wave { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.fx-cta:hover, .fx-cta:focus-visible {
  color: #ffe9a0;
  outline: none;
  animation: none;
}
.fx-cta:active { transform: translateY(1px); }

@keyframes cta-breathe {
  0%   { color: #a9b47c; }
  50%  { color: #e2d290; }
  100% { color: #a9b47c; }
}

/* The send-off. Short, loud, and stepped -- an element that merely vanishes
   reads as a page glitch, and this is the only thing moving on screen at the
   moment it happens. `steps()` keeps it chunky rather than a smooth fade,
   which is the house rule about how things move here. */
.fx-cta.going {
  animation: cta-go 0.42s steps(7, end) forwards;
  pointer-events: none;
}
@keyframes cta-go {
  0%   { transform: scale(1);    color: #fffbe8; }
  30%  { transform: scale(1.18); color: #fffbe8; }
  60%  { transform: scale(1.05); color: #ffe9a0; }
  100% { transform: scale(0.55); color: #ffe9a0; opacity: 0; }
}

/* Somebody who has asked for less movement gets the button and none of the
   fidgeting; it still pops once on the way out, because that pop is feedback
   for their own click rather than decoration. */
@media (prefers-reduced-motion: reduce) {
  .fx-cta { animation: none; }
}

/* ===========================================================================
   THE DISPATCH (940-dispatch)

   The full page between the mission list and the briefing. It has no plate
   and wants none: both screens on either side of it are already black, so
   this draws text straight onto that black and the seam between the three is
   nothing at all. A frame here would announce a third screen.

   Its own black is opaque rather than transparent, because #blackout sits at
   8 and this must cover the stage as completely as the front screen does --
   but the two are the same colour, so nothing is seen to arrive.
   =========================================================================== */

.dsp-layer {
  position: fixed;
  inset: 0;
  z-index: 13;               /* over #overlay (12), under #front (14) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 24px;
  overflow-y: auto;
  background: var(--edge);
  opacity: 0;
  transition: opacity var(--t-dispatchFade) linear;
}
.dsp-layer.in { opacity: 1; }
/*
 * Over the front screen, not under it (751-teasing 001).
 *
 * The z-index above is set for the dispatch's own moment -- between the front
 * resolving and the briefing arriving, when nothing else is up. A door on the
 * front opens the same card while `#front` (14) is still on screen, and at 13
 * that is a modal layer nobody can see: the game reads as frozen. Only the
 * doors set this; `runDispatch` never does.
 */
.dsp-layer.raised { z-index: 15; }

.dsp-inner {
  width: 100%;
  /*
   * Wide enough for the two buttons side by side with their labels on one
   * line -- "I'D BUY THIS -- $9.99" is the longest, and a label that wraps
   * inside a plate reads as a mistake.
   *
   * The prose does *not* use this width: `.dsp-said` and `.dsp-plain` cap
   * themselves below, because a line of text this wide is a line the eye
   * loses its place on. The column is sized by the buttons and the words sit
   * in a narrower measure inside it.
   */
  max-width: 860px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* --------------------------------------------------------------- the man */

/* The same three-part contract the comms panel uses: one baked strip of every
   frame, moved with background-position, and the frame index as a custom
   property so the arithmetic is the stylesheet's. Bigger here than on the
   strip -- he is the only thing on the screen. */
.dsp-who { display: flex; align-items: center; gap: 14px; }
.dsp-face {
  --dsp-face-f: 0;
  /* One frame until the bake says otherwise; the script sets the real count
     from --sk-face-<id>-n. Declared so the stylesheet stands on its own. */
  --dsp-face-n: 1;
  --dsp-face-px: 64px;
  position: relative;
  flex: 0 0 auto;
  width: var(--dsp-face-px);
  height: var(--dsp-face-px);
  image-rendering: pixelated;
}
.dsp-portrait {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: calc(-1 * var(--dsp-face-f) * var(--dsp-face-px)) 0;
  background-size: calc(var(--dsp-face-n) * var(--dsp-face-px)) var(--dsp-face-px);
  image-rendering: pixelated;
}
.dsp-name {
  color: var(--gold);
  font-size: var(--fs-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

/* His line. Uppercase like every other thing he says, and holding its height
   so the card below does not walk down the screen as the text types in. */
.dsp-said {
  color: var(--ink);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  line-height: 1.45;
  min-height: calc(2 * 1.45em);
  margin: 0;
}

/* ------------------------------------------------------------- the cards */

.dsp-body { display: flex; flex-direction: column; gap: 14px; }

/* Plain text, in nobody's voice: the one place in the game the fiction is
   set down, so it is *not* uppercase and not gold. It should read as a person
   typing rather than as the game speaking. */
/* The measure the words are read at, inside a column sized by the buttons. */
.dsp-said, .dsp-plain, .dsp-list { max-width: 640px; }
.dsp-centred .dsp-plain, .dsp-centred .dsp-said { margin-left: auto; margin-right: auto; }
.dsp-plain {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.5;
  margin: 0;
}
.dsp-question { color: var(--ink); }
.dsp-headline {
  color: var(--gold);
  font-size: var(--fs-lg);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  margin: 0;
}
/* Only ever a sentence about a send that failed, so it takes no room until
   there is one: an always-reserved line is what pushed SEND away from the
   box it belongs to (owner, 7 Sep 2026). */
.dsp-note { color: var(--ink-dim); font-size: var(--fs-micro); margin: 0; }
.dsp-note:empty { display: none; }

/* The shop window: a row of what they could afford, cheapest first. */
.dsp-balance { display: flex; align-items: center; gap: 8px; color: var(--gold); font-size: var(--fs-sm); margin: 0; }
.dsp-coin { image-rendering: pixelated; }
.dsp-shelf { display: flex; gap: 22px; flex-wrap: wrap; }
/* A fixed column, not a minimum: the names are three words or one, and a
   cell that grows to its label leaves the shelf ragged. */
.dsp-good { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 132px; }
/* One baseline for three different heights, so every name starts on the same
   line. Tall enough for the tallest item at 2x, and bottom-aligned. */
.dsp-good-stand { display: flex; align-items: flex-end; justify-content: center; height: 64px; }
.dsp-good-art { image-rendering: pixelated; }
.dsp-good-name { color: var(--ink); font-size: var(--fs-micro); text-transform: uppercase; text-align: center; line-height: 1.25; }

/* The shop window is a display, not a sentence, so it is centred -- and its
   buttons with it, or the row hangs off one side of what it belongs to. The
   other two cards stay left: they are text, and centred prose is a poster. */
.dsp-centred { align-items: center; text-align: center; }
.dsp-centred-row { justify-content: center; }

/*
 * The rating: the game's own plotted star, in a real button.
 *
 * The first version was a text glyph at 45% opacity and the owner's note was
 * that it did not look clickable -- correctly, because it was not a control,
 * it was some grey text. A button brings the cursor, the focus ring and the
 * keyboard; the sprite is only the picture on it; and the row fills to the
 * star under the pointer, which is the one gesture every rating control has
 * shared for twenty years.
 */
.dsp-stars { display: flex; gap: 10px; align-items: center; }
.dsp-star {
  background: none;
  border: 0;
  padding: 6px;
  margin: 0;
  cursor: pointer;
  line-height: 0;
}
.dsp-star .fx-star { width: 40px; height: 40px; display: block; }
/* The lift is the pressable tell, and it is a whole pixel: this face is
   plotted and a fractional translate would resample it. */
.dsp-star:hover .fx-star,
.dsp-star:focus-visible .fx-star { transform: translateY(-1px); }
.dsp-star:active .fx-star { transform: translateY(1px); }
.dsp-star:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
.dsp-star:disabled { cursor: default; }
/* Under the row until they choose, then the number they chose. */
.dsp-hint {
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.dsp-stars.picked ~ .dsp-hint { color: var(--gold); }

/*
 * The first textarea in the game, wearing the callsign input's plate.
 *
 * Not uppercase, unlike `.fx-input` and its two siblings: those take a
 * callsign and a join code, which are shouted, and this takes a sentence
 * somebody is writing about the game. Forcing caps on a paragraph makes it
 * unreadable and makes the asking feel like a form.
 */
.dsp-box,
.dsp-input {
  width: 100%;
  min-width: 0;
  background: none;
  border: 10px solid transparent;
  border-image: var(--sk-btn-down) var(--sk-slice-btn) fill / 10px round;
  color: var(--ink);
  font: var(--fs-sm)/1.5 var(--face);
  letter-spacing: var(--track-tight);
  padding: 12px 16px;
  image-rendering: pixelated;
}
/* `--ink-mute` is deliberately below reading contrast, and the token's own
   comment says never to use it for text somebody is meant to read. A
   placeholder is exactly that: it is the instruction until they type. The
   same complaint was made about `.fx-input` and settled the same way. */
.dsp-box::placeholder,
.dsp-input::placeholder { color: var(--ink-faint); }
.dsp-box { resize: vertical; min-height: 4.5em; }

/* The offer's own spacing (owner, 7 Sep 2026). Six bullets, a headline, a
   sub and a price stacked at one gap read as a wall; the list gets room to
   breathe and the price is set apart from it, because the price is the thing
   the whole screen is asking about. */
.dsp-offer .dsp-pane { display: flex; flex-direction: column; gap: 16px; }
.dsp-list {
  margin: 0;
  padding-left: 1.2em;
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.9;
}
/* Set apart from the list above it: the price is the thing the whole screen
   is asking about, and it should not read as a seventh bullet. */
.dsp-price {
  color: var(--gold);
  font-size: var(--fs-lg);
  text-transform: uppercase;
  margin: 10px 0 0;
}

/* ----------------------------------------------------------- the buttons */

/*
 * Always a row at the foot, always the same two shapes, so "leave this" is in
 * the same place on all three cards. 790-qa's rule for the result panel, and
 * the same argument: the buttons must not move with the content.
 *
 * The tones are the front screen's, so this reads as the same game: gold for
 * the thing worth doing, green for an affirmative that sends something, and
 * the dark plate for the way past. A row of identical olive buttons was the
 * owner's note -- with no tone, nothing says which one is the answer.
 */
/*
 * The two share the row equally and never wrap.
 *
 * Sized to their labels they were 310 and 374 wide -- 696 in a 620 column,
 * so they stacked, and stacked at two different widths, which is what the
 * owner saw. Splitting the row makes them identical whatever the words say,
 * which is also 790-qa's rule for the result panel's pair.
 */
.dsp-actions { display: flex; gap: 12px; align-items: stretch; }
/*
 * Equal halves, one line, and a step down from the front screen's size.
 *
 * `.fx-btn` is the 280px hero at `--fs-lg`, which is right for PLAY NOW and
 * too loud here: this screen interrupts somebody on their way into a mission
 * and should ask rather than shout. It is also what makes the longest label
 * the brief prescribes -- "I'D BUY THIS -- $9.99" -- fit on one line inside
 * its plate instead of being cut off at the price, which is the one place a
 * clipped word would actually cost something.
 */
.dsp-go { flex: 1 1 0; min-width: 0; }
.dsp-go .fx-btn-label { white-space: nowrap; font-size: var(--fs-body); }

@media (prefers-reduced-motion: reduce) {
  .dsp-layer { transition: none; }
}
